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

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()无法生效,因为服务器组件的导航逻辑必须在组件渲染阶段同步执行。


正确解决方案架构

保持你当前的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 13:55:59