Skip to content

本地跑起来 ​

前置 ​

  • .NET SDK 10(global.json 已锁定 10.0.400)
  • Node.js 22+ 与 pnpm
  • 浏览器信任本地 HTTPS 开发证书(dotnet dev-certs https --trust)

三个进程 ​

bash
# 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 那边刷新即可看到新页面。

演示账号 ​

用户名密码角色来源
aliceP@ssw0rd-001!admin启动时种子创建(OAuth:DemoUserName / OAuth:DemoUserPassword)

手动验证一次完整流程(curl 版) ​

也可以直接跑现成的脚本

仓库里有 scripts/verify-flow.mjs(node,无需装依赖):覆盖登录 4 种结果 / 同意(含"按回车"边界)/ 拒绝 / 换 token + PKCE / 刷新 / 登出 / 二次登录免同意 / 协议错误响应(未知 client_id),共 40 条断言,逐跳检查 302 的 Location。

bash
# 先重启 OP(脚本要断言"首次授权必然弹同意页",依赖启动时清空 OpenIddict 四张表),再:
node scripts/verify-flow.mjs

下面这段 curl 版更适合"看懂每一步在传什么"。

自动化的"端到端"其实可以用 curl 串起来做,理解每一步在传什么很有用。要点:

bash
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值由谁写入
subIdentity 用户 Id(GUID)AuthorizationController 的 identity.SetClaim(Claims.Subject, ...)
name / preferred_usernamealice同上
emailalice@example.com同上
roleadminGetRolesAsync + GetDestinations
scope请求的 scope 集合identity.SetScopes(request.GetScopes())

重启后的行为 ​

  • 启动时会清空 OpenIddict 四张表 → 旧的 token 全部失效,永久授权也一并没了 → 重启后第一次授权会再次弹同意页。
  • 演示用户与客户端注册会在启动时幂等重建(已存在则跳过)。

OpenIddict + ASP.NET Core Identity 授权服务器,React 登录页,逐步拆解登录链路