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请求方式:
- 通过
useSession钩子或者getServerSession方法获取Next-auth的session对象 - 从session中取出
accessToken字段(也就是你在session回调里挂载的原始令牌) - 在API请求的
Authorization头中添加Bearer ${accessToken}
- 通过
- Cookie跨域问题:如果API和前端不在同一域名下,浏览器不会自动携带Cookie。这种情况要么调整域名配置为同域,要么坚持用Authorization头传递令牌。
- 验证令牌有效性:可以用JWT解码工具解析
__Secure-next-auth.session-token,确认里面的accessToken是否正确、是否过期,排除令牌本身的问题。
内容的提问来源于stack exchange,提问作者Paul DeVito
相关产品推荐
相关产品推荐

