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

Next-auth令牌编码问题导致API请求返回401错误

Next-auth调用API返回401错误,令牌编码问题排查

我搭建了一套基本可用的Next-auth认证系统,配置文件[...nextauth]如下所示。目前核心问题是调用API时返回401错误,但其他客户端用相同凭证能正常访问该API,推测问题出在Cookie中令牌的编码方式上。我之前的项目从来不需要自定义编解码,本来以为把access token存入httpOnly+Secure Cookie后,浏览器会自动携带,但显然漏了某些步骤。

有没有人遇到过类似问题?

[...nextauth]代码

import { clients } from "@/lib/axios";
import { env } from "@/utils/environmentService";
import NextAuth, { NextAuthOptions } from "next-auth";
import { JWT } from "next-auth/jwt";
import querystring from "query-string";

// For more information on each option (and a full list of options) go to
// https://next-auth.js.org/configuration/options
export const authOptions: NextAuthOptions = {
  // https://next-auth.js.org/configuration/providers/oauth
  providers: [
    {
      id: "oidc",
      name: "OIDC",
      type: "oauth",
      wellKnown: `${env.clientUrls.authServer()}/.well-known/openid-configuration`,
      authorization: {
        params: { scope: "openid email profile recipe_management" },
      },
      idToken: true,
      checks: ["pkce", "state"],
      clientId: "recipe_management.next",
      clientSecret: env.auth.secret,
      profile(profile) {
        return {
          id: profile.sub,
          name: profile.name,
          email: profile.email,
          image: profile.picture,
        };
      },
    },
  ],
  events: {
    async signOut({ token }) {
      var refreshToken = token.refreshToken;
      let headers = { "Content-Type": "application/x-www-form-urlencoded" };
      try {
        await clients.authServer.post(
          `/protocol/openid-connect/logout`,
          querystring.stringify({
            refresh_token: refreshToken,
            client_secret: env.auth.secret,
            client_id: "recipe_management.next",
          }),
          { headers }
        );
      } catch (e) {}
    },
  },
  callbacks: {
    async jwt({ token, user, account }) {
      // Initial sign in
      if (account && user) {
        return {
          accessToken: account.access_token,
          accessTokenExpires: Date.now() + account.expires_at * 1000,
          refreshToken: account.refresh_token,
          user,
        };
      }

      // Return previous token if the access token has not expired yet
      if (Date.now() < token.accessTokenExpires) {
        return token;
      }

      // Access token has expired, try to update it
      return refreshAccessToken(token);
    },
    async session({ session, token }) {
      session.user = token.user;
      session.accessToken = token.accessToken;
      session.error = token.error;

      return session;
    },
  },
  cookies: {
    sessionToken: {
      name: `__Secure-next-auth.session-token`,
      options: {
        httpOnly: true,
        sameSite: env.isDevelopment ? "lax" : "strict",
        path: "/",
        secure: true,
      },
    },
    callbackUrl: {
      name: `__Secure-next-auth.callback-url`,
      options: {
        sameSite: env.isDevelopment ? "lax" : "strict",
        path: "/",
        secure: true,
      },
    },
    csrfToken: {
      name: `__Host-next-auth.csrf-token`,
      options: {
        httpOnly: true,
        sameSite: env.isDevelopment ? "lax" : "strict",
        path: "/",
        secure: true,
      },
    },
  },
};

/**
 * Takes a token, and returns a new token with updated
 * `accessToken` and `accessTokenExpires`. If an error occurs,
 * returns the old token and an error property
 */
async function refreshAccessToken(token: JWT) {
  try {
    var params = querystring.stringify({
      client_secret: env.auth.secret,
      client_id: "recipe_management.next",
      grant_type: "refresh_token",
      refresh_token: token.refreshToken,
    });
    const url =
      env.clientUrls.authServer() + "/protocol/openid-connect/token?" + params;

    const response = await fetch(url, {
      headers: {
        "Content-Type": "application/x-www-form-urlencoded",
      },
      method: "POST",
    });

    const refreshedTokens = await response.json();

    if (!response.ok) {
      throw refreshedTokens;
    }

    return {
      ...token,
      accessToken: refreshedTokens.access_token,
      accessTokenExpires: Date.now() + refreshedTokens.expires_at * 1000,
      refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
    };
  } catch (error) {
    console.log(error);

    return {
      ...token,
      error: "RefreshAccessTokenError",
    };
  }
}

export default NextAuth(authOptions);

令牌输出示例

__Secure-next-auth.session-token

eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..JxmjRnzY6T7euTwe.0I2zku0JKJcWi_pY_ryewr2xEBgGXjDxh82VTB7k9Ed5uKtBQA-c7KIl6ShwHyWMxTMSIqzxZhsDFihuUULNB4uQUH2ic0nrWV8R_BLfx9xP92xHe7RW_OwtWQyvVAaeWfysAbWyPW167nVv2VL_mX3ubJRi09jEPA9KIm6S9EJQlM7VU9U7mLJ2VQUa7X9kGRH5xmZ05zexNg-MVPe8anMZOjpsedYYrSpfojCrpBQsCzzROkbY6ig1P2s1dBRIQA1juWG8CidVjN_fFeqJC19DyNJ8XAzGWX5xD317MRAlWHIl_Ehi6eYbjbiHFou5mBXSwTf62FT2aGqNUvAqlTi_em6jQ8RaKfnqZ4rBhfak3W5_QdULfUmqL7LQVc3X9ldVsN5M-h2ETemIMeWuKHL0iUBL2TKapPGaaskuYkGDN_LO5jYkAGyT0-QQRAsskzazpbz9_a6NiWnALFUxxTJLkF0LX6ItLUMQYqV8ywqMbJzFugbyzbYDXuPBfizeo2QYWXv9rs3cjXH7RY-7psCQQQhsvu2zJL1Jp465s-bMnCabxltoqAEdW0gexX3OISJgozxOLIvyDBDTcWKDyFUEAaZ9hld4CgpoV1Pk6jarL5-gjSbmDvzuQkc9vKltW9dOtDiubdFzv5bK_MPvOVpe5HOeEyGo8giyUeKX-AhFCFgvvUWS5RUaW-oiqfrX-E1OZqttO8u1tR4JYUSqLe48Z7EQGtWYDwyk6foLXdpYnQdfPox_lbeEKxpb1-6X_dg2Bm7RgCbN4XyTdmeLnXWC6FdY10AAxvS_iRC9pXcpp-lPLdfY6UuhkmNwxynYagJP6QGD4A-N96a14Ly6GvPxwA7_DcRN0KZ0f2fz0nqI_KF4EZGjKhLr2uEnkPoNtiYO4cCTwG_n4ulSRBnBuisMGbCfd11617hPuRv7nSadQmOwisSIOesJtpMHlQ4HsBCVHf36JEFsdSzGPPZgjRrucfl1eiWANbfTH9hnj7_MfHxBpk3royG3T6BzXBlyy65da-4iZBryw8hDglhsSoWa1Ihbxrm__E2Eob2XZxA2LlSHNqeq-6M6SK1OoEL6ZaLEoq082T-ENVyIj2ZnsqjbqLJUFA5byBDjvTRT01UEfTG2GlXSV2Y2OLOaWCj9WBMe4ejTFncbjd4HC3rySTKl71kYsE_SEYfRg0QgAszr7fdhxOvyjgJ27Xc3B7wGVoSjpTlQLcMGpMZkBs71fo7bkqbw_lxwSQ03ajIWBj2r7wrSIYolK8ZgH1-6leT7qzLve_c5N1jOL5Nc7bOKlLEkiJnwj9euHx-ubPlC31bjkejI85NjwKKsBllLoRUs5nruOrfjg20tN0s_mQIgvB7L4K-T8sVS4bGpsis_z3X00MaMyfZV8nMQDAAT0foJ3E9FmLK-bTsFOulXWiF9RKQNdb703Jk2VMrEE5lgVGWwmo_U2mLHhbhMnY_qjOHOs0UH2oeY43BujwLplct-t8gV8Tmnt4gxKjdZSZI9bBTP5p9i31hSwEt4zTgMGuiuk4kuupZP2b-3vp7Q0wpqtG95XoA_z04XfDK5jDXVT1Fq7VtH2HeACqhIPKuqcMYMP0NZ5MK3V-0ZhgyHuh8nNF_jeayI9ES2blJcNqAU9eP-dMhNf1rhTNHhNu5SCNKrv-KDoDPix1JP_8NSVt9TeLzMrN0HMgtTijhLCpYtzndCaQfdf9WlC93N2hxhLY44YwAIC24JZcrRlgf3DdHHwn6h1be3UlsYKRlnV0fQfevtY43Xg_pfNkZT0kHB1rNkA8CiJ_9NzanqdbyrAZJvhK2eCM4rPQ07wVQmtb2OQXeLdoQaqKZZxhpgJ8SpQcD06BAuo45747oSsu2c4QnnTlNAkDTcFb1izYUA-e8Lvu1Y_D16EpNb0R7qkxd4-DZE9BB-8N8gHN_r0HRl-jW2oJQL2ME5N9D3xSJFsMns3kkCRxA_Cv543zvoBHpdec4NgUxE8THp2okji8enDKeXQ5CUX8qqAmtWXLs339A5Cv8QTwBc154ZVGtjU-AZJSKz_4lu3MqQ1gkjDu8U0fKa9bu6rxdKyzq9nAvHCc7GmZeVM2CgoMI68oDGHWnRFDRlZe6l1DAeM5RHds3kJkeLRQ166hFdIs_Ivf07lZXLNzLidnV2qjLPLHN3Jh95wIU3fnkg54Xp5dmEfke8vJm9-d0LAFFxBgtBGLpgixzmOfFwTRbmpTUlVQ5gnpnmMjt-Ec4DaB2blHX4LE7RzNzbui-mWnEeQT7T3FnzUzraCJr7vkdm5yEXjY6B6vJslIeDPGf_LlGxhlPibpt07i140-EX6Et_z-DLYGD7v_kGOVaeMCtzWlxqIzGKlkqtS7nw5nZnaKDKGDH1m68ZO_sF-WzZGaPuMcHw4_kxi3dIVPMP8pDwKwEm9EaSCZaQWJRadyz3DaxLaYrtF-2XrEa8mPuzi1arpN1kgKJf-kj1d2X8EgKru8orssl3XaWyz4lYZziuNCe0nqxHr8aHqoNtOB3K_AauvzvljIzcPwNxnvXuP6KThE5S4TVxuZz1TKChiGRd8FF1A0fHstE2uNryXiwKgyEjqaCD59DUlq3y__C3FoUbYjDj5XcCdnw4sR_6PnUA__SaAO1Iet7RxpQLjCJyWkD_aLVS-bcvF3lmTf0wQDIDyrrR5bDv7vb8wmAXufUOLSHZ2r7P2lEfROijmEMdHmYXxHTsbYVs2s9ukP3QKCTvCkUQFtCPoflbbSL08HKvPFtnAmNwdRZK69wBlrxjsVqbAcpn75A9qXSn-YMbl6TvH6BXnCby_c3Pc7iylWmqVfTz8JxFhgTNtrdicz4TIZJi23j_qTB_fnXlOYOcSoA6BISSutCLzDy2HjD6JdHgaN3wbOfo2Nl6LdgfEIDw96ZiW8M7wg7tESY_MexR9-_rgcRvyiVHCKkPCEv3-wCQWISvkzKBGFpzUYnUuWP6VItKCrzK20sTyShdJZuhSUKw2Jr_xLDPv4TTIGzb29sreuoK3OFqhl9t_8Q1MpCNr3XGQHPHOY8QBjvYrfxf1sMFM_jfRQdTH6eC6iv-LbooriZHKwXSEAVifXn2EQdCksvv5gLP.BZmHIyTuTpe3L2MkSlWaDA

__Host-next-auth.csrf-token

aebde1a59a6e021acf44aa36ea973083fbe652d9a2272dbd46282c55931e3721%7C7177a554e51e04402a910090162398b6553dd764248e4e1852de152a15769bf3

__Secure-next-auth.callback-url

http%3A%2F%2Flocalhost%3A8582%2Frecipes

问题排查与解决建议

  • 核心误解:session token不是原始access token:你看到的__Secure-next-auth.session-token是Next-auth加密后的完整JWT,里面包含了原始access token,但本身不是API服务需要的令牌。直接用这个Cookie请求API,服务端无法识别,自然返回401。
  • 正确的API请求方式:
    1. 通过useSession钩子或者getServerSession方法获取Next-auth的session对象
    2. 从session中取出accessToken字段(也就是你在session回调里挂载的原始令牌)
    3. 在API请求的Authorization头中添加Bearer ${accessToken}
  • Cookie跨域问题:如果API和前端不在同一域名下,浏览器不会自动携带Cookie。这种情况要么调整域名配置为同域,要么坚持用Authorization头传递令牌。
  • 验证令牌有效性:可以用JWT解码工具解析__Secure-next-auth.session-token,确认里面的accessToken是否正确、是否过期,排除令牌本身的问题。

内容的提问来源于stack exchange,提问作者Paul DeVito

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.21 19:45:43