本地跑起来
前置
- .NET SDK 10(
global.json已锁定10.0.400) - Node.js 22+ 与 pnpm
- 浏览器信任本地 HTTPS 开发证书(
dotnet dev-certs https --trust)
三个进程
# 1) OP(授权服务器 + 资源服务器,7016)
cd backend/Yagang.Identity/Yagang.Identity.Api
dotnet run
# 2) SPA 客户端(5173)
cd demos/yagang-client-demo
pnpm install && pnpm dev
# 3) 改登录页时用(5175,proxy 把 /connect 代理到 7016;平时不需要)
cd frontend/yagang-identity-app
pnpm install && pnpm dev浏览器打开 http://localhost:5173,点「登录」。
登录页改了要重新构建
OP 托管的是 frontend/yagang-identity-app 的构建产物(输出到 OP 的 wwwroot)。 改完页面要 pnpm build(在 frontend/yagang-identity-app 下),OP 那边刷新即可看到新页面。
演示账号
| 用户名 | 密码 | 角色 | 来源 |
|---|---|---|---|
alice | P@ssw0rd-001! | admin | 启动时种子创建(OAuth:DemoUserName / OAuth:DemoUserPassword) |
手动验证一次完整流程(curl 版)
也可以直接跑现成的脚本
仓库里有 scripts/verify-flow.mjs(node,无需装依赖):覆盖登录 4 种结果 / 同意(含"按回车"边界)/ 拒绝 / 换 token + PKCE / 刷新 / 登出 / 二次登录免同意 / 协议错误响应(未知 client_id),共 40 条断言,逐跳检查 302 的 Location。
# 先重启 OP(脚本要断言"首次授权必然弹同意页",依赖启动时清空 OpenIddict 四张表),再:
node scripts/verify-flow.mjs下面这段 curl 版更适合"看懂每一步在传什么"。
自动化的"端到端"其实可以用 curl 串起来做,理解每一步在传什么很有用。要点:
BASE=https://localhost:7016
JAR=cookies.txt # 会话 cookie 都放这里
# ① 未认证的授权请求 → 应当 302 到 /Account/Login?ReturnUrl=...
curl -sk -c $JAR -o /dev/null -D - \
"$BASE/connect/authorize?client_id=client-demo\
&redirect_uri=http%3A%2F%2Flocalhost%3A5173%2Fsignin-callback\
&response_type=code&scope=openid%20demo_api%20offline_access\
&code_challenge=<真 PKCE challenge>&code_challenge_method=S256"
# ② 登录(表单 POST,种 Identity cookie)
# 先取防伪请求令牌(记下返回 JSON 里的 token;下面把它放进隐藏字段)
curl -sk -b $JAR -c $JAR "$BASE/Account/Antiforgery"
# → {"token":"CfDJ8...","formFieldName":"__RequestVerificationToken"}
curl -sk -b $JAR -c $JAR -o /dev/null -D - -X POST "$BASE/Account/Login" \
-H 'Origin: https://localhost:7016' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data "ReturnUrl=%2Fconnect%2Fauthorize%3Fclient_id%3Dclient-demo\
&username=alice&password=<演示密码>&__RequestVerificationToken=<上一步的 token>"
# → 302 Location: /connect/authorize?client_id=client-demo(成功)
# 凭据失败是 302:/Account/Login?ReturnUrl=...&error=invalid_credentials
# 防伪令牌缺失/不配对是 400(空响应体,同 Identity UI)
# 例外:ReturnUrl 非站内(如 https://evil.example)→ LocalRedirect 抛异常 → 500(与 Identity UI 一致)
# ③ 重放①:这次应 302 到 /Account/Consent?...
# ④ 取防伪 token
curl -sk -b $JAR -c $JAR "$BASE/Account/Antiforgery" # → {"token":"..."}
# ⑤ 同意(把①的全部授权参数 + __RequestVerificationToken + submit.Accept=Yes 一起 POST 到 /connect/authorize)
# ⑥ 换 token
curl -sk -b $JAR -X POST "$BASE/connect/token" \
-d 'grant_type=authorization_code&client_id=client-demo&code=<code>' \
-d 'redirect_uri=http://localhost:5173/signin-callback&code_verifier=<verifier>'
# ⑦ 调 API
curl -sk -H "Authorization: Bearer <access_token>" "$BASE/weatherforecast"PKCE 必须真配对
测试时不能用随机字符串当 code_verifier:code_challenge 必须是 base64url(SHA256(verifier)), 否则换 token 会拿到 invalid_grant(ID2052 invalid code_verifier)。
Swagger 里调 API
https://localhost:7016/swagger:点右上角 Authorize,粘贴 access token(不要加 Bearer 前缀, 文档里声明的是 http + bearer 类型 scheme,Swagger UI 会自动补 Bearer 前缀),然后调 GET /weatherforecast。
换 token 后看什么
POST /connect/token 的响应里,本项目种子用户 alice 的 access token 解出来应当有:
| claim | 值 | 由谁写入 |
|---|---|---|
sub | Identity 用户 Id(GUID) | AuthorizationController 的 identity.SetClaim(Claims.Subject, ...) |
name / preferred_username | alice | 同上 |
email | alice@example.com | 同上 |
role | admin | GetRolesAsync + GetDestinations |
scope | 请求的 scope 集合 | identity.SetScopes(request.GetScopes()) |
重启后的行为
- 启动时会清空 OpenIddict 四张表 → 旧的 token 全部失效,永久授权也一并没了 → 重启后第一次授权会再次弹同意页。
- 演示用户与客户端注册会在启动时幂等重建(已存在则跳过)。