Next.js中间件无法检测Spring Boot后端设置的JWT Cookie问题
项目背景
- 架构:Next.js前端(部署于Vercel) + Spring Boot后端(部署于Azure容器),使用axios处理登录请求
- 前端路由:
- 首页(/)
- 内容页(/contents,无需认证)
- 管理员创建内容页(/tech-tide/create-new-content,需认证)
- 管理员登录页(/tech-tide)
- Cookie配置:已设置
SameSite=None、HttpOnly=true、Secure=true,指定domain为后端Azure容器域名 - 异常现象:
- 登录请求返回200,浏览器已存储JWT Cookie,但登录页未跳转至受保护路由,停留在登录页
- 刷新登录页时Cookie消失,访问/contents页时Cookie可见
- 本地运行(前后端均本地/前端本地+后端Azure)正常,Vercel前端连接Azure后端时出现问题
相关配置代码
Spring Boot后端Cookie设置函数
public static void addAuthCookieToResponse(HttpServletResponse response, String token) { ResponseCookie cookie = ResponseCookie.from("tech-tide-auth-cookie", token) .httpOnly(true) .secure(true) .sameSite("None") .path("/") .domain("techtideapi.whitestone-5ff89f6f.northcentralus.azurecontainerapps.io") .maxAge(Duration.ofDays(7)) .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); }
Spring Boot CORS配置类
package com.justme8code.techtide.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import java.util.List; @Configuration public class AppCorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); // Allow credentials (cookies, tokens) config.setAllowedOrigins(List.of("https://techtide.vercel.app","http://localhost:3000")); // Allowed origins config.setAllowedHeaders(List.of( "Authorization", // Allow Authorization header "Content-Type", // Allow Content-Type header "Accept" // Allow Accept header )); // Explicitly specify allowed headers config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); // HTTP Methods allowed UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); // Apply CORS configuration to all paths return new CorsFilter(source); } }
前端Axios配置
import axios from 'axios'; import {TECH_TIDE_BASE_URL} from "@/app/api_urls"; export const axiosInstance = axios.create({ baseURL: `${TECH_TIDE_BASE_URL}`, withCredentials: true, headers : { "Content-Type": "application/json", } });
Next.js中间件代码
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; const COOKIE_NAME = 'tech-tide-auth-cookie'; export function middleware(request: NextRequest) { const jwt = request.cookies.get(COOKIE_NAME)?.value; const path = request.nextUrl.pathname; console.log('Middleware - Path:', path); console.log('Middleware - JWT:', jwt); console.log('Middleware - All Cookies:', request.cookies.getAll()); if (path === '/tech-tider/create-new-content' && !jwt) { console.log('Middleware - Redirecting to /tech-tider'); return NextResponse.redirect(new URL('/tech-tider', request.url)); } console.log('Middleware - Allowing request to continue'); return NextResponse.next(); } export const config = { matcher: '/tech-tider/create-new-content', };
登录按钮触发函数
async function handleSubmit(previousState: unknown, formData: FormData) { const username = formData.get('username') as string; const password = formData.get('password') as string; if (username === "" || password === "") { return { error: "Username or password is required" }; } try { const response = await axiosInstance.post(TECH_TIDE_AUTH_URL, formData, { headers: { "Content-Type": "multipart/form-data" }, withCredentials: true // Crucial for cookie handling }); // Check for successful login based on your backend's response structure if (response.status === 200) { // Redirect to the protected route router.replace("/tech-tider/create-new-content"); return null; } // Handle any potential error responses return { error: 'Login failed' }; } catch (error) { console.error(error); return { error: 'Invalid credentials' }; } }
已执行调试步骤
- 确认浏览器Application标签中Cookie已正确设置
- 查看中间件日志,发现受保护路由中JWT Cookie为undefined
- 确认前端和后端均通过HTTPS提供服务
问题原因
- 路由拼写不一致:中间件和登录函数中使用的路径是
/tech-tider/create-new-content,但实际需要保护的路由是/tech-tide/create-new-content,拼写错误导致中间件逻辑无法正确匹配目标路由,跳转后触发错误重定向。 - 跨域HttpOnly Cookie无法被Next.js中间件读取:后端设置的Cookie domain为Azure容器域名,与前端Vercel域名属于完全不同的域,Next.js中间件运行在Vercel服务器端,无法读取浏览器存储的跨域HttpOnly Cookie,导致中间件判断用户未登录,跳转后重定向回登录页。
- Cookie域隔离特性:浏览器将Cookie与设置它的后端域名绑定,刷新前端登录页时,浏览器未向后端域名发送请求,因此不会携带该Cookie,页面无法读取;访问/contents页时若有向后端的请求,浏览器才会携带Cookie,所以Cookie可见。
解决方案
1. 修正路由拼写错误
将所有代码中/tech-tider的拼写统一改为/tech-tide,包括:
- 中间件中的
path判断和重定向URL - 中间件的
matcher配置 - 登录函数中的跳转路径
2. 调整Cookie配置,解决跨域读取问题
方案A:使用同主域子域名(推荐)
为前端和后端配置同一主域的子域名(如前端app.yourdomain.com,后端api.yourdomain.com),修改后端Cookie配置:
ResponseCookie cookie = ResponseCookie.from("tech-tide-auth-cookie", token) // ... 其他配置 .domain(".yourdomain.com") // 主域前缀,让子域名共享Cookie // ... 其他配置 .build();
此方案下,Next.js中间件可以读取同主域的Cookie,认证逻辑正常。
方案B:移除Cookie的domain配置,改用客户端侧认证验证
移除后端Cookie的domain设置,让浏览器自动绑定到后端域名,然后:
- 登录成功后,前端调用后端的验证登录状态接口(比如
/api/auth/verify),确认Cookie有效 - 在受保护的前端组件中,通过客户端请求验证接口的结果判断是否允许访问,替代服务器端中间件的Cookie读取逻辑
- 或者,登录成功后后端返回JWT token到响应体,前端使用Next.js的
cookies().set()设置一个前端域名的HttpOnly Cookie,供中间件读取(需确保token传输安全)
3. 优化登录跳转逻辑
登录成功后,先等待Cookie生效,再执行跳转:
async function handleSubmit(previousState: unknown, formData: FormData) { // ... 现有逻辑 try { const response = await axiosInstance.post(TECH_TIDE_AUTH_URL, formData, { headers: { "Content-Type": "multipart/form-data" }, withCredentials: true }); if (response.status === 200) { // 新增:调用验证接口确认Cookie生效 await axiosInstance.get('/api/auth/verify', { withCredentials: true }); // 再执行跳转 router.replace("/tech-tide/create-new-content"); return null; } // ... 错误处理 } catch (error) { // ... 错误处理 } }
4. 调整中间件逻辑适配跨域场景
如果无法使用同主域,修改中间件逻辑,允许客户端先进入页面,再在组件内做认证校验:
export function middleware(request: NextRequest) { // 不再在服务器端校验Cookie,改为客户端校验 return NextResponse.next(); }
然后在/tech-tide/create-new-content组件中:
'use client'; import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { axiosInstance } from '@/path/to/axios'; export default function CreateNewContent() { const router = useRouter(); useEffect(() => { const checkAuth = async () => { try { await axiosInstance.get('/api/auth/verify', { withCredentials: true }); } catch (error) { router.push('/tech-tide'); } }; checkAuth(); }, [router]); // 页面内容 return <div>Create New Content</div>; }
内容的提问来源于stack exchange,提问作者Thompson Oretan
相关产品推荐
相关产品推荐

