前言
1.认证服务日志中间件(RequestLoggingMiddleware)
经过之前的学习,项目基本都能跑起来了(除了HybridApp),但是我还是想有始有终,全部学完,如果你此时还运行不了项目,先看看我的验证过程即可
为了了解认证服务(Identity.API)处理了那些请求,方便我们学习OpenID Connect认证授权流程,我在认证服务(Identity.API)添加了一个日志中间件用于输出请求、参数、响应

await _next(context)上面输出请求、参数,下面输出响应结果

2.启动客户端
启动认证服务、webhooks服务、webhook前端,点击登录

3.启动认证服务端
认证服务端打印了日志,收到了GET /请求,返回了首页html
info: Identity.API.RequestLoggingMiddleware[0]********** GET / Start **********========== Incoming Request ==========GET /======================================info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 200********** GET / End *

4.客户端请求认证服务OIDC(/.well-known/openid-configuration)
客户端首先请求 /.well-known/openid-configuration,IdentityServer 返回 OIDC 发现文档,详细说明服务器的地址、授权端点、Token 端点、公钥信息、支持的 Scope、Claim、授权模式和响应类型等,使客户端能够根据这些信息自动、安全地完成登录、获取 Token 和验证用户身份的整个认证授权流程。
GET /.well-known/openid-configuration
部分响应字段含义
| 字段 | 说明 |
|---|---|
issuer |
发行者地址(重要) |
authorization_endpoint |
授权端点(浏览器登录跳转用) |
token_endpoint |
获取 Token 的端点 |
userinfo_endpoint |
获取用户信息 |
jwks_uri |
公钥地址,用来验证 JWT Token |
scopes_supported |
支持哪些 Scope |
claims_supported |
支持哪些 Claim |
grant_types_supported |
支持哪些授权模式 |
response_types_supported |
浏览器回调格式 |
code_challenge_methods_supported |
支持 PKCE |
日志
========== Incoming Request ==========GET /.well-known/openid-configuration======================================info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 200--- Body ---{"issuer":"https://localhost:5243","jwks_uri":"https://localhost:5243/.well-known/openid-configuration/jwks","authorization_endpoint":"https://localhost:5243/connect/authorize","token_endpoint":"https://localhost:5243/connect/token","userinfo_endpoint":"https://localhost:5243/connect/userinfo","end_session_endpoint":"https://localhost:5243/connect/endsession","check_session_iframe":"https://localhost:5243/connect/checksession","revocation_endpoint":"https://localhost:5243/connect/revocation","introspection_endpoint":"https://localhost:5243/connect/introspect","device_authorization_endpoint":"https://localhost:5243/connect/deviceauthorization","backchannel_authentication_endpoint":"https://localhost:5243/connect/ciba","pushed_authorization_request_endpoint":"https://localhost:5243/connect/par","require_pushed_authorization_requests":false,"frontchannel_logout_supported":true,"frontchannel_logout_session_supported":true,"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"scopes_supported":["openid","profile","orders","basket","webhooks","offline_access"],"claims_supported":["sub","name","family_name","given_name","middle_name","nickname","preferred_username","profile","picture","website","gender","birthdate","zoneinfo","locale","updated_at"],"grant_types_supported":["authorization_code","client_credentials","refresh_token","implicit","password","urn:ietf:params:oauth:grant-type:device_code","urn:openid:params:grant-type:ciba"],"response_types_supported":["code","token","id_token","id_token token","code id_token","code token","code id_token token"],"response_modes_supported":["form_post","query","fragment"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post"],"id_token_signing_alg_values_supported":["RS256"],"subject_types_supported":["public"],"code_challenge_methods_supported":["plain","S256"],"request_parameter_supported":true,"request_object_signing_alg_values_supported":["RS256","RS384","RS512","PS256","PS384","PS512","ES256","ES384","ES512","HS256","HS384","HS512"],"prompt_values_supported":["none","login","consent","select_account"],"authorization_response_iss_parameter_supported":true,"backchannel_token_delivery_modes_supported":["poll"],"backchannel_user_code_parameter_supported":true,"dpop_signing_alg_values_supported":["RS256","RS384","RS512","PS256","PS384","PS512","ES256","ES384","ES512"]}======================================********** GET /.well-known/openid-configuration End **********

5.客户端请求认证服务公钥(/.well-known/openid-configuration/jwks)
客户端请求 /.well-known/openid-configuration/jwks,IdentityServer 返回当前有效的公钥集合(JWKS),包含 Key ID、算法、模数和指数等信息,供客户端或资源服务器根据 JWT Token 中的 kid 自动验证签名,从而确保 Token 的真实性、完整性和安全,同时支持密钥轮换。
/.well-known/openid-configuration/jwks
响应字段含义
| 字段 | 说明 |
|---|---|
keys |
公钥集合,可能有多个 Key,每个 Key 用于验证不同的 Token 或 Key Rotation |
kty |
Key 类型,这里是 RSA(非对称加密算法) |
use |
Key 用途,这里是 sig(签名),表示用来验证 JWT 签名 |
kid |
Key ID,标识这把 Key 的唯一 ID,JWT 里会有对应 kid,方便找到对应公钥 |
n |
RSA 模数(modulus),是公钥的一部分,用于验证签名 |
e |
RSA 指数(exponent),公钥另一部分,通常是 AQAB(65537) |
alg |
JWT 签名算法,这里是 RS256(RSA + SHA-256) |
日志
info: Identity.API.RequestLoggingMiddleware[0]********** GET /.well-known/openid-configuration/jwks Start **********========== Incoming Request ==========GET /.well-known/openid-configuration/jwks======================================info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.DiscoveryKeyEndpoint for /.well-known/openid-configuration/jwks
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 200--- Body ---{"keys":[{"kty":"RSA","use":"sig","kid":"F7383A0ED7CD960EF473FD2851803938","e":"AQAB","n":"p3jTfCB0YsKpqJ6CNJi0tVmFBmoyI_D7QLLsbB-TCZ3-HIXDEr_k6zKb2GJ_QP7mncdSnYpJWSv7fWPfM0bL3A6NaMLF9MDjbfD5ti9irEW1dzBvIK0YjWmfks3eq6Mb2mM6PZtNEnoCqEzjgcRkDR1vtClEzUjs1E_i7TB-Y0J_aTYpLf-eN7yA1Obu8zMVRSSVBIwG5W5jljzA2nxk2u9qeDq8Sn0qgGwbX8cyYGQoVWBOPx7zap4cNcL6dHILjnlVrHqAUW9NVXtBWlVDP1Gvnm2zhCVJT_gW1twNhswyFULGVQH1ZWI0NukEqHG6LpN8Ti7Hx-K8MEEv_vQBYw","alg":"RS256"}]}======================================********** GET /.well-known/openid-configuration/jwks End **********

6.客户端向认证服务推送授权请求(/connect/par)
客户端通过 POST /connect/par 将完整授权请求推送给 IdentityServer,服务器验证客户端后返回一个 request_uri,客户端随后使用该 URI 进行浏览器跳转完成标准授权码流程,从而实现 URL 安全、参数完整和支持 PKCE 的现代 OIDC 授权方式。
**PAR **
- 全称:Pushed Authorization Request
- RFC:RFC 9126
- 作用:客户端先把授权请求参数推送到 IdentityServer,而不是直接通过浏览器 URL 传递。
- 好处:
- URL 更短、更安全,不暴露敏感信息(如
client_secret、code_challenge)。 - 可以传输更多参数而不受 URL 长度限制。
- 提升安全性,避免 URL 被记录在日志或浏览器历史中。
- URL 更短、更安全,不暴露敏感信息(如
参数
| 参数 | 说明 |
|---|---|
client_id |
客户端标识(这里是 webhooksclient) |
client_secret |
客户端密钥,用于客户端认证 |
redirect_uri |
授权码回调地址 |
response_type |
响应类型,这里是 code(标准授权码模式) |
scope |
授权范围(openid、profile、webhooks) |
code_challenge / code_challenge_method |
PKCE 验证参数 |
response_mode |
响应模式,这里用 form_post |
nonce |
防止重放攻击的随机值 |
state |
防止 CSRF 攻击 |
POST /connect/par
client_id=webhooksclient&
redirect_uri=http%3A%2F%2Flocalhost%3A5062%2Fsignin-oidc&
response_type=code&
scope=openid+profile+webhooks&
code_challenge=Qq0d_4mV0Fdtx5CJJ4pNlg2d-wKRWS0dLgIuP83DlyU&
code_challenge_method=S256&
response_mode=form_post&
nonce=638998427975851129.NjdjNzg1NzgtYWMwYy00MzY0LWFjOWUtZGI0ZGQwMDM2NWZlODVkY2U2YmMtOTM4MS00ODg1LTkyYWItMzcyMGNkNzFlMzVh&state=CfDJ8AiXIMZqjLRDs2az9HQ7eYPhfTaLGaI1B3xGAEul7c1b7J1duur3lgpABqPRl-rGyuGfpZ-_WG4qlWkzuw2VFuOIPqaRu_mlMoZE14ww2dVpB5WD8jQ2cBDWWBSSo1jJsjUwBMLTycqsDywMJCmoI0qjkO7OnK84ixAGBan20AEImdcbIlH2XZX29xgdsHFsHYZJEb80sxJT2PiBM3NWiWM8yfvazolRUefu8iTtf7jz5HYEJJ1dQLfldPJq3nuKJp3aze_49kzG2UmkrcYaZgKA67Dh-QwzEauJIVdKbsR0XFLFZParXTe9gnGwOb6WecD6wcTRF62YSYIm14BKQyPotwz3KAso0saSOXbAxoswEdc8BxxOydIlEToY7BDl3AYPa3pOaZbbC9bndDZTbyE&
client_secret=secret
客户端认证成功事件
| 字段 | 说明 |
|---|---|
ClientId |
请求的客户端 ID(webhooksclient) |
AuthenticationMethod |
使用的客户端认证方式(SharedSecret) |
EventType |
事件类型,这里是 Success,表示客户端认证成功 |
TimeStamp |
事件发生时间 |
LocalIpAddress / RemoteIpAddress |
服务器和客户端 IP |

响应
| 字段 | 值 | 说明 |
|---|---|---|
request_uri |
urn:ietf:params:oauth:request_uri: 934E308D4D9E332AD35549B39C1D7EB61D14D 6E39B5FF61736603D29E054788A |
唯一标识这次授权请求的 URI。客户端后续浏览器跳转只需带上这个 request_uri,不必把完整参数写在 URL 上。 |
expires_in |
600 |
这个请求 URI 的有效期(秒),这里是 600 秒 = 10 分钟。在有效期内,客户端可以使用该 URI 完成授权码流程。 |
日志
info: Identity.API.RequestLoggingMiddleware[0]********** POST /connect/par Start **********========== Incoming Request ==========POST /connect/par--- Body ---client_id=webhooksclient&redirect_uri=http%3A%2F%2Flocalhost%3A5062%2Fsignin-oidc&response_type=code&scope=openid+profile+webhooks&code_challenge=Qq0d_4mV0Fdtx5CJJ4pNlg2d-wKRWS0dLgIuP83DlyU&code_challenge_method=S256&response_mode=form_post&nonce=638998427975851129.NjdjNzg1NzgtYWMwYy00MzY0LWFjOWUtZGI0ZGQwMDM2NWZlODVkY2U2YmMtOTM4MS00ODg1LTkyYWItMzcyMGNkNzFlMzVh&state=CfDJ8AiXIMZqjLRDs2az9HQ7eYPhfTaLGaI1B3xGAEul7c1b7J1duur3lgpABqPRl-rGyuGfpZ-_WG4qlWkzuw2VFuOIPqaRu_mlMoZE14ww2dVpB5WD8jQ2cBDWWBSSo1jJsjUwBMLTycqsDywMJCmoI0qjkO7OnK84ixAGBan20AEImdcbIlH2XZX29xgdsHFsHYZJEb80sxJT2PiBM3NWiWM8yfvazolRUefu8iTtf7jz5HYEJJ1dQLfldPJq3nuKJp3aze_49kzG2UmkrcYaZgKA67Dh-QwzEauJIVdKbsR0XFLFZParXTe9gnGwOb6WecD6wcTRF62YSYIm14BKQyPotwz3KAso0saSOXbAxoswEdc8BxxOydIlEToY7BDl3AYPa3pOaZbbC9bndDZTbyE&client_secret=secret======================================info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.PushedAuthorizationEndpoint for /connect/par
info: Duende.IdentityServer.Events.DefaultEventService[0]{"ClientId": "webhooksclient","AuthenticationMethod": "SharedSecret","Category": "Authentication","Name": "Client Authentication Success","EventType": "Success","Id": 1010,"ActivityId": "0HNHDLT0V7DOI:00000003","TimeStamp": "2025-11-27T12:19:57.6426806","ProcessId": 6832,"LocalIpAddress": "::1:5243","RemoteIpAddress": "::1"}
warn: Duende.IdentityServer.License[0]A request was made to the pushed authorization endpoint, but you do not have a license. This feature requires the Business Edition or higher tier of license.
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 201--- Body ---{"request_uri":"urn:ietf:params:oauth:request_uri:934E308D4D9E332AD35549B39C1D7EB61D14D6E39B5FF61736603D29E054788A","expires_in":600}======================================********** POST /connect/par End **********

7.客户端请求认证服务授权端点/connect/authorize
/connect/authorize 是 OIDC 授权端点,客户端携带 client_id 和 request_uri(或完整参数)访问它时,IdentityServer 会验证客户端与用户状态,如果用户未登录就重定向到登录页,登录后再根据授权请求生成授权码并重定向回客户端的 redirect_uri。
客户端通过 /connect/authorize 请求授权码时,IdentityServer 检查到用户未登录,因此返回 302 重定向到 /Account/Login 登录页,登录成功后会使用 request_uri 恢复之前的授权请求,继续完成授权码流程。
参数
| 参数 | 说明 |
|---|---|
client_id=webhooksclient |
客户端 ID,标识哪个应用发起授权请求 |
request_uri=urn:ietf:params:oauth:request_uri:... |
PAR 返回的唯一标识符,用于还原完整授权请求参数(scope、redirect_uri、PKCE 等) |
x-client-SKU=ID_NET9_0 |
客户端 SDK 类型(.NET 9) |
x-client-ver=8.0.1.0 |
客户端 SDK 版本号 |
https://localhost:5243/connect/authorize?
client_id=webhooksclient&
request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A4DDC5E84FC16EA94D7661366602F058BD74E02C1FC6D0C8B637FDEEF0D16209B&
x-client-SKU=ID_NET9_0&
x-client-ver=8.0.1.0// 解码后:
https://localhost:5243/connect/authorize?
client_id=webhooksclient&
request_uri=urn:ietf:params:oauth:request_uri:4DDC5E84FC16EA94D7661366602F058BD74E02C1FC6D0C8B637FDEEF0D16209B&
x-client-SKU=ID_NET9_0&
x-client-ver=8.0.1.0

日志
info: Identity.API.RequestLoggingMiddleware[0]********** GET /connect/authorize Start **********========== Incoming Request ==========GET /connect/authorize?client_id=webhooksclient&request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3AED0DC98296957FDACD7D80198DF14973B3D55F5AA3320098202CD38AD6381427&x-client-SKU=ID_NET9_0&x-client-ver=8.0.1.0======================================info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.AuthorizeEndpoint for /connect/authorize
info: Duende.IdentityServer.ResponseHandling.AuthorizeInteractionResponseGenerator[0]Showing login: User is not authenticated
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 302********** GET /connect/authorize End **********

8.用户未登录重定向登录页(/Account/Login)
用户还未登录被重定向到登录页面 /Account/Login,登录成功后继续授权 (via ReturnUrl)
GET /Account/Login

参数
request_uri:Pushed Authorization Request 的引用标识符,客户端之前通过 /par 端点把完整授权请求参数(client_id、scope、redirect_uri、code_challenge、nonce 等)推送到 IdP,然后 IdP 返回 request_uri,浏览器端只带这个引用就能完成授权请求。
client_id:标识哪个客户端应用向 IdP 发起授权请求。授权服务器根据 client_id 校验该客户端的合法性、权限范围、redirect_uri 是否匹配等。
日志
info: Identity.API.RequestLoggingMiddleware[0]********** GET /Account/Login Start **********========== Incoming Request ==========GET /Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Frequest_uri%3Durn%253Aietf%253Aparams%253Aoauth%253Arequest_uri%253AAB9E9AB6C69C6FC7FD5796FD9EB64E5431E9686088161C3CF5B1BD9D31128512%26client_id%3Dwebhooksclient======================================info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 200********** GET /Account/Login End **********

9.登录(/Account/Login)
用户在登录页提交用户名、密码及 CSRF 防护令牌(__RequestVerificationToken),IdentityServer 验证凭证成功后记录登录事件,并通过 302 重定向将浏览器引导回授权端点继续完成授权码流程。
Query 参数
| 参数 | 作用 |
|---|---|
ReturnUrl |
告诉登录页面用户登录成功后要跳转到哪里(通常是授权回调 URL),保证登录完成后能继续原授权流程。 |
request_uri |
指向之前通过 PAR 推送到授权服务器的完整授权请求参数的引用标识符,浏览器只携带它,授权服务器根据它继续 OIDC 授权流程。request_uri 不包含明文参数,它只是一个短期唯一标识符,授权服务器通过它去查找存储在内部的完整参数集合,从而继续 OIDC 授权流程。 |
POST /Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Frequest_uri%3Durn%253Aietf%253Aparams%253Aoauth%253Arequest_uri%253A9A9F6E2E714443C29985C223CB65B6F8A30B3A7F6E8851CD1912400AE680E56A%26client_id%3Dwebhooksclient// 解码后
/Account/Login?
ReturnUrl=/connect/authorize/callback?request_uri=urn:ietf:params:oauth:request_uri:9A9F6E2E714443C29985C223CB65B6F8A30B3A7F6E8851CD1912400AE680E56A&
client_id=webhooksclient
Body 参数
| 字段 | 说明 |
|---|---|
ReturnUrl |
登录成功后重定向的目标 URL(这里是 /connect/authorize/callback,带 request_uri) |
Username |
用户输入的用户名(alice) |
Password |
用户输入的密码 |
button |
登录按钮(通常表单控件) |
__RequestVerificationToken |
防 CSRF(跨站请求伪造) 的 Anti-forgery Token(ASP.NET Core 自动生成) |
RememberLogin |
是否记住登录状态(cookie) |
ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Frequest_uri%3Durn%253Aietf%253Aparams%253Aoauth%253Arequest_uri%253A9A9F6E2E714443C29985C223CB65B6F8A30B3A7F6E8851CD1912400AE680E56A%26client_id%3Dwebhooksclient&
Username=alice&
Password=Pass123%24&
button=login&
__RequestVerificationToken=CfDJ8AiXIMZqjLRDs2az9HQ7eYMHO2ztVM9N2SXUMmejdxF0uodlZrzbIZTnGgDadIJ04Z8dKfZJqpKA5W0HSuHNcAQ3W3FdYKjc2l8m8OFEs8vqmF5OpPFQE33aSEnk2gV4FCvdz-D8rciHWVCjlom3sCg&
RememberLogin=false
登录成功事件
| 字段 | 含义 |
|---|---|
Username / DisplayName |
登录用户信息 |
SubjectId |
IdentityServer 为该用户分配的唯一标识(sub claim 对应) |
ClientId |
发起授权请求的客户端(webhooksclient) |
Endpoint |
登录事件来源(UI 表单) |
Category |
事件类别(Authentication 登录) |
Name |
事件名称(User Login Success) |
EventType |
Success / Failure 等 |
TimeStamp |
登录时间 |
LocalIpAddress / RemoteIpAddress |
客户端和服务器 IP(本地开发测试为 ::1) |

响应
重定向到了授权页
Status Code: 302
日志
info: Identity.API.RequestLoggingMiddleware[0]********** POST /Account/Login Start **********========== Incoming Request ==========POST /Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Frequest_uri%3Durn%253Aietf%253Aparams%253Aoauth%253Arequest_uri%253A9A9F6E2E714443C29985C223CB65B6F8A30B3A7F6E8851CD1912400AE680E56A%26client_id%3Dwebhooksclient--- Body ---ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Frequest_uri%3Durn%253Aietf%253Aparams%253Aoauth%253Arequest_uri%253A9A9F6E2E714443C29985C223CB65B6F8A30B3A7F6E8851CD1912400AE680E56A%26client_id%3Dwebhooksclient&Username=alice&Password=Pass123%24&button=login&__RequestVerificationToken=CfDJ8AiXIMZqjLRDs2az9HQ7eYMHO2ztVM9N2SXUMmejdxF0uodlZrzbIZTnGgDadIJ04Z8dKfZJqpKA5W0HSuHNcAQ3W3FdYKjc2l8m8OFEs8vqmF5OpPFQE33aSEnk2gV4FCvdz-D8rciHWVCjlom3sCg&RememberLogin=false======================================info: Microsoft.EntityFrameworkCore.Database.Command[20101]Executed DbCommand (1ms) [Parameters=[@__normalizedUserName_0='?'], CommandType='Text', CommandTimeout='30']SELECT a."Id", a."AccessFailedCount", a."CardHolderName", a."CardNumber", a."CardType", a."City", a."ConcurrencyStamp", a."Country", a."Email", a."EmailConfirmed", a."Expiration", a."LastName", a."LockoutEnabled", a."LockoutEnd", a."Name", a."NormalizedEmail", a."NormalizedUserName", a."PasswordHash", a."PhoneNumber", a."PhoneNumberConfirmed", a."SecurityNumber", a."SecurityStamp", a."State", a."Street", a."TwoFactorEnabled", a."UserName", a."ZipCode"FROM "AspNetUsers" AS aWHERE a."NormalizedUserName" = @__normalizedUserName_0LIMIT 1
info: Microsoft.EntityFrameworkCore.Database.Command[20101]Executed DbCommand (1ms) [Parameters=[@__user_Id_0='?'], CommandType='Text', CommandTimeout='30']SELECT a."Id", a."ClaimType", a."ClaimValue", a."UserId"FROM "AspNetUserClaims" AS aWHERE a."UserId" = @__user_Id_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]Executed DbCommand (1ms) [Parameters=[@__userId_0='?'], CommandType='Text', CommandTimeout='30']SELECT a0."Name"FROM "AspNetUserRoles" AS aINNER JOIN "AspNetRoles" AS a0 ON a."RoleId" = a0."Id"WHERE a."UserId" = @__userId_0
info: Microsoft.EntityFrameworkCore.Database.Command[20101]Executed DbCommand (1ms) [Parameters=[@__normalizedUserName_0='?'], CommandType='Text', CommandTimeout='30']SELECT a."Id", a."AccessFailedCount", a."CardHolderName", a."CardNumber", a."CardType", a."City", a."ConcurrencyStamp", a."Country", a."Email", a."EmailConfirmed", a."Expiration", a."LastName", a."LockoutEnabled", a."LockoutEnd", a."Name", a."NormalizedEmail", a."NormalizedUserName", a."PasswordHash", a."PhoneNumber", a."PhoneNumberConfirmed", a."SecurityNumber", a."SecurityStamp", a."State", a."Street", a."TwoFactorEnabled", a."UserName", a."ZipCode"FROM "AspNetUsers" AS aWHERE a."NormalizedUserName" = @__normalizedUserName_0LIMIT 1
info: Duende.IdentityServer.Events.DefaultEventService[0]{"Username": "alice","SubjectId": "13051f3a-c540-4f64-8048-b141d7a3e026","DisplayName": "alice","Endpoint": "UI","ClientId": "webhooksclient","Category": "Authentication","Name": "User Login Success","EventType": "Success","Id": 1000,"ActivityId": "0HNHDNUPTKN1L:0000001B","TimeStamp": "2025-11-27T14:17:52.6066697","ProcessId": 20408,"LocalIpAddress": "::1:5243","RemoteIpAddress": "::1"}
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 302********** POST /Account/Login End **********

10.授权回调请求-重定向授权页(/connect/authorize/callback)
/connect/authorize/callback 请求是浏览器在用户登录或同意授权后访问的回调端点,IdentityServer 根据 request_uri 查找之前的完整授权请求,判断用户是否已同意 scope,然后决定显示 Consent 页面或生成授权码/ID Token 并重定向回客户端。
登录成功后,浏览器访问 /connect/authorize/callback,IdentityServer 根据 request_uri 获取完整 PAR 授权请求、查询用户信息,发现用户尚未同意客户端请求的 scope,于是返回 302 重定向到 Consent 页面,让用户确认授权后再继续生成授权码。
Query 参数
request_uri:PAR 授权请求标识符client_id:请求授权的客户端 ID
GET /connect/authorize/callback?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A9A9F6E2E714443C29985C223CB65B6F8A30B3A7F6E8851CD1912400AE680E56A&client_id=webhooksclient
调用 IdentityServer 端点
IdentityServer 拦截 /connect/authorize/callback,调用 AuthorizeCallbackEndpoint 来处理授权回调
根据 request_uri 获取之前通过 PAR 推送的完整授权请求,准备生成授权码或跳转到 Consent
info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]
Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.AuthorizeCallbackEndpoint for /connect/authorize/callback
数据库查询用户信息
IdentityServer 根据用户 ID 查询数据库,获取用户信息,包括用户名、Email、锁定状态、两步验证、角色等
用于判断用户是否已登录并已同意授权,数据库操作是正常的用户信息加载
info: Microsoft.EntityFrameworkCore.Database.Command[20101]Executed DbCommand (1ms) [Parameters=[@__p_0='?'], CommandType='Text', CommandTimeout='30']SELECT a."Id", a."AccessFailedCount", a."CardHolderName", a."CardNumber", a."CardType", a."City", a."ConcurrencyStamp", a."Country", a."Email", a."EmailConfirmed", a."Expiration", a."LastName", a."LockoutEnabled", a."LockoutEnd", a."Name", a."NormalizedEmail", a."NormalizedUserName", a."PasswordHash", a."PhoneNumber", a."PhoneNumberConfirmed", a."SecurityNumber", a."SecurityStamp", a."State", a."Street", a."TwoFactorEnabled", a."UserName", a."ZipCode"FROM "AspNetUsers" AS aWHERE a."Id" = @__p_0LIMIT 1
检查用户是否同意授权
IdentityServer 检查用户是否已对客户端请求的 scope 授权过,用户未需要显示 Consent 页面
info: Duende.IdentityServer.ResponseHandling.AuthorizeInteractionResponseGenerator[0]
Showing consent: User has not yet consented
响应
IdentityServer 不是直接生成授权码,先让用户在 Consent 页面确认 scope,浏览器收到 302 自动跳转到 Consent 页面
Status Code: 302
日志
info: Identity.API.RequestLoggingMiddleware[0]********** GET /connect/authorize/callback Start **********========== Incoming Request ==========GET /connect/authorize/callback?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A9A9F6E2E714443C29985C223CB65B6F8A30B3A7F6E8851CD1912400AE680E56A&client_id=webhooksclient======================================info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.AuthorizeCallbackEndpoint for /connect/authorize/callback
info: Microsoft.EntityFrameworkCore.Database.Command[20101]Executed DbCommand (1ms) [Parameters=[@__p_0='?'], CommandType='Text', CommandTimeout='30']SELECT a."Id", a."AccessFailedCount", a."CardHolderName", a."CardNumber", a."CardType", a."City", a."ConcurrencyStamp", a."Country", a."Email", a."EmailConfirmed", a."Expiration", a."LastName", a."LockoutEnabled", a."LockoutEnd", a."Name", a."NormalizedEmail", a."NormalizedUserName", a."PasswordHash", a."PhoneNumber", a."PhoneNumberConfirmed", a."SecurityNumber", a."SecurityStamp", a."State", a."Street", a."TwoFactorEnabled", a."UserName", a."ZipCode"FROM "AspNetUsers" AS aWHERE a."Id" = @__p_0LIMIT 1
info: Duende.IdentityServer.ResponseHandling.AuthorizeInteractionResponseGenerator[0]Showing consent: User has not yet consented
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 302********** GET /connect/authorize/callback End **********

11.授权页(/consent)
/consent GET 请求用于显示用户授权确认页面,浏览器通过 returnUrl 带上原始授权请求标识(request_uri),用户在页面上同意或拒绝客户端请求的权限,后续继续 OIDC 授权流程。

参数
returnUrl:原始授权回调 URL /connect/authorize/callback(包含 request_uri)
client_id:请求授权的客户端 ID
GET /consent?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Frequest_uri%3Durn%253Aietf%253Aparams%253Aoauth%253Arequest_uri%253A9A9F6E2E714443C29985C223CB65B6F8A30B3A7F6E8851CD1912400AE680E56A%26
client_id%3Dwebhooksclient
日志
info: Identity.API.RequestLoggingMiddleware[0]********** GET /consent Start **********========== Incoming Request ==========GET /consent?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Frequest_uri%3Durn%253Aietf%253Aparams%253Aoauth%253Arequest_uri%253A9A9F6E2E714443C29985C223CB65B6F8A30B3A7F6E8851CD1912400AE680E56A%26client_id%3Dwebhooksclient======================================info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 200********** GET /consent End **********

12.授权(/consent)
/Consent POST 请求用于用户提交授权确认,IdentityServer 验证请求、记录用户同意的 scope(并可记住选择),然后重定向回原始授权回调 URL,继续 OIDC/PAR 授权码生成流程。
Body 参数
| 参数 | 作用 |
|---|---|
ReturnUrl |
Consent 页面完成后要重定向回的授权回调 URL(携带 request_uri) |
ScopesConsented |
用户同意的权限列表(openid, profile, webhooks) |
RememberConsent |
用户是否选择“记住同意”,下次无需再次显示 Consent |
button |
用户点击的按钮(yes / no) |
__RequestVerificationToken |
防 CSRF token,防止跨站请求伪造 |
POST /Consent ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Frequest_uri%3Durn%253Aietf%253Aparams%253Aoauth%253Arequest_uri%253A0F8304C65120C621A2089FF9A63A5E38A3537E0E8CB5686495432DBA1BF592CB%26client_id%3Dwebhooksclient&ScopesConsented=openid&ScopesConsented=profile&ScopesConsented=webhooks&Description=&RememberConsent=true&button=yes&__RequestVerificationToken=CfDJ8AiXIMZqjLRDs2az9HQ7eYNAFpqWllO7FkzY0sJh1tZ0Zwz4NDbj7X-xNQjiQAN7CGdSwXGp241s8k_gcYE8NLCzDramxnEm3EfCR7dRemKPSblJomckGO_xNglgCehDjNYB8ID9brsP2g31WDLZ9HNPA2h2rrnDuuNOFmIDdZZrA2-JNvzKSYCsXZbhGIb5kw&RememberConsent=false
IdentityServer 处理 Consent
用户已同意客户端 webhooksclient 请求的 openid、profile 和 webhooks 权限,并选择记住同意,以便下次无需再次确认。
info: Duende.IdentityServer.Events.DefaultEventService[0]{"SubjectId": "13051f3a-c540-4f64-8048-b141d7a3e026","ClientId": "webhooksclient","RequestedScopes": ["openid","profile","webhooks"],"GrantedScopes": ["openid","profile","webhooks"],"ConsentRemembered": true,"Category": "Grants","Name": "Consent granted","EventType": "Information","Id": 4000,"ActivityId": "0HNHDOMTV007E:0000001B","TimeStamp": "2025-11-27T15:00:49.9958745","ProcessId": 16064,"LocalIpAddress": "::1:5243","RemoteIpAddress": "::1"}
响应
302 重定向到 ReturnUrl。Consent 页面 POST 完成后,授权流程继续
Status Code: 302
日志
info: Identity.API.RequestLoggingMiddleware[0]********** POST /Consent Start **********========== Incoming Request ==========POST /Consent--- Body ---ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Frequest_uri%3Durn%253Aietf%253Aparams%253Aoauth%253Arequest_uri%253A0F8304C65120C621A2089FF9A63A5E38A3537E0E8CB5686495432DBA1BF592CB%26client_id%3Dwebhooksclient&ScopesConsented=openid&ScopesConsented=profile&ScopesConsented=webhooks&Description=&RememberConsent=true&button=yes&__RequestVerificationToken=CfDJ8AiXIMZqjLRDs2az9HQ7eYNAFpqWllO7FkzY0sJh1tZ0Zwz4NDbj7X-xNQjiQAN7CGdSwXGp241s8k_gcYE8NLCzDramxnEm3EfCR7dRemKPSblJomckGO_xNglgCehDjNYB8ID9brsP2g31WDLZ9HNPA2h2rrnDuuNOFmIDdZZrA2-JNvzKSYCsXZbhGIb5kw&RememberConsent=false======================================info: Duende.IdentityServer.Events.DefaultEventService[0]{"SubjectId": "13051f3a-c540-4f64-8048-b141d7a3e026","ClientId": "webhooksclient","RequestedScopes": ["openid","profile","webhooks"],"GrantedScopes": ["openid","profile","webhooks"],"ConsentRemembered": true,"Category": "Grants","Name": "Consent granted","EventType": "Information","Id": 4000,"ActivityId": "0HNHDOMTV007E:0000001B","TimeStamp": "2025-11-27T15:00:49.9958745","ProcessId": 16064,"LocalIpAddress": "::1:5243","RemoteIpAddress": "::1"}
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 302********** POST /Consent End **********

13.授权回调请求-生成授权码(/connect/authorize/callback)
用户在 Consent 页面同意权限后,浏览器访问 /connect/authorize/callback,IdentityServer 根据 request_uri 获取完整授权请求、生成授权码,并重定向回客户端回调 URL,完成 OIDC 授权码颁发流程。
Query 参数
request_uri→ PAR 请求标识符,引用之前推送的完整授权请求client_id→ 授权请求的客户端 ID
GET /connect/authorize/callback?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A0F8304C65120C621A2089FF9A63A5E38A3537E0E8CB5686495432DBA1BF592CB&client_id=webhooksclient
IdentityServer 处理授权回调
IdentityServer 调用 AuthorizeCallbackEndpoint
AuthorizeCallbackEndpoint 是 IdentityServer 中处理 授权码流程回调的关键端点。在用户完成登录和同意授权(Consent)后,浏览器会被重定向到该端点,IdentityServer 会根据前面保存的授权请求信息(如 PAR 的 request_uri 或完整请求参数)执行以下操作:首先验证客户端、用户身份及授权请求合法性,然后生成授权码(Authorization Code),并将其附加到客户端的 redirect_uri 中,最后通过 HTTP 重定向将浏览器返回给客户端应用。该端点还会触发相关事件记录,如登录成功、授权同意等,确保整个授权码流程安全、完整,并支持 PKCE、Scopes 和 Consent 的管理。
Invoking IdentityServer endpoint: AuthorizeCallbackEndpoint
数据库查询用户信息

用户已同意授权
dentityServer 识别到用户已经同意了 requested scopes
fo: Duende.IdentityServer.ResponseHandling.AuthorizeInteractionResponseGenerator[0]User consented to scopes: openid, profile, webhooks
事件记录
-
生成授权码 (
code) -
记录事件:Token 已成功颁发
-
指明客户端回调 URI、Scope、GrantType
Token Issued Success
ClientId: webhooksclient
RedirectUri: http://localhost:5062/signin-oidc
Scopes: openid profile webhooks
GrantType: authorization_code
Tokens: [{TokenType: code}]

响应
Status Code: 200
日志
fo: Identity.API.RequestLoggingMiddleware[0]********** GET /connect/authorize/callback Start **********========== Incoming Request ==========GET /connect/authorize/callback?request_uri=urn%3Aietf%3Aparams%3Aoauth%3Arequest_uri%3A0F8304C65120C621A2089FF9A63A5E38A3537E0E8CB5686495432DBA1BF592CB&client_id=webhooksclient======================================info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.AuthorizeCallbackEndpoint for /connect/authorize/callback
info: Microsoft.EntityFrameworkCore.Database.Command[20101]Executed DbCommand (1ms) [Parameters=[@__p_0='?'], CommandType='Text', CommandTimeout='30']SELECT a."Id", a."AccessFailedCount", a."CardHolderName", a."CardNumber", a."CardType", a."City", a."ConcurrencyStamp", a."Country", a."Email", a."EmailConfirmed", a."Expiration", a."LastName", a."LockoutEnabled", a."LockoutEnd", a."Name", a."NormalizedEmail", a."NormalizedUserName", a."PasswordHash", a."PhoneNumber", a."PhoneNumberConfirmed", a."SecurityNumber", a."SecurityStamp", a."State", a."Street", a."TwoFactorEnabled", a."UserName", a."ZipCode"FROM "AspNetUsers" AS aWHERE a."Id" = @__p_0LIMIT 1
info: Duende.IdentityServer.ResponseHandling.AuthorizeInteractionResponseGenerator[0]User consented to scopes: openid, profile, webhooks
info: Duende.IdentityServer.Events.DefaultEventService[0]{"ClientId": "webhooksclient","ClientName": "Webhooks Client","RedirectUri": "http://localhost:5062/signin-oidc","Endpoint": "Authorize","SubjectId": "13051f3a-c540-4f64-8048-b141d7a3e026","Scopes": "openid profile webhooks","GrantType": "authorization_code","Tokens": [{"TokenType": "code","TokenValue": "****9B-1"}],"Category": "Token","Name": "Token Issued Success","EventType": "Success","Id": 2000,"ActivityId": "0HNHDOMTV007E:0000001D","TimeStamp": "2025-11-27T15:00:50.0619166","ProcessId": 16064,"LocalIpAddress": "::1:5243","RemoteIpAddress": "::1"}
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 200********** GET /connect/authorize/callback End **********

14.授权回调请求-颁发Token(/connect/token)
客户端使用之前从 /connect/authorize/callback 获取的授权码(Authorization Code),连同 client_id、client_secret、redirect_uri 和 code_verifier(PKCE 验证)向 IdentityServer 的 /connect/token 发送 POST 请求;IdentityServer 先验证客户端身份,再验证授权码和 PKCE,确认合法后生成 Access Token 和 ID Token,并将它们返回给客户端,用于访问受保护的 API 和完成用户登录身份认证。
Body 参数
| 参数 | 作用 |
|---|---|
client_id |
客户端标识,表示哪个应用在请求 token |
client_secret |
客户端密钥,用于认证客户端身份(Confidential Client) |
code |
授权码(Authorization Code),之前 /connect/authorize/callback 生成 |
grant_type |
授权类型,这里是 authorization_code |
redirect_uri |
必须与授权请求一致,用于验证回调合法性 |
code_verifier |
PKCE 验证值,确保授权码只能被原始客户端使用 |
POST /connect/token
client_id=webhooksclient&
client_secret=secret&
code=F95F4B21958B6456294D215758170B0D96C770EEA073FD5438B4D7724F4ED69B-1&
grant_type=authorization_code&
redirect_uri=http%3A%2F%2Flocalhost%3A5062%2Fsignin-oidc&
code_verifier=v5TrkqKPser4OaGeHcVlvApu143lRljRbKsFC6yxw8c
客户端认证
IdentityServer 验证 client_id + client_secret 成功,客户端身份合法,可以继续处理授权码

数据库查询用户信息

验证授权码请求
IdentityServer 验证:
- 授权码是否有效且未过期
redirect_uri是否匹配code_verifier是否正确(PKCE 验证)
验证成功后,可以生成 tokens

颁发 Token
-
IdentityServer 生成:
-
ID Token → 包含用户身份信息 (JWT)
-
Access Token → 用于访问受保护资源(API)
-
-
Scope 对应用户同意的权限:
openid、profile、webhooks -
Token 会被返回给客户端

响应
| 字段 | 说明 |
|---|---|
id_token |
OpenID Connect ID Token,包含用户身份信息(如 sub、name、email 等),用于客户端验证用户身份 |
expires_in |
Token 有效期,单位秒,这里是 7200 秒(2 小时) |
token_type |
Token 类型,这里是 Bearer,客户端在请求资源时需在 HTTP Header 中使用 Authorization: Bearer <token> |
scope |
授权范围(Scope),客户端获得的权限,这里包括 openid、profile 和 webhooks |
Status Code: 200
{"id_token":"eyJhbGciOiJSUzI1NiIsw此处省略...","expires_in":7200,"token_type":"Bearer","scope":"openid profile webhooks"}
日志
info: Identity.API.RequestLoggingMiddleware[0]********** POST /connect/token Start **********========== Incoming Request ==========POST /connect/token--- Body ---client_id=webhooksclient&client_secret=secret&code=F95F4B21958B6456294D215758170B0D96C770EEA073FD5438B4D7724F4ED69B-1&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5062%2Fsignin-oidc&code_verifier=v5TrkqKPser4OaGeHcVlvApu143lRljRbKsFC6yxw8c======================================info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.TokenEndpoint for /connect/token
info: Duende.IdentityServer.Events.DefaultEventService[0]{"ClientId": "webhooksclient","AuthenticationMethod": "SharedSecret","Category": "Authentication","Name": "Client Authentication Success","EventType": "Success","Id": 1010,"ActivityId": "0HNHDOMTV007F:00000004","TimeStamp": "2025-11-27T15:00:50.1456808","ProcessId": 16064,"LocalIpAddress": "::1:5243","RemoteIpAddress": "::1"}
info: Microsoft.EntityFrameworkCore.Database.Command[20101]Executed DbCommand (1ms) [Parameters=[@__p_0='?'], CommandType='Text', CommandTimeout='30']SELECT a."Id", a."AccessFailedCount", a."CardHolderName", a."CardNumber", a."CardType", a."City", a."ConcurrencyStamp", a."Country", a."Email", a."EmailConfirmed", a."Expiration", a."LastName", a."LockoutEnabled", a."LockoutEnd", a."Name", a."NormalizedEmail", a."NormalizedUserName", a."PasswordHash", a."PhoneNumber", a."PhoneNumberConfirmed", a."SecurityNumber", a."SecurityStamp", a."State", a."Street", a."TwoFactorEnabled", a."UserName", a."ZipCode"FROM "AspNetUsers" AS aWHERE a."Id" = @__p_0LIMIT 1
info: Duende.IdentityServer.Validation.TokenRequestValidator[0]Token request validation success, {"ClientId": "webhooksclient","ClientName": "Webhooks Client","GrantType": "authorization_code","AuthorizationCode": "****9B-1","RefreshToken": "********","Raw": {"client_id": "webhooksclient","client_secret": "***REDACTED***","code": "***REDACTED***","grant_type": "authorization_code","redirect_uri": "http://localhost:5062/signin-oidc","code_verifier": "v5TrkqKPser4OaGeHcVlvApu143lRljRbKsFC6yxw8c"}}
info: Duende.IdentityServer.Events.DefaultEventService[0]{"ClientId": "webhooksclient","ClientName": "Webhooks Client","Endpoint": "Token","SubjectId": "13051f3a-c540-4f64-8048-b141d7a3e026","Scopes": "openid profile webhooks","GrantType": "authorization_code","Tokens": [{"TokenType": "id_token","TokenValue": "****A8QA"},{"TokenType": "access_token","TokenValue": "****kF9w"}],"Category": "Token","Name": "Token Issued Success","EventType": "Success","Id": 2000,"ActivityId": "0HNHDOMTV007F:00000004","TimeStamp": "2025-11-27T15:00:50.2195689","ProcessId": 16064,"LocalIpAddress": "::1:5243","RemoteIpAddress": "::1"}
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 200--- Body ---{"id_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IkY3MzgzQTBFRDdDRDk2MEVGNDczRkQyODUxODAzOTM4IiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo1MjQzIiwibmJmIjoxNzY0MjU1NjUwLCJpYXQiOjE3NjQyNTU2NTAsImV4cCI6MTc2NDI2Mjg1MCwiYXVkIjoid2ViaG9va3NjbGllbnQiLCJhbXIiOlsicHdkIl0sIm5vbmNlIjoiNjM4OTk4NTI0NDc1ODM5NjQxLk5HTmxNV0prTldNdE1EY3lOaTAwTnpFeExUazROak10TXpGbU0yUTRaRGhtTjJNeE1tSXpZelZsWVRndE5qYzBZUzAwWkdFMkxXSXdaVGN0WW1RNU9EYzVNekV6WkdWaSIsImF0X2hhc2giOiJrOHRjOXBGRDZvVmxDRzF3dE9MT1FnIiwic2lkIjoiQjlGOTMxRkJCODVFMEQ2NjM3M0Y2MTVFNTY2NkQ0RTUiLCJzdWIiOiIxMzA1MWYzYS1jNTQwLTRmNjQtODA0OC1iMTQxZDdhM2UwMjYiLCJhdXRoX3RpbWUiOjE3NjQyNTMwNzIsImlkcCI6ImxvY2FsIiwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWxpY2UiLCJ1bmlxdWVfbmFtZSI6ImFsaWNlIiwibmFtZSI6IkFsaWNlIiwibGFzdF9uYW1lIjoiU21pdGgiLCJjYXJkX251bWJlciI6IlhYWFhYWFhYWFhYWDE4ODEiLCJjYXJkX2hvbGRlciI6IkFsaWNlIFNtaXRoIiwiY2FyZF9zZWN1cml0eV9udW1iZXIiOiIxMjMiLCJjYXJkX2V4cGlyYXRpb24iOiIxMi8yNCIsImFkZHJlc3NfY2l0eSI6IlJlZG1vbmQiLCJhZGRyZXNzX2NvdW50cnkiOiJVLlMuIiwiYWRkcmVzc19zdGF0ZSI6IldBIiwiYWRkcmVzc19zdHJlZXQiOiIxNTcwMyBORSA2MXN0IEN0IiwiYWRkcmVzc196aXBfY29kZSI6Ijk4MDUyIiwiZW1haWwiOiJBbGljZVNtaXRoQGVtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJwaG9uZV9udW1iZXIiOiIxMjM0NTY3ODkwIiwicGhvbmVfbnVtYmVyX3ZlcmlmaWVkIjpmYWxzZX0.oxcegeZZfrHuQ35VBe4KFMMFK8MAHOGATG-Rc_ERdk9fdFf6o28knw-ciJJz-48kr1e1EUODwzH5MAE-Zv9KqzSnymN6jAalQ-zTOgRsoVtsDBW2P9mEb69LdkEa3F9kmTFu4-aEBUVk_LZ0H68Nml5JG9fb5YRHoW6oK5k6IY665i8fFOh2mPQuwtRQCAde4s789efWL01974g98PACn2hfCia53xOPV4RQHIGlX-c7yR2H3h9aCdei8Phh2V_FuAFvafQYIcVByrDjXIfpFcr8MDcCa-SBPo-UUyADTRLZsySwNgsqQplkpITXun03MlzcMUzW1l5hA_reBYA8QA","access_token":"eyJhbGciOiJSUzI1NiIsImtpZCI6IkY3MzgzQTBFRDdDRDk2MEVGNDczRkQyODUxODAzOTM4IiwidHlwIjoiYXQrand0In0.eyJpc3MiOiJodHRwczovL2xvY2FsaG9zdDo1MjQzIiwibmJmIjoxNzY0MjU1NjUwLCJpYXQiOjE3NjQyNTU2NTAsImV4cCI6MTc2NDI2Mjg1MCwiYXVkIjoid2ViaG9va3MiLCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIiwid2ViaG9va3MiXSwiYW1yIjpbInB3ZCJdLCJjbGllbnRfaWQiOiJ3ZWJob29rc2NsaWVudCIsInN1YiI6IjEzMDUxZjNhLWM1NDAtNGY2NC04MDQ4LWIxNDFkN2EzZTAyNiIsImF1dGhfdGltZSI6MTc2NDI1MzA3MiwiaWRwIjoibG9jYWwiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhbGljZSIsInVuaXF1ZV9uYW1lIjoiYWxpY2UiLCJuYW1lIjoiQWxpY2UiLCJsYXN0X25hbWUiOiJTbWl0aCIsImNhcmRfbnVtYmVyIjoiWFhYWFhYWFhYWFhYMTg4MSIsImNhcmRfaG9sZGVyIjoiQWxpY2UgU21pdGgiLCJjYXJkX3NlY3VyaXR5X251bWJlciI6IjEyMyIsImNhcmRfZXhwaXJhdGlvbiI6IjEyLzI0IiwiYWRkcmVzc19jaXR5IjoiUmVkbW9uZCIsImFkZHJlc3NfY291bnRyeSI6IlUuUy4iLCJhZGRyZXNzX3N0YXRlIjoiV0EiLCJhZGRyZXNzX3N0cmVldCI6IjE1NzAzIE5FIDYxc3QgQ3QiLCJhZGRyZXNzX3ppcF9jb2RlIjoiOTgwNTIiLCJlbWFpbCI6IkFsaWNlU21pdGhAZW1haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInBob25lX251bWJlciI6IjEyMzQ1Njc4OTAiLCJwaG9uZV9udW1iZXJfdmVyaWZpZWQiOmZhbHNlLCJzaWQiOiJCOUY5MzFGQkI4NUUwRDY2MzczRjYxNUU1NjY2RDRFNSIsImp0aSI6IjNBMTVENTI4NTU1NUJDMzJDNDZDNEMwNjBENDcyNUE3In0.NaLKrVPbP_ojZoDTZUzWenehjTDUf8Jc7ztQn-GQmmRXngx7bTexQnF086_eZtJwm2A5k_Y_dcjcYmIHZhb4L3eHVLokI2CX5r6SuV4fRJBaZ9_E8XjN-0EYBvy6T8jUC8sP_Wk78auQETvgxf5VNsw5xo6ay2W-m9MfPZHRvKZDNDqVUF_uJdKZOf5hj0Qu6r9wRbbOGMQnNtKL7Kyo3KrYtb1TCuXnGSItKiXE1jQAhVVWsAT31mIwuV1RJclbXBlEbye6GO27wWB-sivVHwuSt0s3KDb6JzKHTFNw40GDrqtj4G6thyFYtWIe2kmbfFnF8K4I9CIf4OsMgNkF9w","expires_in":7200,"token_type":"Bearer","scope":"openid profile webhooks"}======================================********** POST /connect/token End **********

15.根据Access Token返回用户完整信息(/connect/userinfo)
客户端发起 GET 请求访问 UserInfoEndpoint,IdentityServer 接收请求并通过 Profile Service 查询数据库中对应用户的详细信息(如用户名、姓名、邮箱、手机号、地址及部分信用卡信息),生成包含所有 claims 的 JSON 响应返回给客户端
IdentityServer 处理
IdentityServer 接收请求后,将其路由到 UserInfoEndpoint。UserInfoEndpoint 是 OIDC 提供的接口,客户端通过 Access Token 调用它可以获取已认证用户的详细信息(Claims),用于显示用户资料或进行授权决策。
内部会验证 Access Token 是否有效,以及该 Token 是否有权访问用户信息。
info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.UserInfoEndpoint for /connect/userinfo
数据库查询用户信息
IdentityServer 根据 Access Token 中的 sub(用户 ID)查询数据库。此处使用 PostgreSQL 的 AspNetUsers 表获取用户完整信息。

IdentityServer 将数据库中的用户信息转换为Claims
Claims 列表包括:
- 标准 OpenID Connect Claims:
sub、preferred_username、name、email等 - 自定义 Claims:如银行卡信息(
card_number、card_holder等)、地址信息(address_city、address_state等)

响应
返回的都是数据库AspNetUsers 表存储的用户信息
Status Code: 200
{"sub":"13051f3a-c540-4f64-8048-b141d7a3e026","preferred_username":"alice","unique_name":"alice","name":"Alice","last_name":"Smith","card_number":"XXXXXXXXXXXX1881","card_holder":"Alice Smith","card_security_number":"123","card_expiration":"12/24","address_city":"Redmond","address_country":"U.S.","address_state":"WA","address_street":"15703 NE 61st Ct","address_zip_code":"98052","email":"AliceSmith@email.com","email_verified":true,"phone_number":"1234567890","phone_number_verified":false}
日志
info: Identity.API.RequestLoggingMiddleware[0]********** GET /connect/userinfo Start **********========== Incoming Request ==========GET /connect/userinfo======================================info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.UserInfoEndpoint for /connect/userinfo
info: Microsoft.EntityFrameworkCore.Database.Command[20101]Executed DbCommand (1ms) [Parameters=[@__p_0='?'], CommandType='Text', CommandTimeout='30']SELECT a."Id", a."AccessFailedCount", a."CardHolderName", a."CardNumber", a."CardType", a."City", a."ConcurrencyStamp", a."Country", a."Email", a."EmailConfirmed", a."Expiration", a."LastName", a."LockoutEnabled", a."LockoutEnd", a."Name", a."NormalizedEmail", a."NormalizedUserName", a."PasswordHash", a."PhoneNumber", a."PhoneNumberConfirmed", a."SecurityNumber", a."SecurityStamp", a."State", a."Street", a."TwoFactorEnabled", a."UserName", a."ZipCode"FROM "AspNetUsers" AS aWHERE a."Id" = @__p_0LIMIT 1
info: Duende.IdentityServer.ResponseHandling.UserInfoResponseGenerator[0]Profile service returned the following claim types: sub preferred_username unique_name name last_name card_number card_holder card_security_number card_expiration address_city address_country address_state address_street address_zip_code email email_verified phone_number phone_number_verified
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 200--- Body ---{"sub":"13051f3a-c540-4f64-8048-b141d7a3e026","preferred_username":"alice","unique_name":"alice","name":"Alice","last_name":"Smith","card_number":"XXXXXXXXXXXX1881","card_holder":"Alice Smith","card_security_number":"123","card_expiration":"12/24","address_city":"Redmond","address_country":"U.S.","address_state":"WA","address_street":"15703 NE 61st Ct","address_zip_code":"98052","email":"AliceSmith@email.com","email_verified":true,"phone_number":"1234567890","phone_number_verified":false}======================================********** GET /connect/userinfo End **********

16.客户端再次请求认证服务OIDC发现身份服务端点(/.well-known/openid-configuration)
通过再次请求,客户端可以获取 IdentityServer 暴露的所有关键端点(如授权端点 /connect/authorize、Token 端点 /connect/token、用户信息端点 /connect/userinfo、登出端点 /connect/endsession 等)、支持的 OAuth2/OIDC 功能(如 grant 类型、response type、scope、claim、签名算法、PKCE 方法、Prompt 值等),以及公钥 URL(jwks_uri)用于验证签名。这保证了客户端无需硬编码端点和能力信息,可以动态适配服务器能力,并为后续授权码交换、访问 Token 获取以及用户信息请求提供必要的基础信息,是整个 OIDC 授权流程初始化和配置发现的重要环节。
info: Identity.API.RequestLoggingMiddleware[0]********** GET /.well-known/openid-configuration Start **********========== Incoming Request ==========GET /.well-known/openid-configuration======================================info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.DiscoveryEndpoint for /.well-known/openid-configuration
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 200--- Body ---{"issuer":"https://localhost:5243","jwks_uri":"https://localhost:5243/.well-known/openid-configuration/jwks","authorization_endpoint":"https://localhost:5243/connect/authorize","token_endpoint":"https://localhost:5243/connect/token","userinfo_endpoint":"https://localhost:5243/connect/userinfo","end_session_endpoint":"https://localhost:5243/connect/endsession","check_session_iframe":"https://localhost:5243/connect/checksession","revocation_endpoint":"https://localhost:5243/connect/revocation","introspection_endpoint":"https://localhost:5243/connect/introspect","device_authorization_endpoint":"https://localhost:5243/connect/deviceauthorization","backchannel_authentication_endpoint":"https://localhost:5243/connect/ciba","pushed_authorization_request_endpoint":"https://localhost:5243/connect/par","require_pushed_authorization_requests":false,"frontchannel_logout_supported":true,"frontchannel_logout_session_supported":true,"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"scopes_supported":["openid","profile","orders","basket","webhooks","offline_access"],"claims_supported":["sub","name","family_name","given_name","middle_name","nickname","preferred_username","profile","picture","website","gender","birthdate","zoneinfo","locale","updated_at"],"grant_types_supported":["authorization_code","client_credentials","refresh_token","implicit","password","urn:ietf:params:oauth:grant-type:device_code","urn:openid:params:grant-type:ciba"],"response_types_supported":["code","token","id_token","id_token token","code id_token","code token","code id_token token"],"response_modes_supported":["form_post","query","fragment"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post"],"id_token_signing_alg_values_supported":["RS256"],"subject_types_supported":["public"],"code_challenge_methods_supported":["plain","S256"],"request_parameter_supported":true,"request_object_signing_alg_values_supported":["RS256","RS384","RS512","PS256","PS384","PS512","ES256","ES384","ES512","HS256","HS384","HS512"],"prompt_values_supported":["none","login","consent","select_account"],"authorization_response_iss_parameter_supported":true,"backchannel_token_delivery_modes_supported":["poll"],"backchannel_user_code_parameter_supported":true,"dpop_signing_alg_values_supported":["RS256","RS384","RS512","PS256","PS384","PS512","ES256","ES384","ES512"]}======================================********** GET /.well-known/openid-configuration End **********

17.客户端再次请求认证服务公钥(/.well-known/openid-configuration/jwks)
客户端或中间件再次请求 /.well-known/openid-configuration/jwks 公钥,是为了确保 使用最新公钥验证 ID token 或 access token 的签名安全,尤其是在可能存在密钥轮换、缓存过期或应用初始化等场景下,这样可以保证 token 的完整性和可信性。
info: Identity.API.RequestLoggingMiddleware[0]********** GET /.well-known/openid-configuration/jwks Start **********========== Incoming Request ==========GET /.well-known/openid-configuration/jwks======================================info: Duende.IdentityServer.Hosting.IdentityServerMiddleware[0]Invoking IdentityServer endpoint: Duende.IdentityServer.Endpoints.DiscoveryKeyEndpoint for /.well-known/openid-configuration/jwks
info: Identity.API.RequestLoggingMiddleware[0]========== Outgoing Response ==========Status Code: 200--- Body ---{"keys":[{"kty":"RSA","use":"sig","kid":"F7383A0ED7CD960EF473FD2851803938","e":"AQAB","n":"p3jTfCB0YsKpqJ6CNJi0tVmFBmoyI_D7QLLsbB-TCZ3-HIXDEr_k6zKb2GJ_QP7mncdSnYpJWSv7fWPfM0bL3A6NaMLF9MDjbfD5ti9irEW1dzBvIK0YjWmfks3eq6Mb2mM6PZtNEnoCqEzjgcRkDR1vtClEzUjs1E_i7TB-Y0J_aTYpLf-eN7yA1Obu8zMVRSSVBIwG5W5jljzA2nxk2u9qeDq8Sn0qgGwbX8cyYGQoVWBOPx7zap4cNcL6dHILjnlVrHqAUW9NVXtBWlVDP1Gvnm2zhCVJT_gW1twNhswyFULGVQH1ZWI0NukEqHG6LpN8Ti7Hx-K8MEEv_vQBYw","alg":"RS256"}]}======================================********** GET /.well-known/openid-configuration/jwks End **********

18.总结
OpenID Connect (OIDC):基于 OAuth 2.0 的认证层,用于验证用户身份,同时返回用户信息(ID Token)。
角色:
- 客户端 (Client / Relying Party):请求认证的应用,比如
webhooksclient。 - 资源拥有者 (Resource Owner / User):最终用户,比如日志中的
alice。 - 身份提供者 (Identity Provider / IdP):处理认证与授权的服务,比如 Duende IdentityServer。
- 受保护资源 (Resource Server):提供实际数据的 API,可以用 Access Token 访问。
18.1客户端发起授权请求 (Authorization Request)
客户端向 IdentityServer 的 /connect/authorize 或 /connect/par 发送请求:
POST /connect/par
client_id=webhooksclient
redirect_uri=http://localhost:5062/signin-oidc
response_type=code
scope=openid profile webhooks
code_challenge=CU6Wz...
code_challenge_method=S256
18.2用户重定向到登录页面 (User Login)
IdentityServer 检查用户是否已认证:
- 未认证 → 重定向到
/Account/Login。 - 已认证 → 直接跳到同意页面或颁发授权码。
GET /connect/authorize
Showing login: User is not authenticatedPOST /Account/Login
Username=alice
Password=Pass123$
IdentityServer 收到请求后,会生成一个 Request URI 并返回给客户端。
18.3用户同意授权 (Consent)
如果客户端请求的 Scope 包含敏感权限,IdentityServer 会要求用户同意。
用户可以选择记住同意(RememberConsent)。
用户登录
IdentityServer 校验用户凭证。
登录成功,生成用户会话。
GET /consent
POST /Consent
ScopesConsented=openid profile webhooks
ConsentRemembered=true
18.4颁发授权码 (Authorization Code)
用户登录并同意授权后,IdentityServer 生成授权码 (Authorization Code)。
授权码通过 redirect_uri 返回给客户端。
IdentityServer 记录用户同意信息,方便下次免确认。
GET /connect/authorize/callback
User consented to scopes: openid, profile, webhooks
Token Issued Success (code)
18.5颁发授权码 (Authorization Code)
用户登录并同意授权后,IdentityServer 生成授权码 (Authorization Code)。
授权码通过 redirect_uri 返回给客户端。
客户端收到授权码,准备兑换令牌。
GET /connect/authorize/callback
User consented to scopes: openid, profile, webhooks
Token Issued Success (code)
18.6客户端使用授权码请求令牌 (Token Request)
客户端向 /connect/token 发送 POST 请求
code_verifier与 code_challenge 配对,用于 PKCE 验证。
IdentityServer 校验授权码有效性、客户端身份、PKCE。
POST /connect/token
client_id=webhooksclient
client_secret=secret
code=授权码
grant_type=authorization_code
redirect_uri=http://localhost:5062/signin-oidc
code_verifier=N5yjin3JPv...
18.7颁发令牌 (Token Response)
验证成功后,IdentityServer 返回:
-
ID Token:JWT,包含用户身份信息(
sub、name、email等)。 -
Access Token:用于访问受保护的 API(如
webhooks)。 -
Refresh Token(可选):刷新 Access Token。
{"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkY3Mzg...","access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IkY3Mzg...","expires_in": 3600,"token_type": "Bearer"
}Token Issued Success
id_token=****
access_token=****
18.8客户端使用令牌访问资源 (Resource Access)
客户端携带 Access Token 调用 API。
API 验证 Access Token,返回资源。
18.9流程
| 步骤 | 日志关键点 |
|---|---|
| 发起授权 | POST /connect/par → Request URI |
| 用户登录 | GET /Account/Login, POST /Account/Login |
| 用户同意 | GET /consent, POST /Consent |
| 授权码回调 | GET /connect/authorize/callback |
| 令牌请求 | POST /connect/token |
| 返回令牌 | id_token + access_token |
写了好几个小时,短时间想全部消化有点难,慢慢再看吧,一遍学不会就多学几遍
📌 创作不易,感谢支持!
每一篇内容都凝聚了心血与热情,如果我的内容对您有帮助,欢迎请我喝杯咖啡☕,您的支持是我持续分享的最大动力!
💬 加入交流群(QQ群):576434538
