项目架构
三个角色
| 角色 | 目录 | 地址 | 职责 |
|---|---|---|---|
| OP(授权服务器 + 资源服务器) | backend/Yagang.Identity/Yagang.Identity.Api | https://localhost:7016 | OpenIddict 提供 authorize / token / logout / revocation / userinfo;ASP.NET Core Identity 管理用户会话;/weatherforecast 是受保护的示例 API |
| React 登录前端 | frontend/yagang-identity-app | 构建产物输出到 OP 的 wwwroot(同源) | 登录 / 同意 / 登出 / 外部账号确认四个页面,全部用原生表单 POST 提交 |
| 演示客户端 | demos/yagang-client-demo | http://localhost:5173 | 用 oidc-client-ts 发起授权码 + PKCE 流程,拿 token 调 API |
代码怎么分层
判断"某段代码该写在哪里"时,按职责对号入座:
| 层 | 位置 | 职责 |
|---|---|---|
| 协议端点 | Controllers/AuthorizationController.cs | /connect/authorize、/connect/token、/connect/logout 的透传处理:Challenge、同意判定、签发 code、换 token、撤销由框架内置 |
| 外部提供方 | Controllers/AuthenticationController.cs | ~/callback/login/{provider}:校验 OpenIddict Client 的回调,把外部身份写进 Identity external cookie |
| 页面后端 | Controllers/AccountController.cs | React 页面要的非协议端点:POST /Account/Login、GET /Account/Antiforgery、外部登录入口 /Account/ExternalProviders + POST /Account/ExternalLogin + /Account/ExternalLoginCallback,以及确认页用的 /Account/ExternalLoginPending + POST /Account/ExternalLoginRegister |
| 资源 API | Controllers/WeatherForecastController.cs | 受 [Authorize](OpenIddict Validation)保护的示例数据 |
| 页面 | frontend/yagang-identity-app/src/pages/*.tsx | 登录 / 同意 / 登出 / 外部账号确认——UI 与交互全在这里,服务端不渲染任何视图 |
| 配置与数据 | OAuthOptions.cs、appsettings.json、Models/、Data/ApplicationDbContext.cs | 客户端注册、页面地址、演示账号全部配置驱动;Identity 用户/角色表 + OpenIddict 四张表 |
分层:三层认证别混淆
初学者最容易晕的是"这个 cookie 和那个 token 到底谁管谁"。这个项目里同时存在三层:
┌──────────────────────────────────────────────────────────────────┐
│ ① Identity 应用 cookie(用户会话) │
│ .AspNetCore.Identity.Application,HttpOnly,14 天滑动(默认) │
│ 由 SignInManager 种下;AuthorizationController 用它判断"用户是谁"│
│ 登录页 / 登录端点只为它服务 │
├──────────────────────────────────────────────────────────────────┤
│ ② OpenIddict 授权 code / token(对客户端的凭据) │
│ 授权端点签发的短命 code;客户端用它换 access/refresh token │
│ 状态存在 oi_tok / oi_ath / oi_app / oi_sco 四张表 │
├──────────────────────────────────────────────────────────────────┤
│ ③ access token(调 API 的通行证,JWS 未加密) │
│ 由 OpenIddict Validation 校验(本项目走 discovery + jwks 验签) │
│ WeatherForecastController 上的 [Authorize] 认的就是它 │
└──────────────────────────────────────────────────────────────────┘记忆口诀
cookie 管"人",code 管"这一趟授权",access token 管"调 API"。 同意页只影响 ②(是否写永久授权),跟 ① 的登录状态没关系;登出则要同时清掉 ①(SignInManager.SignOutAsync)并让 ②/③ 失效。
两个 OpenIddict 角色共存
Program.cs 里同时调用了 AddClient 和 AddServer,这不是笔误:
AddServer:本进程是"授权服务器"(发 code/token、管端点、管 scope)。AddClient:本进程同时还是"某个外部提供方(GitHub)的客户端"——用户点「使用 GitHub 登录」时, 本进程要去 GitHub 换 code,回调落在~/callback/login/{provider}(AuthenticationController)。
浏览器 ──点击「使用 GitHub 登录」──▶ 本进程 Challenge 转发 scheme ──302──▶ github.com/oauth/authorize
│
浏览器 ◀──写 Identity cookie(外部身份)── ~/callback/login/github ◀──code────┘
│
└─ /Account/ExternalLoginCallback:把外部身份换成真正的 Identity 用户 → 回到原授权请求端点一览
| 端点 | 类型 | 谁处理 |
|---|---|---|
GET/POST /connect/authorize | OpenIddict Server(透传到 MVC) | AuthorizationController.Authorize / .Accept / .Deny |
POST /connect/token | OpenIddict Server(透传) | AuthorizationController.Exchange |
GET/POST /connect/logout | OpenIddict Server(透传) | AuthorizationController.Logout / .LogoutPost |
POST /connect/revocation | OpenIddict Server | 框架内置处理器(项目原有的撤销端点) |
GET /connect/userinfo | OpenIddict Server | 框架内置处理器(只配置 URI,不需要写控制器) |
POST /Account/Login | 页面后端 | AccountController |
GET /Account/Antiforgery | 页面后端 | AccountController |
GET /Account/ExternalProvidersPOST /Account/ExternalLogin(表单字段 provider)GET /Account/ExternalLoginCallbackGET /Account/ExternalLoginPendingPOST /Account/ExternalLoginRegister | 页面后端 | AccountController |
GET/POST /callback/login/{provider} | OpenIddict Client(重定向端点透传) | AuthenticationController.LogInCallback |
GET /weatherforecast | 普通 API | WeatherForecastController([Authorize] + OpenIddict Validation) |
GET /Account/LoginGET /Account/ConsentGET /Account/LogoutGET /Account/ExternalLoginConfirm | React 页面(SPA MapFallback 返回 wwwroot/index.html;/api、/connect、/callback 前缀除外) | React 构建产物(wwwroot,Router 路由与处理端点同名) |
为什么"页面后端"要自己写?
它们不是 OAuth/OIDC 协议端点,而是"登录页面的后端":登录表单提交、外部提供方按钮列表、外部登录落地、防伪隐藏字段。 本项目不用 ASP.NET Core Identity 自带的 Razor 页面(AddDefaultUI),UI 全部由 React 承担, 所以这几件事由 Controllers/AccountController.cs 实现(详见 AccountController 走读)。