Skip to content

坑速查 ​

按"症状 → 根因 → 处理"列。全部是这个项目真实踩过的(含改造前的手工会话时代)。

一、启动 / 构建类 ​

1. Unable to resolve service for type 'ITempDataDictionaryFactory' ​

  • 症状:启动后访问 /connect/authorize 500。
  • 根因:用了 AddControllers(),而 AuthorizationController 用了 TempData(防死循环标记)。
  • 处理:改成 AddControllersWithViews()(本项目不渲染视图,只是需要它注册 TempData 与防伪过滤器相关服务)。

2. 启动抛 OpenIddict 选项校验异常(授权码流程必须有重定向端点) ​

  • 症状:SetRedirectionEndpointUris 缺失时启动失败。
  • 根因:启用某流程 ⇒ 必须有对应端点;GitHub 改成条件注册后,"没配凭据"时就没有端点。
  • 处理:在 AddClient 里显式 options.SetRedirectionEndpointUris("callback/login/github")。

3. 旧 database.db 缺 Identity 表 → 运行时 500 "no such table: AspNetUsers" ​

  • 根因:EnsureCreatedAsync 对已存在的库不会补建缺失的表。
  • 处理:启动时检测 AspNetUsers 是否存在,不存在就整库重建(本项目已实现)。

4. 删库文件报"正被另一进程使用"(Windows) ​

  • 根因:SQLite 连接池持有文件句柄;DbContext 还开着连接。
  • 处理:
    csharp
    await context.Database.CloseConnectionAsync();
    Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
    await context.Database.EnsureDeletedAsync();

5. 端口/exe 被占用:MSB3021 ... being used by another process ​

  • 根因:上一次 dotnet run 的进程还在(或 VS Code 的调试进程)。
  • 处理:先停掉进程再 build/run。用 Get-CimInstance Win32_Process 找到具体 PID 后 Stop-Process -Id <PID>。

6. 重命名 backend/ 子目录 ACCESS DENIED ​

  • 症状:VS Code / .NET 工具链正在使用该子树时,重命名该子树内的既有目录一律失败(新建的空目录可以改)。
  • 绕过:改"逐层搬子项 + 删除空目录"(移动子项与删除空目录都正常)。
  • 教训:Windows 上搬目录,先关掉可能持有句柄的 IDE 进程,或用"搬内容"而不是"搬目录"。

7. 登录后仍被要求登录(死循环) ​

  • 症状:/connect/authorize → 登录页 → 登录成功 → 又回登录页。
  • 根因:原请求带 prompt=login(或 max_age 判定不满足),导航回去又被 Challenge。
  • 处理:① TempData["IgnoreAuthenticationChallenge"] 跳过"刚从登录页回来"的那一次——这是唯一防线,在服务端(见 ① 授权请求 的 Challenge 逻辑)。
  • 历史注(2026-10-02):原先这里还列了"第②层 = React 登录页兜底重建地址时删掉 prompt",已删除—— 那是 2026-09-27 表单 POST 改造前的残留,正常路径根本不会执行(详见 ② 登录页 的"读 ReturnUrl"一节)。

8. fetch 打 /connect/authorize 拿不到结果 ​

  • 根因:响应 302 对 fetch 是"不透明重定向",JS 读不到 Location,也没有 cookie 语义。
  • 处理:登录页用表单 POST 交给浏览器(服务端 302 回 ReturnUrl);同意 / 登出页同理必须用表单,不能用 fetch。 同意页/登出页同理必须用普通表单 POST。

9. 直接访问受保护页面 302 到 404 ​

  • 根因:cookie 认证的默认 LoginPath 是 /Account/Login;如果登录页实际不在这个地址(或该地址没有页面),Challenge 落地就是 404。
  • 处理:本项目登录页恰好就是 /Account/Login(React 页面),默认值直接可用; 万一以后改了登录页地址,要显式设置 options.LoginPath,并让 appsettings 的 OAuth:LoginPageUrl 与它一致。

10. 登录 CSRF / Antiforgery 相关结论 ​

  • API(/weatherforecast)走 Bearer,没有 CSRF 攻击面,不需要防伪;
  • POST /Account/Login 是"跨站也能种 cookie"的端点 → 用同源 Origin 校验关闭面(本项目做法);
  • 同意 / 登出 POST 用 [ValidateAntiForgeryToken],React 侧用 GET /Account/Antiforgery 取令牌(表单无法自定义请求头 → 统一走隐藏字段)。

三、同意流程类 ​

11. 每次登录都弹同意页 ​

按可能性排查:

  1. 客户端 ConsentType 不是 Explicit?(本项目是 Explicit,符合预期会弹)
  2. 服务重启过?(清空了 oi_ath)
  3. 请求的 scope 变了?(永久授权按 scope 精确匹配,多一个少一个都不算命中)
  4. 客户端带了 prompt=consent?(强制再确认)

12. 点"拒绝"后客户端收到 error=access_denied ​

  • 这是预期行为(ID2015),由 Deny() 的 Forbid(OpenIddict scheme) 产生。

13. 同意页 / 登出页 / 登录页表单提交防伪失败 ​

  • 根因:没先 GET /Account/Antiforgery,或提交时漏了隐藏字段 __RequestVerificationToken (AntiforgeryOptions.FormFieldName,框架默认名)。
  • 注意:这个 GET 会种下 HttpOnly 的 cookie 令牌(.AspNetCore.Antiforgery.*);先清 cookie 再拿令牌是最稳的测试姿势。
  • 原生表单 POST 无法自定义请求头,所以四个页面统一用表单字段;请求头通道(HeaderName,默认 RequestVerificationToken)是给 fetch/XHR 场景的,本项目用不到。

13′. 登录后又被送回登录页,URL 上带 ?error=... ​

  • 症状:POST /Account/Login 返回 302,Location 是 /Account/Login?ReturnUrl=...&error=invalid_credentials (账号密码错);缺/错防伪令牌则是 400 空响应体([ValidateAntiForgeryToken],与 Identity UI 一致;本项目不再做 Origin 校验)。
  • 根因:登录是表单 POST,失败不会返回 JSON,而是"302 回登录页 + error 查询参数", 由 React 页面把 error 映射成中文提示。
  • 修法:① 缺令牌 → 先 GET /Account/Antiforgery(cookie jar 保存 cookie 令牌)并把 token 放进隐藏字段; ② 用 curl 测试时别忘 -b/-c cookies.txt 保持会话(cookie 令牌与请求令牌必须配对)。

13″. 同意页按回车 / 参数丢失 → 再点"同意"报 client_id is missing(ID2029) ​

  • 症状:在同意页按回车(表单里没有 submit.Accept / submit.Deny)后,页面变成一个没有参数的同意页; 再点"同意"直接报 error:invalid_request … The mandatory 'client_id' parameter is missing。
  • 根因:Authorize 同时接 GET 与 POST。缺 submit.* 时两个 FormValueRequired action 都不匹配 → MVC 回落到 Authorize → 又走"弹同意页"分支。此时授权参数在表单里, 而"302 到 React 同意页"这一步只读了 Request.Query → 参数全丢。
  • 修法(已改):按 Request.HasFormContentType ? Request.Form : Request.Query 取参数(Razor 版同意页也是同一判断; 但要排除 __RequestVerificationToken, 见③ 同意流程)。
  • 顺带确认:回车提交不会创建永久授权、也不会签 code(Accept 只认 submit.Accept)。

四、Token / 校验类 ​

14. 换 token 报 ID2054 / ID2052 ​

  • ID2054:授权请求没带 PKCE(本项目客户端强制 PKCE)。
  • ID2052:code_verifier 与 code_challenge 不匹配。测试时必须用真配对: code_challenge = base64url(SHA256(ASCII(code_verifier)))。

15. 拿不到 refresh_token ​

  • 根因:OpenIddict 只在令牌请求含 offline_access scope 时才发 refresh token。
  • 处理:客户端 scope 加上 offline_access(且客户端权限里有 Permissions.Prefixes.Scope + Scopes.OfflineAccess)。

16. 角色没有出现在 id_token 里 ​

  • 根因:GetDestinations 里 role 进 id_token 需要 roles scope:claim.Subject.HasScope(Scopes.Roles)。
  • 处理:客户端请求 roles scope,并把它注册到 RegisterScopes + 客户端权限白名单。

17. Permissions.Scopes.OpenId 不存在 ​

  • openid 由 OpenIddict 默认允许,代码里没有这个常量 → 写了会编译不过。

18. access token 里的 claim 少了几个 ​

  • 根因:OpenIddict 默认不把自定义 claim 放进 token,必须 SetDestinations(...) 声明目的地。
  • 另注意安全戳 claim 被特意排除(敏感值)——类型取自 IdentityOptions.ClaimsIdentity.SecurityStampClaimType(默认 AspNet.Identity.SecurityStamp),不硬编码。

五、前端 / SPA 类 ​

  • 根因:React Router history 模式,磁盘上没有该文件。
  • 处理:确认 SPA 回落生效(app.MapFallback 返回 wwwroot/index.html,且该路径不在 /api、/connect、/callback 排除前缀内)。

20. 页面白屏 / ErrorBoundary ​

  • 根因:配了 vite base 但没配 React Router basename。
  • 处理:{ basename: import.meta.env.BASE_URL.replace(/\/$/, '') }。

21. 目录请求 / 404 ​

  • 处理:app.UseDefaultFiles()(把目录请求解析到 index.html,React 产物就在 wwwroot 根)+ app.UseStaticFiles(),且要在认证之前。

22. 跨源调用被浏览器拒绝 ​

  • 症状:5173 客户端拉 discovery / 换 token 失败,控制台报 CORS 或 credential 问题。
  • 处理:WithOrigins("http://localhost:5173").AllowCredentials();不要用 AllowAnyOrigin()(与 credentials 冲突)。

23. React Compiler 与"提前 return" ​

  • 症状:组件编译失败(本地曾出现 Login.tsx 里 try 块内提前 return + 三元表达式)。
  • 处理:把提前 return 改写成 if/else 链、把 setState 移出 finally。 排查手段:给 React 插件传 logger 选项拿到 CompileError.reason。

23′. 提交时给按钮加 disabled → 字段丢了,服务端 400 ​

  • 症状:同意页点"同意"后 400(或登出页点了没反应),日志里 [FormValueRequired("submit.Accept")] 匹配不上。
  • 根因:浏览器是在 submit 事件之后才收集表单数据的,而 disabled 控件不进入表单数据; 如果在提交时把 name="submit.Accept"(或登出页的 Confirm)按钮 disable 掉,这个字段就消失了。
  • 处理:提交中用 antd Button loading(只加 class + 拦后续点击,不加 disabled 属性),别写 disabled={submitting}。 同理"提交中把输入框 disable"也会把用户名/密码挤出表单数据。

23″. 以为"用了 form 就没有 loading" ​

  • 症状:想给表单提交加转圈,但觉得 form 会整页跳走所以做不到。
  • 事实:浏览器在新文档开始接管前不会卸载旧文档,所以 onSubmit 里切 submitting → 按钮 loading + <Spin fullscreen>,在"服务端处理 + 网络往返"期间完全可见(业界登录页都这么做)。
  • 处理:四页统一 onSubmit={handleSubmit}(不 preventDefault,只在重复提交时拦);见 表单登录与 loading(业界做法)。

23‴. 客户端登出后界面仍是"已登录 + 旧 token"(假登录态) ​

  • 症状:从 demo 客户端点"登出"、在 OP 登出页确认、跳回 5173 后,界面还显示 token 面板和"登出"按钮; 刷新也不变。此时 OP 会话其实已经清掉了。
  • 根因:客户端只做了 window.location.href = .../connect/logout,没有删 localStorage 里的 user (oidc.user:https://localhost:7016:client-demo)→ 回到页面时 getUser() 仍能读到旧用户。
  • 修法(已改):跳转前 await userManager.removeUser()(清 token 并触发 userUnloaded → setUser(null)), 再 clearStaleState()、最后才跳 end-session;见客户端说明。

六、外部登录类 ​

24. 登录页没有「使用 GitHub 登录」按钮 ​

  • 根因:没配 OAuth:External:GitHub:ClientId(未配置时 /Account/ExternalProviders 返回 [])。
  • 处理:填配置(或环境变量)后重启 OP。

25. POST /Account/ExternalLogin(provider=GitHub)404 ​

  • 同上:提供方未注册——SignInManager.GetExternalAuthenticationSchemesAsync() 里没有 GitHub(即没有注册带 DisplayName 的转发 scheme)。

26. 回调报 The external authorization data cannot be used for authentication. ​

  • 根因:提供方没返回可用的用户 claim(Principal.Identity.IsAuthenticated == false)。
  • 说明:这时应该改用 result.Properties.GetTokens() 拿 access token 代表用户做事,而不是把它当登录。

27. 外部登录后回到登录页 ​

  • 根因:/Account/ExternalLoginCallback 拿不到外部身份(cookie 丢了 / 直接手输了该 URL)。
  • 预期行为:本项目在这种情况下 302 回 /Account/Login?ReturnUrl=...&error=external_login_unavailable(Identity 默认 UI 的等价做法是"回登录页并提示"), 用户重新点"使用 GitHub 登录"即可。

七、文档站 / CI 类 ​

28. GitHub Pages 子路径资源 404 ​

  • 根因:仓库是项目站点(https://<user>.github.io/<repo>/)但 base 没配。
  • 处理:docs/.vitepress/config.ts 里 base: '/yagang-identity/'。
  • 根因:VitePress 会检查站内链接。写到 docs/ 目录之外的相对链接(如 ../../backend/...)会被判为死链。
  • 处理:文档里提到仓库路径时用 code 行内代码,不要做成 Markdown 链接。

30. 部署 job 失败:Pages 未启用 ​

  • 症状:actions/configure-pages 报 Pages 未启用 / 或 actions/deploy-pages 失败。
  • 处理:仓库 Settings → Pages → Build and deployment → Source 选 GitHub Actions(只需一次)。

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