You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 14如何实现基于角色的路由保护及Refresh Token处理?

Next.js 14 基于角色的路由保护实现方案

一、基于角色的路由保护实现(SSR、CSR、Middleware)

1. Middleware 方式(全局拦截,推荐)

Middleware 是 Next.js 13+ 的全局路由拦截方案,能在请求到达页面之前完成权限校验,优先级最高,适合全局统一管控路由权限。

实现步骤与代码示例

在项目根目录创建 middleware.ts 文件:

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// 定义路由-角色映射规则
const ROLE_ROUTES = {
  '/admin': ['admin'],
  '/seller': ['seller'],
  '/buyer': ['buyer'],
  '/profile': ['admin', 'seller', 'buyer'] // 多角色可访问的公共路由
}

export async function middleware(request: NextRequest) {
  const accessToken = request.cookies.get('accessToken')?.value
  let userRole = null

  if (accessToken) {
    try {
      // 调用API验证token并获取用户角色
      const res = await fetch('http://localhost:5000/api/v1/verify-token', {
        headers: { Authorization: `Bearer ${accessToken}` },
        credentials: 'include'
      })

      if (res.ok) {
        const user = await res.json()
        userRole = user.role
      } else {
        // Token无效,清除Cookie并重定向登录页
        const response = NextResponse.redirect(new URL('/login', request.url))
        response.cookies.delete('accessToken')
        return response
      }
    } catch (err) {
      const response = NextResponse.redirect(new URL('/login', request.url))
      response.cookies.delete('accessToken')
      return response
    }
  }

  // 匹配当前路由对应的权限要求
  const currentPath = request.nextUrl.pathname
  const matchedRoute = Object.keys(ROLE_ROUTES).find(route => currentPath.startsWith(route))
  const requiredRoles = matchedRoute ? ROLE_ROUTES[matchedRoute] : null

  if (requiredRoles) {
    // 无角色或角色不匹配时,重定向到登录页
    if (!userRole || !requiredRoles.includes(userRole)) {
      return NextResponse.redirect(new URL('/login', request.url))
    }
  }

  return NextResponse.next()
}

// 指定需要拦截的路由范围
export const config = {
  matcher: ['/admin/:path*', '/seller/:path*', '/buyer/:path*', '/profile']
}

2. SSR 方式(服务器端渲染时校验)

在服务器端获取 Cookie 并完成权限校验,适合需要在渲染前获取用户数据的页面,支持 Pages Router 和 App Router。

App Router 代码示例

// app/admin/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

async function AdminPage() {
  const cookieStore = cookies()
  const accessToken = cookieStore.get('accessToken')?.value

  // 无Token直接跳转登录页
  if (!accessToken) redirect('/login')

  let userRole = null
  try {
    const res = await fetch('http://localhost:5000/api/v1/verify-token', {
      headers: { Authorization: `Bearer ${accessToken}` },
      credentials: 'include'
    })
    
    if (!res.ok) throw new Error('Token无效')
    const user = await res.json()
    userRole = user.role
  } catch (err) {
    redirect('/login')
  }

  // 角色不匹配跳转403页面
  if (userRole !== 'admin') redirect('/403')

  return (
    <div>
      <h1>Admin Dashboard</h1>
      {/* 页面内容 */}
    </div>
  )
}

export default AdminPage

3. CSR 方式(客户端渲染时校验)

在客户端组件中通过钩子函数完成权限校验,适合纯客户端交互的页面。

自定义权限钩子示例

// hooks/useAuth.ts
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'

export function useAuth(requiredRoles: string[]) {
  const [user, setUser] = useState<{ email: string; role: string } | null>(null)
  const [loading, setLoading] = useState(true)
  const router = useRouter()

  useEffect(() => {
    const checkAuth = async () => {
      // 从Cookie中提取accessToken
      const accessToken = document.cookie.split('; ')
        .find(row => row.startsWith('accessToken='))
        ?.split('=')[1]

      if (!accessToken) {
        router.push('/login')
        setLoading(false)
        return
      }

      try {
        const res = await fetch('http://localhost:5000/api/v1/verify-token', {
          headers: { Authorization: `Bearer ${accessToken}` },
          credentials: 'include'
        })

        if (res.ok) {
          const userData = await res.json()
          if (!requiredRoles.includes(userData.role)) {
            router.push('/403')
          } else {
            setUser(userData)
          }
        } else {
          router.push('/login')
        }
      } catch (err) {
        router.push('/login')
      } finally {
        setLoading(false)
      }
    }

    checkAuth()
  }, [router, requiredRoles])

  return { user, loading }
}

页面使用示例

// app/seller/page.tsx
'use client'

import { useAuth } from '@/hooks/useAuth'

function SellerPage() {
  const { user, loading } = useAuth(['seller'])

  if (loading) return <div>加载中...</div>

  return (
    <div>
      <h1>Seller Dashboard</h1>
      <p>欢迎,{user?.email}</p>
    </div>
  )
}

export default SellerPage

二、结合Refresh Token的路由保护与令牌刷新

1. 刷新时机

  • API请求返回401时:accessToken过期,先尝试刷新令牌,成功后重试原请求,失败则跳转登录页。
  • 页面初始化校验时:SSR/CSR权限校验失败(401)时,先执行令牌刷新逻辑,再重新校验。
  • 定时主动刷新:根据accessToken过期时间(如3分钟),在过期前30秒自动调用刷新接口(仅适合CSR场景)。

2. 实现方案

第一步:封装令牌刷新工具函数

// utils/auth.ts
export async function refreshAccessToken(): Promise<boolean> {
  try {
    const res = await fetch('http://localhost:5000/api/v1/refresh-token', {
      method: 'POST',
      credentials: 'include' // 携带存储refreshToken的Cookie
    })

    if (res.ok) {
      // 新的accessToken由API自动写入Cookie,无需手动处理
      return true
    } else {
      // refreshToken也过期,清除所有授权Cookie
      document.cookie = 'accessToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
      document.cookie = 'refreshToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'
      return false
    }
  } catch (err) {
    console.error('刷新令牌失败:', err)
    return false
  }
}

第二步:在Middleware中集成刷新逻辑

修改 middleware.ts 中的Token校验逻辑:

// 省略其他代码...
if (accessToken) {
  try {
    const res = await fetch('http://localhost:5000/api/v1/verify-token', {
      headers: { Authorization: `Bearer ${accessToken}` },
      credentials: 'include'
    })

    if (res.ok) {
      const user = await res.json()
      userRole = user.role
    } else if (res.status === 401) {
      // accessToken过期,尝试刷新令牌
      const refreshSuccess = await refreshAccessToken()
      if (refreshSuccess) {
        // 刷新成功后,重新获取新Token并校验
        const newAccessToken = request.cookies.get('accessToken')?.value
        if (newAccessToken) {
          const newRes = await fetch('http://localhost:5000/api/v1/verify-token', {
            headers: { Authorization: `Bearer ${newAccessToken}` },
            credentials: 'include'
          })
          if (newRes.ok) {
            const user = await newRes.json()
            userRole = user.role
          } else {
            throw new Error('新Token无效')
          }
        } else {
          throw new Error('获取新Token失败')
        }
      } else {
        throw new Error('刷新令牌失败')
      }
    }
  } catch (err) {
    const response = NextResponse.redirect(new URL('/login', request.url))
    response.cookies.delete('accessToken')
    response.cookies.delete('refreshToken')
    return response
  }
}
// 省略其他代码...

第三步:在CSR钩子中集成刷新逻辑

修改 useAuth 钩子:

// hooks/useAuth.ts
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { refreshAccessToken } from '@/utils/auth'

export function useAuth(requiredRoles: string[]) {
  const [user, setUser] = useState<{ email: string; role: string } | null>(null)
  const [loading, setLoading] = useState(true)
  const router = useRouter()

  useEffect(() => {
    const checkAuth = async () => {
      let accessToken = document.cookie.split('; ')
        .find(row => row.startsWith('accessToken='))
        ?.split('=')[1]

      if (!accessToken) {
        router.push('/login')
        setLoading(false)
        return
      }

      try {
        const res = await fetch('http://localhost:5000/api/v1/verify-token', {
          headers: { Authorization: `Bearer ${accessToken}` },
          credentials: 'include'
        })

        if (res.ok) {
          const userData = await res.json()
          if (!requiredRoles.includes(userData.role)) {
            router.push('/403')
          } else {
            setUser(userData)
          }
        } else if (res.status === 401) {
          // 尝试刷新令牌
          const refreshSuccess = await refreshAccessToken()
          if (refreshSuccess) {
            // 刷新成功后重新校验
            accessToken = document.cookie.split('; ')
              .find(row => row.startsWith('accessToken='))
              ?.split('=')[1]
            const newRes = await fetch('http://localhost:5000/api/v1/verify-token', {
              headers: { Authorization: `Bearer ${accessToken}` },
              credentials: 'include'
            })
            if (newRes.ok) {
              const userData = await newRes.json()
              if (!requiredRoles.includes(userData.role)) {
                router.push('/403')
              } else {
                setUser(userData)
              }
            } else {
              router.push('/login')
            }
          } else {
            router.push('/login')
          }
        } else {
          router.push('/login')
        }
      } catch (err) {
        router.push('/login')
      } finally {
        setLoading(false)
      }
    }

    checkAuth()
  }, [router, requiredRoles])

  return { user, loading }
}

3. 注意事项

  • 生产环境中,refreshToken的Cookie需设置 HttpOnly、Secure、SameSite: strict,防范XSS和CSRF攻击。
  • 刷新令牌接口需校验refreshToken的有效性,返回新的accessToken并更新对应Cookie。
  • 配置API的CORS规则,允许Next.js域名的请求携带Cookie。

内容的提问来源于stack exchange,提问作者Sakilahmed saju

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 22:14:55