my-first-ai-web_问题记录04——从一次 GitHub 登录,看懂 NextAuth、OAuth 与 Next.js Middleware

6 阅读9分钟

跟着双越老师的划水AI项目学习记录,从Vue转向React+Next.js的踩坑经验分享。

前言

本文记录我在学习 day007 - 使用 NextAuth 实现 Github 登录一节中, Next.js 15 与 NextAuth v5(Auth.js)中接入 GitHub 登录的完整过程。重点是理解一次登录背后,浏览器、Next.js、NextAuth 和 GitHub 如何协作。

一、这次实践涉及哪些技术

一个看似简单的“使用 GitHub 登录”,实际上串起了多个前后端知识点:

  1. Next.js App Router:通过文件和目录建立 URL 路由。
  2. Route Handler:使用 route.ts 处理 GETPOST 等 HTTP 请求。
  3. React Server Component:在服务端组件中调用 auth() 获取当前登录状态。
  4. Server Action:通过 <form action> 在服务端调用 signIn()signOut()
  5. Next.js Middleware:在页面渲染前检查用户是否有权访问目标路径。
  6. OAuth 2.0 授权码流程:在应用、浏览器和 GitHub 之间完成授权与 Token 交换。
  7. 认证与授权:先确认“用户是谁”,再判断“用户能访问什么”。
  8. Cookie 与 Session:在后续请求中维持用户的登录状态。
  9. HTTP 重定向:通过 302303Location 在多个地址之间跳转。
  10. 服务端网络与代理:浏览器能访问 GitHub,不代表 Node.js 服务端也能访问。
  11. 约定优于配置:Next.js 根据特殊文件名和导出名称自动连接框架能力。
  12. 第三方能力集成:将高风险、复杂的认证流程交给成熟认证库处理。

二、项目中的认证文件

最终涉及以下文件:

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 负责决定何时调用 GETPOST、页面组件和 middleware。
  • NextAuth 负责解析认证 action、交换 Token、读取用户资料和管理 Session。

封装虽然减少了业务代码,但学习时不能只记住两行配置。只有展开浏览器、Next.js、NextAuth、GitHub 和 Cookie 之间的完整链路,遇到问题时才能判断故障究竟位于路由、配置、授权、网络、Token 交换还是 Session 阶段。

十二、本节结论

完成 GitHub 登录后,我不只是获得了一个登录按钮,还第一次完整接触了:

文件系统路由
→ Route Handler
→ Server Action
→ OAuth 授权码
→ 服务端 Token 交换
→ Session Cookie
→ 服务端重新渲染
→ middleware 页面保护
→ authorized 授权判断

后续仍需要继续解决:

  1. 将登录用户与数据库中的业务数据关联。
  2. 在服务端校验文档所有权,防止越权访问。
  3. 为登录失败、网络超时和权限拒绝提供清晰提示。
  4. 配置生产环境域名、HTTPS 和 GitHub OAuth callback URL。
  5. 进一步阅读 NextAuth(config)handlers.GET 和 Session Cookie 的内部实现。