流程总览
一张图看完
浏览器(SPA 5173 + React 登录页 /Account/Login)
│
│ ① 顶层导航 GET /connect/authorize?client_id=client-demo&redirect_uri=...&code_challenge=...
▼
AuthorizationController.Authorize ── 未认证? ──▶ TempData["IgnoreAuthenticationChallenge"]=true
│ Challenge(RedirectUri=原请求)
│ → 302 /Account/Login?ReturnUrl=<原请求> ② 登录页
│ │ 用户提交用户名/密码
│ │ 表单 POST /Account/Login
│ │ → SignInManager 种 cookie
│ ▼ 顶层导航回 ReturnUrl
├─ 已认证 ──▶ 查永久授权(oi_ath)
│ │ 无 + ConsentType=Explicit ▶ 302 /Account/Consent?... ③ 同意页
│ │ │ 点"同意"(POST submit.Accept)
│ │ ▼
│ │ AuthorizationController.Accept
│ │ → 建永久授权 → SignIn → 签 code
│ └─ 有 ▶ 直接 SignIn → 签 code
▼
302 redirect_uri?code=...&state=...
│
│ ④ 客户端 POST /connect/token(code + PKCE verifier)
▼
AuthorizationController.Exchange → 重建 claims → 签发 access/id(/refresh) token
│
│ (带 token 调 API:客户端 GET /weatherforecast + Authorization: Bearer → OpenIddict Validation 验签 → 200)
│
│ ⑤ 登出 GET /connect/logout → React 登出确认页 → POST /connect/logout → 清 cookie → 302 回客户端
│
│ ⑥(可选)外部登录:登录页点"使用 GitHub 登录" → GitHub → /callback/login/github → 落地或确认页 → 回到 ①六个阶段的入口
| 阶段 | 页面 | 核心代码 | 关键状态 |
|---|---|---|---|
| ① 授权请求与未认证 Challenge | ① 授权请求 | AuthorizationController.Authorize + ConfigureApplicationCookie | Identity 应用 cookie 是否存在 |
| ② 登录 | ② 登录 | Login.tsx + Controllers/AccountController.cs | Identity 应用 cookie 被种下 |
| ③ 同意 | ③ 同意 | AuthorizationController.Authorize 的 switch + Accept / Deny | oi_ath 表里的永久授权 |
| ④ 换 token | ④ 换 token | AuthorizationController.Exchange | oi_tok 表的授权码 |
| ⑤ 登出 | ⑤ 登出 | AuthorizationController.Logout / LogoutPost | cookie 被清、token 失效 |
| ⑥ 外部登录 | ⑥ GitHub 外部登录 | AccountController(入口 / 落地 / 确认页端点)+ AuthenticationController + AddGitHub | 外部提供方凭据 + AspNetUserLogins 关联(未绑定须用户显式确认后建号) |
谁写了哪些 cookie / 表
| 载体 | 内容 | 谁写 | 什么时候失效 |
|---|---|---|---|
.AspNetCore.Identity.Application(默认名) | Identity 应用 cookie(用户会话,HttpOnly) | SignInManager(登录端点 / 外部登录落地) | 票据 14 天滑动过期(默认值);登录时勾选"记住我" → 持久 cookie(带 expires),否则会话 cookie;登出时 SignOutAsync |
.AspNetCore.Antiforgery.* | 防伪 cookie | GET /Account/Antiforgery | 会话级 |
oi_app | 客户端注册 | 启动时种子 | 每次启动清空重建 |
oi_ath | 永久授权(= 用户已同意) | AuthorizationController.Accept / 免同意分支 | 每次启动清空 → 重启后重新弹同意页 |
oi_tok | 授权码 / refresh token | OpenIddict 签发/撤销 | 每次启动清空 |
oi_sco | scope 资源映射 | 启动时按需 | 每次启动清空 |
AspNetUsers / AspNetRoles / AspNetUserLogins | Identity 用户、角色、外部登录关联 | 启动种子 / 外部登录落地端点 | 不清(除非旧库被整体重建) |
请求参数速查
客户端在 ① 发来的参数,后面每一步都在用:
| 参数 | 含义 | 后续哪里用到 |
|---|---|---|
client_id | 谁在请求授权 | FindByClientIdAsync 查 oi_app |
redirect_uri | 授权结果回到哪 | 与注册值比对;签 code 后 302 到它 |
response_type=code | 要授权码 | OpenIddict 校验 + 客户端权限白名单 |
scope | 要哪些权限 | 查永久授权按 scope 匹配;GetDestinations 决定 claim 去向 |
state | 客户端自己的防 CSRF 随机串 | OP 原样回传;由客户端校验 |
code_challenge / code_challenge_method | PKCE | 换 token 时校验 code_verifier |
nonce | id_token 防重放 | 由客户端校验 |
prompt | login / consent / none | ②的重新登录判定、③的强制同意判定 |
max_age | 会话最长年龄 | ① 的"cookie 是否够新鲜"判定 |
三个安全机制容易混
state 由客户端校验;防伪 token 与 PKCE 由 OP 校验——三者防的不是同一件事, 对照见安全机制对照(state / 防伪 / PKCE)。