Next.js独立后端JWT认证求助:401拦截器Cookie权限报错
Next.js 对接独立后端 JWT 认证的正确方案及 401 处理问题解决
核心问题诊断
你遇到的Cookies can only be modified in a Server Action or Route Handler错误,本质是:
- 你的
deleteSession()函数依赖next/headers的cookies()API,只能在**服务器端环境(Server Action/Route Handler/服务器组件)**运行 - Axios拦截器在客户端组件或服务器组件的异步请求中运行时,若处于客户端上下文,直接调用
deleteSession()会触发环境不匹配的错误
另外,服务器组件中直接调用Axios时,redirect()无法生效,因为服务器组件的导航逻辑必须在组件渲染阶段同步执行。
正确解决方案架构
1. 统一用 HttpOnly Cookie 存储 Session
保持你当前的Session管理逻辑(createSession/getSession/deleteSession),因为HttpOnly Cookie能避免XSS风险,且天然支持服务器组件直接读取,这是Next.js中最安全的会话存储方案。
2. 用 Route Handler 做 API 代理
将所有后端请求通过Next.js的Route Handler转发,这样所有认证逻辑(令牌携带、401处理、令牌刷新)都在服务器端执行,能安全操作Cookie。
创建app/api/proxy/[...path]/route.ts:
import { NextRequest, NextResponse } from "next/server"; import { getSession, deleteSession, createSession } from "@/lib/session"; // 处理GET请求 export async function GET(req: NextRequest) { const session = await getSession(); if (!session) { return NextResponse.redirect(new URL("/auth/signin", req.url)); } // 构造目标后端URL const path = req.nextUrl.pathname.replace("/api/proxy", ""); const targetUrl = `http://localhost:8000/api${path}${req.nextUrl.search}`; try { // 携带accessToken请求后端 const response = await fetch(targetUrl, { headers: { Authorization: `Bearer ${session.accessToken}`, "Content-Type": "application/json", }, }); // 处理401状态码 if (response.status === 401) { // 尝试用refreshToken刷新令牌 const refreshRes = await fetch("http://localhost:8000/api/auth/refresh", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken: session.refreshToken }), }); if (refreshRes.ok) { const { accessToken, refreshToken: newRefreshToken } = await refreshRes.json(); // 更新Session Cookie await createSession({ ...session, accessToken, refreshToken: newRefreshToken, }); // 重试原请求 const retryRes = await fetch(targetUrl, { headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, }); return new NextResponse(retryRes.body, { status: retryRes.status, headers: retryRes.headers, }); } else { // 刷新失败,清除Session并跳转登录页 await deleteSession(); return NextResponse.redirect(new URL("/auth/signin", req.url)); } } // 正常返回后端响应 return new NextResponse(response.body, { status: response.status, headers: response.headers, }); } catch (error) { return NextResponse.json( { success: false, message: "请求失败" }, { status: 500 } ); } } // 同理实现POST/PUT/DELETE等请求方法 export async function POST(req: NextRequest) { // 逻辑与GET类似,需转发请求体 const session = await getSession(); if (!session) { return NextResponse.redirect(new URL("/auth/signin", req.url)); } const path = req.nextUrl.pathname.replace("/api/proxy", ""); const targetUrl = `http://localhost:8000/api${path}`; const body = await req.json(); try { const response = await fetch(targetUrl, { method: "POST", headers: { Authorization: `Bearer ${session.accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); // 同样处理401和刷新逻辑... return new NextResponse(response.body, { status: response.status, headers: response.headers, }); } catch (error) { return NextResponse.json({ success: false, message: "请求失败" }, { status: 500 }); } }
3. 重构Axios配置(仅用于客户端组件)
客户端组件不再直接请求后端,而是请求上述代理接口,同时通过Server Action处理Cookie修改:
import axios from "axios"; import { ApiError } from "./type"; import { deleteSession } from "./session"; import { useRouter } from "next/navigation"; export const API = axios.create({ baseURL: `/api/proxy`, headers: { "Content-Type": "application/json", Accept: "application/json", }, }); API.interceptors.response.use( (response) => response, async (error) => { if (error.response?.status === 401) { // 调用Server Action删除Session await deleteSession(); // 客户端导航跳转登录页 const router = useRouter(); router.push("/auth/signin"); } if (axios.isAxiosError(error) && error.response?.data) { const apiError: ApiError = error.response.data; return Promise.reject(apiError); } return Promise.reject({ success: false, code: error.response?.status || 500, message: error.message || "未知错误", }); } );
4. 服务器组件的数据获取方式
服务器组件直接使用fetch请求代理接口,并在渲染阶段处理认证逻辑:
import { getSession, deleteSession } from "@/lib/session"; import { redirect } from "next/navigation"; export default async function ProfilePage() { const session = await getSession(); if (!session) redirect("/auth/signin"); try { const response = await fetch(`/api/proxy/me`); if (response.status === 401) { await deleteSession(); redirect("/auth/signin"); } const data = await response.json(); return <div>{JSON.stringify(data, null, 2)}</div>; } catch (error) { return <div>加载个人信息失败</div>; } }
关键注意事项
- 禁止在客户端存储敏感令牌:accessToken和refreshToken必须存在HttpOnly Cookie中,客户端不能直接读取
- 服务器组件优先用fetch:服务器组件中尽量避免使用Axios,直接用
fetch能更好地与Next.js的服务器端渲染逻辑兼容 - 令牌刷新逻辑放在服务器端:避免客户端接触refreshToken,降低泄露风险
内容的提问来源于stack exchange,提问作者Abdulrahman
相关产品推荐
相关产品推荐

