这是什么项目
这是一个从零理解 OAuth 2.0 / OpenID Connect 登录流程的学习项目:一个自己的授权服务器 (OpenIddict + ASP.NET Core Identity)+ 一套 React 写的登录 / 同意 / 登出页面 + 一个演示用的 SPA 客户端, 然后把整条登录链路逐步拆开讲。
三个进程,三个角色
| 角色 | 目录 | 地址 | 干什么 |
|---|---|---|---|
| OP(授权服务器 + 资源服务器) | backend/Yagang.Identity/Yagang.Identity.Api | https://localhost:7016 | 认证用户、弹同意页、签发 code/token、校验 access token 并提供 /weatherforecast |
| SPA 客户端(要登录的那个应用) | demos/yagang-client-demo | http://localhost:5173 | 用 oidc-client-ts 发起授权码 + PKCE 流程,拿 token 调 API |
| React 登录前端 | frontend/yagang-identity-app | 构建产物同源托管在 OP 的 wwwroot(页面 URL:/Account/Login、/Account/Consent、/Account/Logout、/Account/ExternalLoginConfirm) | 登录页 / 同意页 / 登出确认页 / 外部账号确认页 |
为什么登录页要"同源托管"
登录页如果和 OP 不同源(比如独立部署在另一个端口/域名),就要处理 CORS、SameSite cookie、第三方 cookie 被浏览器拦截等一堆问题。 把构建产物输出到 OP 的 wwwroot 后,登录页与 OP 同源: 表单 POST 直接带 cookie、fetch('/Account/ExternalProviders') 这类同源 XHR 也不需要任何 CORS 配置。
一次登录会发生什么(极简版)
1. 客户端 重定向 → GET /connect/authorize?client_id=client-demo&...&code_challenge=...
2. OP 未认证 → 302 /Account/Login?ReturnUrl=<上面那个地址>
3. 用户在 React 登录页输入 alice / P@ssw0rd-001! → POST /Account/Login → OP 种 Identity cookie
4. 浏览器回到 /connect/authorize(这次已认证)
5. OP 发现「该用户 + 该客户端 + 这些 scope」还没有永久授权 → 302 /Account/Consent?...
6. 用户点「同意」→ POST /connect/authorize(submit.Accept) → OP 写永久授权 → 302 回客户端 redirect_uri?code=...
7. 客户端用 code + PKCE verifier → POST /connect/token → 拿到 access_token / id_token(含 role=admin)
8. 客户端带 access_token 调 GET /weatherforecast → 200
9. 登出:GET /connect/logout → React 登出确认页 → POST /connect/logout → 清 cookie → 302 回客户端每一段都在流程总览里展开成一页。
学习路径
- 项目架构:先搞清楚"哪一层负责什么",以及 cookie / code / access token 三层认证怎么区分。
- 本地跑起来:把 OP + 客户端跑起来,边读边点。
- 流程总览:完整时序 + 每步的请求/响应形态。
- ①→⑥ 各阶段:逐段看代码与判定条件。
- 代码走读:回到
Program.cs、四个控制器、React 四页,看实现细节。 - 表单登录与 loading(业界做法):为什么大厂登录页都是真
<form>,以及用 form 怎么做出"转圈"效果。 - 坑速查:真实踩过的坑,含报错信息与根因。
关键结论先放这里
三句话记住这个项目
- 服务端 = OpenIddict 授权服务器 + ASP.NET Core Identity 会话:协议侧由
AuthorizationController承担,外部提供方回调在AuthenticationController,React 页面需要的东西(登录提交、防伪 token、外部登录入口与落地)全部集中在Controllers/AccountController.cs。 - 登录 = Identity 应用 cookie(
SignInManager种的.AspNetCore.Identity.Application,默认名),授权 = OpenIddict 签发的 code / token,两者是独立的两个层次。 - 同意状态存在
oi_ath表(永久授权)。示例库每次启动会清空 OpenIddict 四张表 → 重启后第一次授权一定会再弹同意页。