《双Token机制?》Next.js 双 Token 登录与无感刷新实战教程

前言

在现代 Web 应用中,用户登录鉴权是前端开发必修课。尤其是在企业级项目中,我们常常面临这些问题:

  • 如何保证用户登录后页面长期保持登录状态?
  • accessToken 过期怎么办?刷新 token 怎么安全实现?
  • 如何保证安全性又兼顾用户体验?

今天,我将结合 Next.js + Prisma + JWT + 双 Token 的实战经验,详细讲解如何实现一个 完整的安全登录系统。文章会从数据库设计、token 生成、前端中间件、无感刷新逻辑到面试加分点,全方位拆解,让小白也能读懂。


一、双 Token 登录概念

双 Token 登录是一种现代 Web 应用常用方案,核心理念是:

Token 生命周期 存储位置 作用
Access Token 短期(10--30 分钟) HttpOnly Cookie 或请求 Header 用于接口鉴权,每次请求验证用户身份
Refresh Token 长期(7 天甚至更长) HttpOnly Cookie + 数据库 用于生成新的 accessToken,实现无感刷新

优势

  1. 安全性高:accessToken 过期快,即使被盗也影响有限
  2. 用户体验好:refreshToken 可实现无感刷新,用户无需频繁登录
  3. 可控性强:数据库存储 refreshToken,可随时注销或失效

二、Next.js 路由与 dashboard 目录

在 Next.js 中,每个目录对应路由,dashboard 目录常用于登录后可访问的后台页面集合

bash 复制代码
app/
├─ dashboard/
│  ├─ page.tsx        → /dashboard
│  ├─ settings/page.tsx → /dashboard/settings
│  └─ users/page.tsx  → /dashboard/users
├─ login/page.tsx      → /login
├─ layout.tsx

特点:

  • dashboard 下的页面都是 受保护路由
  • 可以在 dashboard/layout.tsx 统一布局(侧边栏、导航栏)
  • 中间件可以统一处理整个 dashboard 的鉴权逻辑

双 Token 体系结合数据库存储和中间件无感刷新,实现了安全、可控和高用户体验的登录方案:accessToken 短期保证安全,refreshToken 长期保证无感刷新,同时数据库存储 refreshToken 可以随时注销或轮换,支持多端管理

dashboard 目录的作用

在我们的双 Token 登录系统中,dashboard 目录扮演了核心角色,它是所有登录后可访问页面的集合。理解 dashboard 的作用,对实现安全、可控的登录体系非常重要。


受保护路由

  • dashboard 下的所有页面都是登录后才能访问的路由
  • 如果用户没有登录(accessToken 和 refreshToken 都无效),中间件会拦截请求,并重定向到登录页。
  • 举例目录结构:
bash 复制代码
app/
├─ dashboard/
│  ├─ page.tsx        → /dashboard
│  ├─ settings/page.tsx → /dashboard/settings
│  └─ users/page.tsx  → /dashboard/users

逻辑解析

  1. 用户请求 /dashboard 或子页面
  2. 中间件检查 accessToken 是否有效
  3. accessToken 有效 → 允许访问
  4. accessToken 无效但 refreshToken 有效 → 自动刷新 token,无感登录
  5. 两者都无效 → 重定向登录页

面试加分点:dashboard 目录就是"受保护路由集合",中间件统一管理鉴权,保证安全性和用户体验。


统一布局(layout.tsx)

在 Next.js 中,dashboard/layout.tsx 可以统一配置所有子页面的布局,比如:

  • 左侧导航栏(菜单)
  • 顶部导航(用户信息、登出按钮)
  • 内容区域(子页面渲染)

示例:

javascript 复制代码
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
    return (
        <div className="dashboard-layout">
            <aside className="sidebar">导航菜单</aside>
            <header className="header">用户信息 / 登出</header>
            <main className="content">{children}</main>
        </div>
    )
}

优势

  1. 所有 dashboard 子页面统一样式和布局
  2. 页面切换不会重复加载导航和侧边栏

3️⃣ 中间件统一鉴权

中间件可以针对整个 dashboard 目录进行统一鉴权:

csharp 复制代码
export async function middleware(request: NextRequest) {
    const accessToken = request.cookies.get('accessToken')?.value
    const refreshToken = request.cookies.get('refreshToken')?.value

    if (accessToken) {
        const payload = await verifyToken(accessToken)
        if (payload) {
            const headers = new Headers(request.headers)
            headers.set('x-user-id', payload.userId as string)
            return NextResponse.next({ request: { headers } })
        }
    }

    if (refreshToken) {
        const payload = await verifyToken(refreshToken)
        if (payload) {
            const refreshUrl = new URL('/api/auth/refresh', request.url)
            refreshUrl.searchParams.set('redirect', request.url)
            return NextResponse.redirect(refreshUrl)
        }
    }

    return NextResponse.redirect(new URL('/login', request.url))
}

解析

  1. 中间件在请求到达 dashboard 子页面前执行
  2. accessToken 有效 → 注入 x-user-id header → 页面和 API 可以直接使用用户信息
  3. accessToken 无效 + refreshToken 有效 → 重定向 /api/auth/refresh → 无感刷新
  4. 两者都无效 → 强制重定向登录页
  • 用中间件统一处理鉴权逻辑,避免每个页面重复代码
  • 将用户信息注入 header,提高下游 API 访问效率
  • 支持无感刷新,提高用户体验

三、Prisma 数据库设计

Prisma 用于管理用户和内容表:

kotlin 复制代码
model users {
  id           Int      @id @default(autoincrement())
  email        String   @unique
  password     String
  refreshToken String?
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
  posts        Post[]
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String
  published Boolean  @default(false)
  author    users    @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
}

关键点

  1. refreshToken 存数据库:保证登录态可控,支持登出和多端管理

  2. Prisma Migration 区分数据和结构

    • 修改表结构或新增字段 → 需要迁移(migration)
    • 插入或更新数据(如新增用户或刷新 token) → 不需要迁移
  3. password 使用 bcrypt 加密存储

refreshToken 存数据库:保证登录态可控,支持登出和多端管理

原理

  • refreshToken 是长期有效的 token,存储在浏览器 Cookie 中
  • 如果只存前端,服务端无法知道用户是否登出或者是否失效
  • 因此,我们在 Prisma users 表中存储 refreshToken
kotlin 复制代码
model users {
  id           Int      @id @default(autoincrement())
  email        String   @unique
  password     String
  refreshToken String?  // 存储 refreshToken
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
}

优势

  1. 支持单端登出

    • 用户在一台设备登出 → 清空数据库 refreshToken
    • 其他设备尝试刷新 accessToken 时会失败 → 强制重新登录
  2. 支持 token 轮换

    • 每次刷新 accessToken 时生成新的 refreshToken
    • 数据库存储最新的 refreshToken
    • 防止旧 token 被重放攻击
  3. 支持多端管理

    • 不同设备登录可以生成不同 refreshToken
    • 可以按设备控制是否失效

面试亮点:

  • "数据库存储 refreshToken 让服务端完全掌控登录状态,支持登出、轮换和多端管理。"

Prisma Migration 区分数据和结构

在开发中,有些操作会修改数据库内容,有些会修改数据库结构,需要区分:

结构变更(需要 Migration)

  • 新增字段,例如 users 表新增 lastLoginTime
  • 修改字段类型或约束
  • 新增或修改表

Prisma 命令

csharp 复制代码
npx prisma migrate dev --name add-lastLoginTime

生成 SQL 并更新数据库结构

数据操作(不需要 Migration)

  • 插入或更新数据,如:

    • 新用户注册
    • 登录后更新 refreshToken
    • 修改用户资料
css 复制代码
await prisma.users.update({
    where: { id: user.id },
    data: { refreshToken: newRefreshToken }
})

这些操作不会改变表结构,不需要迁移

小结

  • Migration = 改结构
  • 数据操作 = 改数据
  • 面试回答示例:

"Prisma 区分表结构和数据操作,只有结构变更才需要迁移,数据更新不需要迁移,这保证了开发效率和数据库安全。"


** password 使用 bcrypt 加密存储**

直接存明文密码是非常危险的,一旦数据库泄露,用户账号就会被盗用。

bcrypt 加密流程

  1. 注册时:
javascript 复制代码
import bcrypt from 'bcrypt'

const hashedPassword = await bcrypt.hash(password, 10)
await prisma.users.create({
    data: { email, password: hashedPassword }
})
  • 10 是 saltRounds,表示加密复杂度
  • 生成的 hashedPassword 是不可逆的
  1. 登录验证时:
vbnet 复制代码
const isPasswordValid = await bcrypt.compare(password, user.password)
if (!isPasswordValid) {
    return NextResponse.json({ error: '密码错误' }, { status: 401 })
}
  • compare 将用户输入密码和数据库加密密码比对
  • 即使数据库泄露,攻击者无法还原明文密码

优势

  • 安全:不可逆,加密算法抗暴力破解

  • 面试亮点

    • "使用 bcrypt 对密码加盐哈希存储,保障用户数据安全,是大厂必备做法"

四、bcrypt 密码加密

注册或登录时,使用 bcrypt 处理密码:

vbnet 复制代码
const isPasswordValid = await bcrypt.compare(password, user.password)
if (!isPasswordValid) {
    return NextResponse.json({ error: '密码错误,登录失败' }, { status: 401 })
}

解析:

  • bcrypt 是不可逆加密算法,保证数据库中不会存储明文密码
  • compare 用于验证用户输入密码是否匹配加密后的密码

面试回答:

"使用 bcrypt 对密码进行哈希,加密后的密码存储在数据库,防止明文泄露,保障安全。"


五、生成双 Token 与存储 refreshToken

登录成功后,生成 accessToken 和 refreshToken,并存储 refreshToken:

python 复制代码
const { accessToken, refreshToken } = await createToken(user.id)

await prisma.users.update({
    where: { id: user.id },
    data: { refreshToken }
})

setAuthCookies(accessToken, refreshToken)

解析

  1. createToken(user.id) → 返回两个 token

    • accessToken:短期有效

      • 用于 API 鉴权,通常有效期 10--30 分钟
      • 存储在前端 HttpOnly Cookie 或请求 Header 中
      • 过期后无法使用,保证安全性,即使被盗也影响有限
    • refreshToken:长期有效,存数据库

      • 用于生成新的 accessToken,实现无感刷新
      • 有效期通常 7 天或更长
      • 存数据库保证服务端可控,支持登出、轮换、多端管理

  1. prisma.users.update → 更新数据库中的 refreshToken

    • 每次用户登录或 refreshToken 刷新时,都更新数据库中的 refreshToken

    • 保证每次刷新都能轮换 token

      • 老的 refreshToken 立即失效
      • 防止重放攻击(Replay Attack)
      • 多端登录时可控制单端失效
    • 数据库存储的 refreshToken 与用户浏览器中的 refreshToken 一一对应,保证安全可控


  1. setAuthCookies → 存储 token 为 HttpOnly Cookie

    • 防 XSS(跨站脚本攻击)

      • HttpOnly Cookie 浏览器 JavaScript 无法访问
      • 防止前端脚本窃取 token
    • 配置 sameSite: strict 防 CSRF(跨站请求伪造)

      • 浏览器只在同源请求中发送 Cookie
      • 防止第三方网站伪造请求访问你的接口
    • 示例配置

    php 复制代码
    response.cookies.set('accessToken', accessToken, {
        httpOnly: true,
        maxAge: 60 * 15, // 15 分钟
        sameSite: 'strict',
        path: '/'
    })
    
    response.cookies.set('refreshToken', refreshToken, {
        httpOnly: true,
        maxAge: 60 * 60 * 24 * 7, // 7 天
        sameSite: 'strict',
        path: '/'
    })

总结:通过 createToken 生成双 token、更新数据库 refreshToken 并用 HttpOnly Cookie 存储,既保证了安全性,又实现了无感刷新,用户体验与安全性兼顾。


六、中间件鉴权 + 无感刷新

核心中间件逻辑:

csharp 复制代码
if (accessToken) {
    const accessPayload = await verifyToken(accessToken)
    if (accessPayload) {
        const requestHeaders = new Headers(request.headers)
        requestHeaders.set('x-user-id', accessPayload.userId as string)
        return NextResponse.next({ request: { headers: requestHeaders } })
    }
}

// accessToken 过期,但 refreshToken 有效
if (refreshToken) {
    const refreshPayload = await verifyToken(refreshToken)
    if (refreshPayload) {
        const refreshUrl = new URL('/api/auth/refresh', request.url)
        refreshUrl.searchParams.set('redirect', request.url)
        return NextResponse.redirect(refreshUrl)
    }
}

// 两个 token 都无效
return NextResponse.redirect(new URL('/login', request.url))

逐步解析

1️⃣ accessToken 有效

  • verifyToken(accessToken) → 解码 token

    • 验证 accessToken 是否过期
    • 解析出用户信息(例如 userId
  • 如果有效:

    • 克隆请求头new Headers(request.headers)

      • 原始 request.headers 是只读的,需要克隆才能修改
      • 克隆后可以安全地注入自定义 header
    • 添加 x-user-id → 下游 API 可直接读取

      • 让后端接口不必每次都解析 accessToken
      • 下游服务可以通过 x-user-id 知道当前操作的用户
    • NextResponse.next() → 请求继续执行

      • 页面正常访问,无需跳转或刷新
      • 提升用户体验

优势:下游无需重复解析 token,即可获取用户 ID,访问速度快,安全且高效


2️⃣ accessToken 无效,refreshToken 有效

  • 解码 refreshToken

    • 验证 refreshToken 是否有效
    • 确认用户身份信息
  • 如果有效:

    • 重定向到 /api/auth/refresh

      • 后端生成 新的 accessToken + refreshToken
      • 更新数据库中的 refreshToken,实现 token 轮换
    • 页面无感知 → "无感刷新"

      • 用户不会看到登录提示
      • 页面继续访问 dashboard 或其他受保护页面
      • 无感刷新提高用户体验

这就是双 Token 无感刷新核心逻辑

  • accessToken 失效 → refreshToken 自动刷新
  • 用户无感知
  • 保证安全性 + 用户体验兼顾

3️⃣ 两个 token 都无效

  • 重定向登录页 → 用户必须重新登录

    • 这是最后一道安全防线
    • 防止非法访问 dashboard 或 API
  • 保证安全性

    • accessToken 和 refreshToken 都无效,说明用户会话已经过期
    • 强制登录,确保系统不会被未授权访问

七、accessToken header 注入详解

php 复制代码
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-user-id', accessPayload.userId as string)
return NextResponse.next({ request: { headers: requestHeaders } })
  • 为什么要这么做?

    1. 原始 request.headers 是只读的,必须克隆
    2. 注入 x-user-id 方便后续 API 直接使用
    3. 安全:header 在服务端传递,前端无法篡改

面试回答:

"这种方式保证了线程安全,同时让下游请求无需解析 token 就能获取用户身份。"


八、有感刷新 VS 无感刷新

类型 用户体验 实现方式
有感刷新 用户看到登录提示,需要重新输入密码 token 过期 → 弹登录框或跳转登录页
无感刷新 用户无感知,页面一直可用 accessToken 过期 → refreshToken 自动刷新 → 页面继续访问
  • 无感刷新 是现代 SPA / Next.js 应用标准做法
  • 提升用户体验,同时保证安全性

九、总结

  1. 双 Token 机制:accessToken + refreshToken
  2. refreshToken 存数据库:支持失效、轮换和单端登出
  3. 中间件统一鉴权 :注入 x-user-id,实现无感刷新
  4. HttpOnly Cookie:防 XSS / CSRF
  5. Prisma Migration:表结构变更才需要 migration,数据操作无需迁移
  6. 流程清晰:注册 → 登录 → token 存储 → 鉴权 → 无感刷新 → 页面访问

面试高分回答:

"Next.js + Prisma + 双 Token 登录 + 中间件无感刷新,实现了安全、可控、用户无感知的登录体系,accessToken 短期有效保证安全,refreshToken 长期有效支持无感刷新,Prisma 数据库存储 refreshToken 支持多端管理。"


可视化流程

  1. 登录流程图

    • 用户输入账号密码 → 后端验证 → 返回 accessToken + refreshToken → 存 HttpOnly Cookie
  2. 访问 dashboard 流程图

    • 请求 dashboard → 中间件验证 accessToken
    • accessToken 有效 → 注入 x-user-id → 页面继续访问
    • accessToken 无效 + refreshToken 有效 → redirect /api/auth/refresh → 后端刷新 token → 页面无感刷新
    • 两者无效 → redirect /login
  3. Token 生命周期图

    • accessToken 短期,refreshToken 长期
    • 每次刷新轮换 refreshToken
    • 数据库存储保证可控

🔥 总结

通过这篇文章,你可以完整掌握:

  • Next.js 双 Token 登录体系
  • Prisma 数据库 + refreshToken 存储
  • bcrypt 密码加密
  • 中间件鉴权 + 无感刷新实现
  • 有感刷新 vs 无感刷新

一句话总结:
Next.js + Prisma + 双 Token + 中间件无感刷新 = 安全、可控、高体验的现代登录鉴权体系

相关推荐
该用户已不存在19 小时前
拒绝无效内卷,这 7 个 JavaScript 库让代码更能打
前端·javascript·后端
json{shen:"jing"}19 小时前
06_事件处理
前端·javascript·html
Awu122719 小时前
⚡全局自动化:我用Vite插件为所有CRUD组件一键添加指令
前端·vite·前端工程化
aircrushin19 小时前
Claude Code 新标准:三分钟了解什么是 Agent Skills?
前端·人工智能
Fzuim19 小时前
前端JS嵌入AI聊天
前端·ai
import_random19 小时前
[python]pyenv工具之shims
前端
树叶会结冰19 小时前
TypeScript---对象:不自在但实在
前端·javascript·typescript
风止何安啊19 小时前
一个切图仔的2025年度总结:AI 与 Vibe Coding 教会了大学生啥?
前端·人工智能·ai编程
怪可爱的地球人19 小时前
keep-alive缓存组件
前端