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

Next.js中间件无法检测Spring Boot后端设置的JWT Cookie问题

跨域登录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提供服务

问题原因

  1. 路由拼写不一致:中间件和登录函数中使用的路径是/tech-tider/create-new-content,但实际需要保护的路由是/tech-tide/create-new-content,拼写错误导致中间件逻辑无法正确匹配目标路由,跳转后触发错误重定向。
  2. 跨域HttpOnly Cookie无法被Next.js中间件读取:后端设置的Cookie domain为Azure容器域名,与前端Vercel域名属于完全不同的域,Next.js中间件运行在Vercel服务器端,无法读取浏览器存储的跨域HttpOnly Cookie,导致中间件判断用户未登录,跳转后重定向回登录页。
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 10:27:01