NextJS 14 SSR(Auth0认证)如何实现客户端直接携带JWT调用外部.NET API并处理令牌刷新
兄弟,我太懂你这种为了调用个API还要绕一圈服务端的麻烦了——不仅重复写代码,还多了一次网络跳转,完全是浪费精力!咱们直接把流程改成客户端直连你的.NET API,同时把token过期刷新的问题也给解决掉,一步步来:
第一步:修改Auth0回调逻辑,把Token存到安全Cookie里
默认情况下,@auth0/nextjs-auth0会把token存在自己的session里,客户端拿不到也没法直接用。所以咱们要在Auth0的回调完成后,把access_token和refresh_token写到HttpOnly、Secure的Cookie里——这种Cookie只有浏览器能自动携带,JS读不到,安全性有保障。
打开你的app/api/auth/[auth0]/route.ts,修改handleAuth的回调逻辑:
import { handleAuth, handleCallback } from '@auth0/nextjs-auth0'; export const GET = handleAuth({ async callback(req, res) { try { await handleCallback(req, res, { async afterCallback(req, res, session, state) { // 计算access_token的剩余有效期,转成秒 const accessTokenMaxAge = Math.floor((session.accessTokenExpiresAt - Date.now()) / 1000); // 把token写到Cookie里,配置安全属性 res.setHeader('Set-Cookie', [ `access_token=${session.accessToken}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${accessTokenMaxAge}`, `refresh_token=${session.refreshToken}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=2592000` // 30天,和Auth0配置保持一致 ]); return session; } }); } catch (error) { res.status((error as any).status || 500).end((error as Error).message); } } });
第二步:给外部.NET API配置跨域(CORS)
因为你的NextJS客户端和.NET API大概率不是同一个域名,所以得让API允许来自NextJS域名的请求,并且允许携带Cookie凭证。打开.NET项目的Program.cs,添加CORS配置:
builder.Services.AddCors(options => { options.AddPolicy("AllowNextJSClients", policy => { // 替换成你的NextJS域名,比如"https://your-nextjs-app.com" policy.WithOrigins("https://your-nextjs-domain.com") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // 关键:允许携带Cookie凭证 }); }); // 注意要放在UseRouting之后,UseAuthorization之前 app.UseCors("AllowNextJSClients");
第三步:让.NET API从Cookie里读取JWT验证
默认.NET的JWT认证是从Authorization头拿token,现在咱们要改成从Cookie里读access_token,调整认证中间件的配置:
using Microsoft.AspNetCore.Authentication.JwtBearer; builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "https://your-auth0-domain.com"; // 替换成你的Auth0域名 options.Audience = "your-api-audience"; // 替换成你的API在Auth0里配置的Audience // 从Cookie里获取JWT token options.Events = new JwtBearerEvents { OnMessageReceived = context => { context.Token = context.Request.Cookies["access_token"]; return Task.CompletedTask; } }; });
第四步:实现NextJS的Token刷新接口
当access_token过期后,咱们要用refresh_token去Auth0换个新的。写个专门的API路由来干这个活,客户端不用直接和Auth0打交道,安全性更高。新建app/api/refresh-token/route.ts:
import { getSession, updateSession } from '@auth0/nextjs-auth0'; import { NextResponse } from 'next/server'; export const POST = async (req: Request) => { const session = await getSession(); if (!session || !session.refreshToken) { return NextResponse.json({ error: '没有可用的刷新令牌' }, { status: 401 }); } try { // 调用Auth0的令牌刷新接口 const auth0Response = await fetch(`https://${process.env.AUTH0_DOMAIN}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ grant_type: 'refresh_token', client_id: process.env.AUTH0_CLIENT_ID, client_secret: process.env.AUTH0_CLIENT_SECRET, refresh_token: session.refreshToken, audience: process.env.AUTH0_AUDIENCE // 你的API Audience }) }); const tokenData = await auth0Response.json(); if (!auth0Response.ok) throw new Error(tokenData.error || '刷新令牌失败'); // 更新session里的token信息 session.accessToken = tokenData.access_token; session.accessTokenExpiresAt = Date.now() + (tokenData.expires_in * 1000); // 如果Auth0返回了新的refresh_token,也同步更新 if (tokenData.refresh_token) session.refreshToken = tokenData.refresh_token; await updateSession(req, session); // 把新的access_token写到Cookie里 const newAccessTokenCookie = `access_token=${tokenData.access_token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=${tokenData.expires_in}`; return new NextResponse(JSON.stringify({ success: true }), { headers: { 'Set-Cookie': newAccessTokenCookie } }); } catch (error) { return NextResponse.json({ error: (error as Error).message }, { status: 500 }); } };
第五步:客户端封装Fetch函数,自动处理Token过期
客户端调用API的时候,咱们封装一个fetch函数,正常发请求,如果返回401(token过期),就先调用刷新接口拿新token,然后再重试原请求。注意要加上credentials: 'include',让浏览器自动把Cookie带过去:
// 客户端的工具函数,比如utils/api-fetch.ts async function apiFetch(url: string, options: RequestInit = {}) { // 第一次请求API const response = await fetch(url, { ...options, credentials: 'include' // 关键:让浏览器自动携带Cookie }); // 如果返回401,尝试刷新token并重试 if (response.status === 401) { const refreshResponse = await fetch('/api/refresh-token', { method: 'POST', credentials: 'include' }); if (refreshResponse.ok) { // 刷新成功,重试原请求 return await fetch(url, { ...options, credentials: 'include' }); } else { // 刷新失败,跳转到登录页重新登录 window.location.href = '/api/auth/login'; return Promise.reject(new Error('刷新令牌失败,请重新登录')); } } return response; } // 用法示例:调用你的.NET API async function getProtectedData() { const response = await apiFetch('https://your-dotnet-api.com/protected-endpoint'); if (!response.ok) throw new Error('请求失败'); return await response.json(); }
一些关键注意事项
- 域名与Cookie配置:如果NextJS和API是不同子域(比如
app.yourdomain.com和api.yourdomain.com),Cookie的SameSite要设为Lax,并且要加上Domain=.yourdomain.com(注意前面的点),这样两个子域都能拿到Cookie;如果是完全不同的域名,跨域Cookie的限制会比较多,这时候可能得考虑用Bearer Token的方式(但要注意XSS风险,得加其他安全措施) - 安全第一:生产环境一定要开HTTPS,否则
SecureCookie不会生效;HttpOnlyCookie必须开启,防止XSS攻击偷token - Auth0控制台配置:要在Auth0控制台给你的应用开启Offline Access权限(在应用的Settings -> Advanced -> Grant Types里勾选Refresh Token),并且设置好Refresh Token的有效期
备注:内容来源于stack exchange,提问作者raezer

