跟着双越老师的划水AI项目学习记录,从Vue转向React+Next.js的踩坑经验分享。
前言
本文记录我在学习 day007 - 使用 NextAuth 实现 Github 登录一节中, Next.js 15 与 NextAuth v5(Auth.js)中接入 GitHub 登录的完整过程。重点是理解一次登录背后,浏览器、Next.js、NextAuth 和 GitHub 如何协作。
一、这次实践涉及哪些技术
一个看似简单的“使用 GitHub 登录”,实际上串起了多个前后端知识点:
- Next.js App Router:通过文件和目录建立 URL 路由。
- Route Handler:使用
route.ts处理GET、POST等 HTTP 请求。 - React Server Component:在服务端组件中调用
auth()获取当前登录状态。 - Server Action:通过
<form action>在服务端调用signIn()和signOut()。 - Next.js Middleware:在页面渲染前检查用户是否有权访问目标路径。
- OAuth 2.0 授权码流程:在应用、浏览器和 GitHub 之间完成授权与 Token 交换。
- 认证与授权:先确认“用户是谁”,再判断“用户能访问什么”。
- Cookie 与 Session:在后续请求中维持用户的登录状态。
- HTTP 重定向:通过
302、303和Location在多个地址之间跳转。 - 服务端网络与代理:浏览器能访问 GitHub,不代表 Node.js 服务端也能访问。
- 约定优于配置:Next.js 根据特殊文件名和导出名称自动连接框架能力。
- 第三方能力集成:将高风险、复杂的认证流程交给成熟认证库处理。
二、项目中的认证文件
最终涉及以下文件:
src/
├── auth.ts
├── middleware.ts
└── app/
├── api/
│ └── auth/
│ └── [...nextauth]/
│ └── route.ts
└── user-test/
└── page.tsx
它们各自负责不同的事情:
| 文件 | 职责 |
|---|---|
src/auth.ts | 配置 GitHub Provider,生成认证相关函数,定义授权规则 |
src/app/api/auth/[...nextauth]/route.ts | 将 NextAuth 的 HTTP 处理器交给 Next.js |
src/app/user-test/page.tsx | 展示 Session,并提供登录、退出入口 |
src/middleware.ts | 在目标页面渲染前执行认证和授权判断 |
三、初始化 NextAuth
安装 NextAuth v5 beta:
npm install next-auth@beta
在 src/auth.ts 中配置 GitHub Provider:
import NextAuth from 'next-auth'
import GitHub from 'next-auth/providers/github'
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub],
callbacks: {
authorized({ request, auth }) {
const { pathname } = request.nextUrl
if (pathname.startsWith('/work/')) {
return !!auth
}
return true
},
},
})
NextAuth(...) 返回了多项能力:
handlers:处理认证相关 HTTP 请求。auth:读取和验证当前 Session,也可以作为 middleware 使用。signIn:发起登录。signOut:退出登录。
环境变量保存在项目根目录的 .env.local:
AUTH_SECRET=随机生成的应用密钥
AUTH_GITHUB_ID=GitHub OAuth App Client ID
AUTH_GITHUB_SECRET=GitHub OAuth App Client Secret
这些 Secret 只能在服务端使用,不能写入客户端组件,也不能提交到 Git 仓库。
四、route.ts 如何把请求交给 NextAuth
认证接口位于:
src/app/api/auth/[...nextauth]/route.ts
代码很短:
import { handlers } from '@/auth'
export const { GET, POST } = handlers
这段代码等价于:
export const GET = handlers.GET
export const POST = handlers.POST
它之所以有效,依赖两层 Next.js 约定。
1. 文件路径匹配 URL
[...nextauth] 是 catch-all 动态路由,可以匹配 auth 后面的多个路径:
/api/auth/signin
/api/auth/signout
/api/auth/session
/api/auth/callback/github
2. 导出名称匹配 HTTP 方法
在 route.ts 中,Next.js 会按照请求方法寻找同名导出:
GET 请求 → GET(request)
POST 请求 → POST(request)
DELETE 请求 → DELETE(request)
因此,当浏览器访问:
GET /api/auth/callback/github?code=...
Next.js 会执行:
handlers.GET(request)
随后 NextAuth 再从 URL 中解析:
action = callback
provider = github
code = GitHub 返回的临时授权码
route.ts 没有自己实现 OAuth,它只是连接 Next.js 与 NextAuth 的适配层。
五、测试页面如何读取登录状态
src/app/user-test/page.tsx 是服务端组件。它的核心结构如下:
import { auth, signIn, signOut } from '@/auth'
async function login() {
'use server'
await signIn('github')
}
async function logout() {
'use server'
await signOut()
}
export default async function UserTest() {
const session = await auth()
return (
<div>
<p>{JSON.stringify(session)}</p>
<form action={login}>
<button type="submit">登录</button>
</form>
<form action={logout}>
<button type="submit">退出登录</button>
</form>
</div>
)
}
访问 /user-test 时:
GET /user-test
↓
Next.js 根据 src/app/user-test/page.tsx 匹配路由
↓
React 服务端渲染器调用默认导出的 UserTest()
↓
UserTest() 执行 await auth()
↓
NextAuth 从当前请求中读取并验证 Session Cookie
↓
服务端根据 session 生成页面结果
最初我把 signIn() 写进了按钮的 onClick,随后遇到错误:
Event handlers cannot be passed to Client Component props
原因是 page.tsx 是服务端组件,服务端函数不能通过 onClick 传给浏览器。这里使用 <form action={login}> 和 Server Action,可以保留页面的服务端组件边界。
六、一次完整的 GitHub 登录流程
下面是从打开测试页到显示用户信息的完整链路:
浏览器访问 GET /user-test
↓
Next.js 匹配 src/app/user-test/page.tsx
↓
服务端执行 UserTest(),其中调用 auth()
↓
NextAuth 读取并验证 Session Cookie
↓
未登录,因此页面显示登录表单
↓
用户提交登录表单
↓
浏览器提交 POST /user-test
↓
Server Action 调用 signIn('github')
↓
NextAuth 生成 GitHub OAuth 授权地址
↓
浏览器跳转到 GitHub 授权页
↓
用户在 GitHub 确认授权
↓
GitHub 返回重定向响应:
Location: http://localhost:3000/api/auth/callback/github?code=...
↓
浏览器根据 Location 访问本地 callback 地址
↓
Next.js 调用 middleware(在其匹配范围内)
↓
auth middleware 读取当前 Session,并调用 callbacks.authorized
↓
该路径不是 /work/*,authorized 返回 true,继续处理请求
↓
Next.js 匹配 src/app/api/auth/[...nextauth]/route.ts
↓
请求方法是 GET,因此调用 handlers.GET(request)
↓
NextAuth 解析出 callback、github 和临时 code
↓
NextAuth 服务端携带 code、Client ID、Client Secret
请求 GitHub Token 接口
↓
GitHub 校验成功,返回 Access Token
↓
NextAuth 使用 Access Token 获取 GitHub 用户资料
↓
NextAuth 为当前应用创建 Session
↓
NextAuth 在重定向响应中返回 Set-Cookie
↓
浏览器保存 Session Cookie
↓
浏览器根据 Location 再次 GET /user-test
↓
新请求自动携带 Session Cookie
↓
UserTest() 再次执行 auth()
↓
这次 auth() 返回已登录用户的 Session
↓
页面显示 GitHub 用户信息
这里有一个容易误解的细节:GitHub 服务器不能主动请求开发者电脑上的 localhost。所谓“GitHub 回调本地”,实际是 GitHub 返回一个重定向响应,让浏览器访问本地 callback URL。
七、OAuth 授权码为什么要分两段
登录过程中,GitHub 首先把临时 code 放在回调 URL 中:
/api/auth/callback/github?code=...
随后 NextAuth 在服务端使用这个 code 换取 Access Token。
之所以不直接把 Access Token 放进浏览器 URL,是为了避免真正的访问凭证暴露在地址栏、浏览器历史和前端脚本中。Client Secret 也始终只存在于服务端。
可以把这两段理解为:
浏览器参与:用户确认“我允许这个应用访问”
服务端参与:应用证明自己的身份,并用临时 code 换取 Token
这个流程与单点登录有相似体验,但概念并不完全相同:
- OAuth 主要解决第三方授权问题。
- GitHub 登录利用 OAuth 获得用户身份资料,再建立当前应用自己的 Session。
- SSO 强调用户登录一次后,可以访问多个相互信任的系统。
八、middleware 如何执行 authorized
src/middleware.ts 中只有一行:
export { auth as middleware } from '@/auth'
它近似等价于:
import { auth } from '@/auth'
export const middleware = auth
Next.js 识别 src/middleware.ts 和名为 middleware 的导出。当匹配范围内的请求到来时,执行链路是:
Next.js 收到请求
↓
Next.js 调用 middleware(request)
↓
实际调用 NextAuth 生成的 auth(request)
↓
auth 读取并解析 Session
↓
auth 调用配置中的 callbacks.authorized({ request, auth: session })
↓
根据 authorized 返回值决定继续、拒绝或重定向
需要区分两个同名概念:
export const { auth } = NextAuth(...)
这里导出的 auth 是一个函数。
authorized({ request, auth }) {}
这里参数中的 auth 是当前请求解析出的 Session;未登录时通常是 null。
当前授权规则是:
/work/* + 已登录 → 放行
/work/* + 未登录 → 拦截并进入登录流程
其他路径 → 放行
middleware 解决的是页面入口保护,但它不能代替完整的业务权限校验。例如,用户已经登录,不代表他可以修改其他用户的文档。更新和删除数据时,服务端仍需校验资源归属。
九、为什么登录成功后又请求了一次页面
Session Cookie 是在 callback 的响应中才写入浏览器的。登录前已经渲染的页面并不知道新 Session,因此 NextAuth 会重定向回应用,触发一次新请求:
callback 响应
├── Set-Cookie: 写入 Session
└── Location: /user-test
↓
浏览器保存 Cookie
↓
浏览器重新 GET /user-test,并自动携带 Cookie
↓
服务端重新执行 auth()
↓
页面得到已登录状态
两次请求的关键差别是:
第一次 GET /user-test:没有 Session Cookie
第二次 GET /user-test:携带 Session Cookie
十、一次服务端代理故障
接入过程中,GitHub 授权页可以正常打开,但授权后回到了:
/api/auth/error?error=Configuration
真正有价值的信息不在浏览器错误页,而在 Next.js 服务端终端:
[auth][cause]: TypeError: fetch failed
[auth][details]: {
"name": "ConnectTimeoutError",
"code": "UND_ERR_CONNECT_TIMEOUT",
"provider": "github"
}
这说明流程已经完成了:
GitHub 授权成功
↓
浏览器成功回到本地 callback
↓
NextAuth 成功取得临时 code
↓
Node.js 请求 GitHub Token 接口时连接超时
问题最终通过让运行 Next.js 的终端进程使用代理解决。
这次故障让我认识到:
浏览器访问 GitHub ≠ Node.js 能访问 GitHub
curl 能访问 GitHub ≠ Node.js fetch 一定能访问 GitHub
前端跳转成功 ≠ 服务端 Token 交换成功
排查 OAuth 问题时,应先判断失败发生在哪一段,而不是看到 Configuration 就立刻重新生成 Secret。
十一、我对框架封装的新认识
这次实践中最“隐晦”的两行代码是:
export const { GET, POST } = handlers
export { auth as middleware } from '@/auth'
它们体现的是框架常见的“控制反转”和“约定优于配置”:
- 我们没有手动启动路由处理器,而是导出符合 Next.js 约定的函数,等待框架调用。
- 我们没有自己实现 OAuth,而是向 NextAuth 提供 Provider 和回调规则。
- Next.js 负责决定何时调用
GET、POST、页面组件和 middleware。 - NextAuth 负责解析认证 action、交换 Token、读取用户资料和管理 Session。
封装虽然减少了业务代码,但学习时不能只记住两行配置。只有展开浏览器、Next.js、NextAuth、GitHub 和 Cookie 之间的完整链路,遇到问题时才能判断故障究竟位于路由、配置、授权、网络、Token 交换还是 Session 阶段。
十二、本节结论
完成 GitHub 登录后,我不只是获得了一个登录按钮,还第一次完整接触了:
文件系统路由
→ Route Handler
→ Server Action
→ OAuth 授权码
→ 服务端 Token 交换
→ Session Cookie
→ 服务端重新渲染
→ middleware 页面保护
→ authorized 授权判断
后续仍需要继续解决:
- 将登录用户与数据库中的业务数据关联。
- 在服务端校验文档所有权,防止越权访问。
- 为登录失败、网络超时和权限拒绝提供清晰提示。
- 配置生产环境域名、HTTPS 和 GitHub OAuth callback URL。
- 进一步阅读
NextAuth(config)、handlers.GET和 Session Cookie 的内部实现。