独立后端下NextJS认证:如何请求令牌保护的路由
我查了大量资料,网上相关指南大多只展示把令牌存入Cookie和会话刷新的示例,但我最关心(也最棘手)的是后续怎么向令牌保护的路由发起HTTP请求。
项目涉及主体
- 远程后端服务器(支持通过Authorization头或user_token Cookie认证)
- NextJS(服务端)
- NextJS(客户端)
期望认证流程
- 用户填写邮箱密码表单,通过Server Action从NextJS客户端发请求到NextJS服务端;
- NextJS服务端向远程后端发起请求,状态正常的话,通过
{ cookies } from 'next/headers'设置http-only的"user_token" Cookie并返回响应给客户端; - 之后要实现从NextJS服务端和客户端都能向后端的令牌保护路由发起请求。
问题解答
a) NextJS服务端fetch的credentials问题
NextJS服务端环境中,fetch的credentials: "include"不会自动携带通过cookies().set设置的Cookie——因为服务端的Cookie存储和浏览器的Cookie池完全隔离。你必须手动从cookies()中获取令牌,再通过请求头或Cookie字段传递给后端。
推荐方案(用Authorization头):
export const profileGet = async () => { const token = cookies().get("user_token")?.value; return fetch(`${CONFIG.BASE_URL}/profile`, { method: "GET", headers: { ...DEFAULT_HEADERS, Authorization: `Bearer ${token}` }, }); };
若需用Cookie传递:
export const profileGet = async () => { const token = cookies().get("user_token")?.value; return fetch(`${CONFIG.BASE_URL}/profile`, { method: "GET", headers: { ...DEFAULT_HEADERS, Cookie: `user_token=${token}` }, }); };
credentials: "include"在服务端fetch中仅会携带当前用户请求传入的Cookie(即浏览器发往NextJS服务端的Cookie),而非你通过cookies().set设置的Cookie,因此无法让它直接生效。
b) 生产环境同域下的客户端Cookie携带
如果NextJS与后端同域(如均为example.com),只要Cookie配置满足以下条件,客户端请求会自动携带user_token:
- Cookie的
domain设为.example.com(或不设置,默认绑定当前域名) path设为/sameSite设为lax或none(跨子域时none需配合secure: true)- 生产环境下
secure必须设为true(HTTPS环境才会携带Cookie)
你的storeToCookies函数中secure: false需要在生产环境改为true,否则浏览器不会在HTTPS请求中携带该Cookie。
c) 开发模式下跨域名Cookie共享
开发模式下NextJS运行在localhost:3000,后端在其他域名(如localhost:8000),默认Cookie无法跨域共享,有两种解决方案:
方案1:反向代理(推荐)
在next.config.js中配置反向代理,将后端请求映射到localhost:3000的子路径,使客户端请求与NextJS同域:
// next.config.js module.exports = { async rewrites() { return [ { source: '/api/:path*', destination: 'http://localhost:8000/:path*', // 替换为你的后端地址 }, ]; }, };
随后将客户端apisauce的baseURL改为/api,请求会自动代理到后端,Cookie也会被自动携带。
方案2:Cookie与CORS配置调整
若不想用代理,需修改Cookie配置并调整后端CORS:
- 更新
storeToCookies的Cookie参数:
cookies().set( key, typeof value === "string" ? value : JSON.stringify(value), { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'none', path: '/', domain: 'localhost', } );
- 后端CORS配置需设置:
Access-Control-Allow-Credentials: trueAccess-Control-Allow-Origin: http://localhost:3000(不能设为*)
修改后,开发模式下Cookie可同时作用于localhost:3000和后端域名。
代码示例
Login Server Action(简化代码)
export const loginAction = async (state: unknown, formData: unknown): Promise<LoginFormState> => { if (!(formData instanceof FormData)) return { message: "Invalid payload", ok: false }; // Validate form fields const validatedFields = LoginFormSchema.safeParse({ email: formData.get('email'), password: formData.get('password'), }) // If any form fields are invalid, return early if (!validatedFields.success) { console.log('validation error'); return { errors: validatedFields.error.flatten().fieldErrors, } as LoginFormState } const loginResponse = await loginPost({ ...validatedFields.data }); if (!classicNextApiResponseValidator(loginResponse)) { const error = await requestErrorFromNextResponse(loginResponse); return { message: error.description, ok: false } } const responseData: T.LoginResponse = await loginResponse.json(); await storeToCookies("user_token", responseData.accessToken); if (responseData.emailVerified) { await redirectAction("/dashboard", {}); } else { await redirectAction("/emailVerification", {}); } return { message: "successfully", ok: true } };
storeToCookies函数
"use server"; import { cookies } from 'next/headers'; import { CookiesKey } from './types'; export const storeToCookies = async <T = any>(key: CookiesKey, value: T, options: {} = {}) => { if (typeof key === "string") cookies().set( key, typeof value === "string" ? value : JSON.stringify(value), { httpOnly: true, secure: false, sameSite: 'lax', path: '/', } ); };
NextJS服务端请求受保护路由的尝试(credentials: "include"无效,手动传Cookie有效)
export const profileGet = async () => { return fetch(`${CONFIG.BASE_URL}/profile`, { method: "GET", credentials: "include", headers: DEFAULT_HEADERS, }); };
客户端请求的apisauce实例
const sauce = create({ withCredentials: true, baseURL: CONFIG.BASE_URL, headers: { Accept: 'application/json', } });
内容的提问来源于stack exchange,提问作者Evok

