登录、邮件与 OAuth

沿着登录状态、邮箱验证、邮件发送和 OAuth 回调定位认证问题。

登录失败、收不到邮件和第三方回调失败经常出现在同一条流程中,但处理方式不同。先在浏览器 Network 中找出失败请求,再查看响应体和服务端日志,不要只依据页面最后跳转到了哪里判断。

一、登录后又回到登录页

按顺序观察两个请求:登录请求的响应,以及登录后第一个需要身份的请求。

  1. 登录响应是否成功,是否要求继续验证邮箱或双重验证码。
  2. 响应是否设置了 Cookie,浏览器是否提示该 Cookie 被拒绝。
  3. 后续请求是否带上 Cookie,Host 和协议是否与登录时一致。
  4. 服务端是否能读取到有效会话,用户状态是否仍允许访问。

本地不要混用 localhost 和 127.0.0.1,生产不要从 workers.dev 登录后转到自定义域名继续验证。不同主机的 Cookie 不会自动共享。

生产环境还要核对 VITE_SITE_URL、HTTPS、代理是否改写响应头,以及浏览器是否实际访问了旧站点。若怀疑旧 Cookie 影响结果,可以使用新的浏览器会话复现,但不能只以“清除 Cookie 后好了”代替查明域名或会话问题。

不要把 Cookie 原值复制进日志或工单。需要关联会话时,在受控环境中检查 saas_session 的有效期和对应用户即可。

二、区分 401、403 和 429

状态常见方向还需要查看什么
401缺少有效登录会话或令牌Cookie、会话有效期、接口使用的认证方式
403验证状态、权限、账号状态或请求来源不满足要求code、error、details、中间件日志
429请求被限流触发的操作、响应中的等待信息、限流配置

403 并不总能证明访问者已经登录。有些请求在进入业务处理前就会被来源或安全策略拒绝,也不能把所有 403 都当作缺少管理员角色。

如果响应包含 details.redirectUrl,先看它要求进入的是邮箱验证、双重验证还是其他流程。不要通过移除 guard 或给普通用户授予管理员角色来解决认证问题。

认证拦截和节流有些只在生产模式挂载,所以本地反复尝试成功不代表线上不会触发限制。持续重试还可能延长排查过程,应先停止重复操作,再核对当前配置。

三、注册后为什么要求邮箱验证

当前模板的 auth.emailVerification.defaultRequired 默认为 false,默认不要求所有新用户先完成邮箱验证。用户被要求验证时,检查当前项目是否调整了配置,或者正在执行一个单独要求验证的账号操作。

邮箱验证涉及验证会话、邮件内容和当前浏览器会话。收到多封邮件时,优先使用当前操作产生的最新邮件,不要把另一次注册、修改邮箱或重置密码的验证码混在一起。

如果提示无效或过期,应检查对应验证会话和时间,而不是手工把用户的邮箱验证状态改成成功。密码重置还可能要求继续完成双重验证,邮箱验证通过不等于整个重置流程完成。

四、本地显示发送成功,却没有收到邮件

当前 npm run dev 使用开发模式,邮件组件会在终端输出:

[DEV EMAIL PREVIEW]

其中包含收件地址、标题和正文,发送结果使用开发预览 ID。此时不会调用真实邮件服务,即使 .env 已填写 Resend Key,也不能用收件箱是否收到邮件判断开发流程成败。

在本地终端查看这次预览,使用其中的验证信息完成流程。预览内容可能包含验证码和验证链接,不要直接上传完整终端截图。

npm run preview 经过打包流程,不能把开发模式的邮件模拟行为套用到所有预览或生产运行方式。验证真实投递时,应使用配置完整的相应环境,并在服务商侧确认投递结果。

五、生产环境邮件发送失败

先运行 npm run doctor:remote,再检查实际失败请求。诊断脚本读取的是本地 .env.production,不能证明线上 Worker 已加载这份最新配置。

当前模板默认使用 Resend:

项目位置检查内容
config/deploy.tsemailProvider.type 是否为 resend
.env.productionRESEND_API_KEY 是否填写且属于目标账号
config/base.ts实际发件地址 fromEmailAddress.email,以及回复地址 supportEmail
Resend 控制台发件域名是否验证,Key 是否有发送权限,投递是否失败
线上 Worker修改 Secret 后是否执行了更新部署

若改用 Cloudflare Email,检查 EMAIL 类型为 send_email 的 Binding。如果配置了 allowed_sender_addresses,其中必须包含实际发件地址。还要按当前邮件服务配置检查收件限制,不能假定添加 Binding 就能向任意地址发送。

只有某类邮件失败时,检查 src/libs/email/template/factory.ts 中的模板注册、调用数据和语言文案。配置错误、模板渲染错误、服务商拒绝和最终退信属于不同阶段。

服务商接受请求后仍未收到邮件,应继续查看投递、退信和垃圾邮件情况。应用返回发送成功,只表示当前发送调用成功,不保证邮件已经进入收件箱。

六、出现 emailSendTooOften

配置位于 deploy.spam.resourceProtection.emailSendService,默认窗口为 1d、上限为 5。虽然字段名是 maxEmailsPerUser,当前受保护发送流程实际使用收件邮箱生成计数 Key。

这是一个配额窗口,不应理解为一定在收件人所在时区的午夜重置。开发模式遇到配额问题会记录提示,生产模式则可能拒绝继续发送。

排查时确认是否重复点击、前端自动重试,或多个流程向同一邮箱反复发送。先停止重复调用,再根据业务需要评估配置,不要为通过测试而清空线上计数或无限提高上限。

七、双重验证码或恢复码无效

先确认账号是否已经完成双重验证设置,然后检查验证器设备时间、当前使用的账号条目,以及是否误用了旧设置中的验证码。

恢复码通常只能按设计使用一次。用户提交已经使用过的恢复码,应走剩余恢复方式,不应修改数据库把它重新标记为未使用。

如果多个用户同时出现双重验证资料无法解密,重点检查发布时是否更换了 SAAS_SECRET。它还保护 OAuth 客户端 Secret 和其他加密资料,应恢复项目原有配置并评估影响,而不是再生成一个新密钥。

八、管理员邮箱已配置,但仍进不了后台

config/base.ts 的 adminEmails 不是“每次登录自动变成管理员”的开关。

当前 UserEmailVerifiedEvent 只在本站账号邮箱验证流程中,按已验证邮箱与名单的精确匹配结果生成管理员角色授予命令。密码重置邮箱验证不会授予,第三方 OAuth 返回邮箱已验证也不能代替该流程。

按顺序检查:名单是否与已验证地址一致、是否完成本站账号验证、相关 Event 和 grant_role Command 是否成功。已授予角色也不会因为从配置名单删除邮箱就自动撤销,撤销需要走相应权限管理流程。

九、Google 或 GitHub 登录失败

第三方登录和应用自己提供的 OAuth 服务分开配置。Google、GitHub 登录使用下面的回调:

提供商回调地址
Google{VITE_SITE_URL}/api/auth/oauth2/google
GitHub{VITE_SITE_URL}/api/auth/oauth2/github

回调不匹配时,直接比较浏览器实际发出的 redirect_uri 与提供商后台的值,包括协议、主机、端口和路径。不要只检查 .env 中“看上去正确”的地址。

按钮未显示或初始化失败时,检查对应的 Client ID 和 Client Secret 是否成对填写。Google One Tap 还要求 auth.enableGoogleOneTap 开启,模板默认关闭该功能。

回调后状态校验失败时,检查发起和完成登录是否属于同一浏览器会话、同一个站点地址,是否使用了旧标签页中的授权链接。域名切换后要同时更新平台回调和应用配置。

具体平台配置步骤见Google OAuth和GitHub OAuth。

十、内置 OAuth 2.0 服务无法授权

内置服务使用 /oauth/*,其客户端在本应用中管理,不能使用 Google 或 GitHub 的 Client Secret 来请求本应用令牌。

检查客户端状态、Redirect URI、允许的 Grant Type、请求 Scope,以及 PKCE 参数。模板默认要求 S256,授权请求中的 Challenge 和换令牌时的 Verifier 必须来自同一次授权流程。

授权码不能当作长期凭据重复使用。排查换令牌失败时,重新开始一次完整授权,保存脱敏后的协议错误名,不记录授权码、客户端 Secret 或令牌原文。

若授权页提示升级,检查 config/upgrade.ts。当前模板默认配置为空,定制规则后才会按角色或权益条件展示相应升级方案。规则匹配与业务 API 的权限检查仍需分别验证。

修复后的验证

使用普通账号完成一次登录、退出和再次登录。涉及邮件时,确认验证状态确实更新。涉及 OAuth 时,从新发起授权直到回到应用完整执行一次。涉及管理员角色时,再用普通账号确认后台仍然不可访问。