登录和仓库授权,为什么要拆成两段来处理?用户要用 GitHub 账号登录,登录之后还要授权 App 访问他的仓库,自动化翻译和 PR 才能跑起来。登录方案对比过 Passport.js、自写 OAuth 客户端、NextAuth.js 三套,最终选了 NextAuth.js v5。原因很直接:v5(Auth.js)相比 v4 改用 Edge-friendly 的设计、支持 OAuth Provider 灵活扩展,特别适合与 GitHub App 组合做”用户登录 + App 凭据管理”。本项目把”登录”和”仓库访问”明确分层:登录用 NextAuth.js v5 走 GitHub OAuth,仓库访问用 GitHub App Installation Token。
一、为什么选 NextAuth.js v5
选型时我先拿 v4 和 v5 做了张对比表,按项目痛点逐项打分:
| 能力 | v4 | v5 |
|---|---|---|
| 边缘运行时支持 | 有限 | 原生 |
| Provider 配置 | 固定结构 | 灵活回调 |
| 会话策略 | 默认 JWT | JWT / DB 双选 |
| TypeScript | 弱 | 一等公民 |
打分结果很明确:项目跑在边缘函数上,v4 对边缘运行时支持有限,光这一点就把它排除了;Provider 配置从固定结构变成灵活回调,意味着可以自定义授权参数,把 user:email 这种附加 scope 带进去。另外 v4 的 TypeScript 类型支持偏弱,回调参数经常要自己 cast,v5 把类型提到一等公民后,session 和 token 的扩展就顺了。不过 v5 也有代价——API 变动大,网上大部分教程还是 v4 的,迁移时得对照官方文档逐个核对,这个时间成本要提前算进去。
二、Provider 配置
在 lib/auth.ts 中声明 GitHub Provider。这段代码解决的是:告诉 NextAuth 用哪个 OAuth Provider、带哪些 scope,以及把 GitHub 返回的 access_token 塞进会话里,供后续 API 调用使用:
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
authorization: {
params: { scope: "read:user user:email" },
},
}),
],
callbacks: {
async jwt({ token, account }) {
if (account?.access_token) token.ghAccessToken = account.access_token;
return token;
},
async session({ session, token }) {
(session as any).ghAccessToken = token.ghAccessToken;
return session;
},
},
});
写完这段有个容易翻车的地方:access_token 在生产环境默认不会返回,需要在 GitHub OAuth App 设置里勾选 “Allow this app to issue user access tokens”。我在测试环境一切正常、部署后就拿不到 token,查了半天才发现是 OAuth App 设置的问题。scope 也建议按最小权限给,能跑通就别多加,后续想扩再回来改。
三、回调路由
在 App Router 下挂载 handler,代码只有三行,但这是整套登录跑通的关键入口:
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;
如果不挂这条路由,OAuth 回调就没有落点,GitHub 跳转回来会直接 404。这里要留意路径必须是 [...nextauth] 这种 catch-all 写法,GitHub OAuth App 里的回调地址也要配置成对应的完整 URL,两边只要有一处不一致,授权就静默失败。
四、登录与登出
- 触发登录:
signIn("github"); - 触发登出:
signOut(); - 服务端获取会话:
const session = await auth()。
三个动作覆盖了登录页、退出按钮和服务端守卫三个场景。踩过的坑是:登录按钮不能简单用普通链接替代,必须调用 signIn,否则 cookie 的 set 时机不对,回跳后会话对不上。登出同理,直接清 cookie 不彻底,用 signOut 才能把服务端会话状态一起清掉。
五、扩展点:User Access Token
NextAuth.js v5 不强制把 accesstoken 暴露给前端。本项目把 accesstoken 存到 JWT,再用服务端 API 拿,避免泄露到浏览器。之前有一个版本直接把 token 塞进 session 传给前端组件,浏览器里明文可读,被安全扫描标记后立刻改掉:
const session = await auth();
const token = (session as any).ghAccessToken as string | undefined;
在服务端拿 token 就安全多了——浏览器里只有加密后的 JWT,明文 access_token 始终不离开服务端。后端用 token 调 GET /user/installations 拉取用户已安装的 GitHub App 列表。
六、与 GitHub App 的协作
GitHub App 是”仓库访问”的主体,NextAuth 拿到的 User Access Token 仅用来:
- 验证用户身份;
- 列出用户安装的 App 与仓库;
- 触发安装流程(如果还没装)。
仓库读写全部走 Installation Token,与登录 token 完全解耦。这个分层让权限管理清爽很多:登录 token 只关心”你是谁”,Installation Token 只关心”App 能动哪些仓库”。之前纠结过要不要让登录 token 直接带仓库权限,后来想通了——用户授权范围和控制台配置各是一套逻辑,混在一起,撤销某个仓库授权都得动登录流程,耦合太深。
七、会话管理
- 策略:默认 JWT;
- 有效期:30 天,自动续期;
- 加密:
AUTH_SECRET通过环境变量注入; - 角色:
session.user.role决定是否能访问管理后台。
会话策略选 JWT 而不是数据库会话,是因为项目不需要在服务端保存会话状态,无状态方案在边缘函数上更省事,扩容也不用迁移会话表。30 天的有效期是产品定的:翻译工具是低频使用场景,太短用户频繁重登,太长又增加 token 泄露窗口。自动续期靠的是每次请求时刷新过期时间,配合 AUTH_SECRET 加密,前端拿到的 JWT 无法被篡改。角色字段是给管理后台用的,普通用户和管理员走同一套登录,靠它区分入口。
八、登录失败与回退
登录链路很长,任何一个环节失败都要有兜底,否则用户卡在半路不知道怎么处理:
| 场景 | 处理 |
|---|---|
| 用户拒绝授权 | 显示提示并提供重新登录入口 |
| OAuth 失败 | 显示错误码与排查链接 |
| Token 失效 | 静默重定向到登录页 |
| 多账号 | session 中存 providerAccountId,区分不同登录身份 |
几个场景里,用户拒绝授权和 Token 失效出现频率比较高。拒绝授权的处理要温和,给一个明确的”重新登录”入口就行,别让用户觉得卡死;Token 失效则要静默重定向,不能弹出错误页吓用户。多账号区分是上线后补的,有用户反馈用两个 GitHub 账号测试时状态串了,加上 providerAccountId 后各账号的会话就互不干扰了。
九、安全注意
- 回调 URL 必须使用 https,且与 GitHub OAuth 设置一致;
AUTH_SECRET至少 32 字节随机;- 不在日志中打印 access_token;
- 定期 rotate client secret。
四条里回调 URL 和 client secret 是我重点盯的。回调 URL 用 http 的话,授权回跳会暴露 code,配合钓鱼很容易被截获;client secret 的 rotate 我通过密钥管理服务做了定期轮换,换完立即在部署配置里同步,避免新旧不一致导致线上 401。日志打印 access_token 这条,我在日志框架里加了过滤,凡是命中敏感字段的一律脱敏再落盘。
十、回归与监控
- 监控:登录成功率、token 刷新次数、session 失效次数;
- 测试:用 mock GitHub API 模拟 OAuth 流程;
- 日志:登录事件留痕,但不写敏感字段。
这三项落地后,线上问题定位快了很多。以前登录偶发失败只能让用户复现,现在看登录成功率就能定位到是 OAuth 服务异常还是本地配置问题。mock 测试跑在 CI 里,GitHub 侧接口变动或者 scope 配置改错,合并前就能拦下来。
常见问题(FAQ)
Q1:NextAuth.js v5 需要数据库吗?
不一定。JWT 模式无需数据库,要做账号列表时再用 Adapter。
Q2:能不能只用一个 GitHub App 兼做登录与仓库访问?
可以,但失去了权限分层与速率隔离,不推荐。
Q3:access_token 怎么避免过期?
设置 account.refresh_token 并在 callback 中刷新,或缩短 session 过期时间让用户重新登录。