深入解析:从 “跨域报错到彻底解决”:Spring Boot+Security+JWT 实战踩坑指南

作为后端开发者,你是否曾遇到过这样的场景:前端调用接口时控制台疯狂报错 Access to fetch at 'xxx' from origin 'xxx' has been blocked by CORS policy,注释掉一行 permitAll() 就跨域,加上又能正常访问?明明配置了 CorsFilter 却始终不生效,甚至出现 Failed to load response data: No data found for resource with given identifier 这样的 “幽灵错误”?

跨域问题看似简单,实则涉及浏览器同源策略、Spring 过滤器链、Spring Security 拦截逻辑等多个层面的联动。本文将从实际项目踩坑场景出发,用 “问题复现→底层原理→分步解决→代码实战” 的逻辑,带你彻底搞懂跨域问题的本质,掌握 Spring Boot+Security+JWT 环境下跨域配置的标准方案,从此不再被跨域 “折磨”。

一、问题复现:那些年我们踩过的跨域坑

在开始讲解原理前,先复现几个实际项目中高频出现的跨域场景,看看你是否也曾遇到过类似问题。

1.1 场景 1:配置了 CorsFilter,注释 permitAll () 就跨域

项目环境

  • Spring Boot 3.2.5(最新稳定版)
  • Spring Security 6.2.4
  • JDK 17
  • JWT 认证(jjwt-api 0.12.5)

核心代码

(1)CORS 配置类(WebDefaultConfig.java)
package com.sq.twinbee.config.web;
import org.springframework.context.annotation.Bean;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
 * Web基础配置类,包含CORS、资源映射、路径匹配等配置
 * @author ken
 */
public class WebDefaultConfig implements WebMvcConfigurer {
    /**
     * 跨域过滤器配置
     * 解决前后端分离场景下的跨域请求问题
     * @return CorsFilter 跨域过滤器实例
     */
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // 允许所有来源(此处为后续问题埋下伏笔)
        config.addAllowedOriginPattern("*");
        // 允许所有HTTP方法(GET、POST、PUT、DELETE、OPTIONS等)
        config.addAllowedMethod("*");
        // 允许所有请求头(包括自定义头如token、app-id)
        config.addAllowedHeader("*");
        // 允许携带凭证(如Cookie、JWT Token)
        config.setAllowCredentials(true);
        // 预检请求缓存时间(秒),减少重复预检请求
        config.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        // 对所有/api/**路径的请求应用跨域配置
        configSource.registerCorsConfiguration("/api/**", config);
        return new CorsFilter(configSource);
    }
    /**
     * 静态资源映射配置
     * 解决Swagger、前端静态资源访问问题
     * @param registry 资源处理器注册器
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/META-INF/resources/");
    }
    /**
     * 路径匹配配置:忽略URL大小写
     * 提高接口访问的容错性
     * @param configurer 路径匹配配置器
     */
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        AntPathMatcher matcher = new AntPathMatcher();
        // 设置路径匹配不区分大小写
        matcher.setCaseSensitive(false);
        configurer.setPathMatcher(matcher);
    }
}
(2)Spring Security 配置类(SecurityConfig.java)
package com.sq.twinbee.config.security;
import com.sq.twinbee.config.web.point.JwtAuthenticationEntryPoint;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
 * Spring Security配置类,负责认证、授权、会话管理等配置
 * @author ken
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final AuthenticationProvider authenticationProvider;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    /**
     * 配置Security过滤器链
     * 定义请求授权规则、认证异常处理、会话策略等
     * @param http HttpSecurity配置对象
     * @return SecurityFilterChain 安全过滤器链实例
     * @throws Exception 配置过程中可能抛出的异常
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF保护(JWT认证场景下无需CSRF)
                .csrf(csrf -> csrf.disable())
                // 配置未认证请求的处理策略
                .exceptionHandling(ex -> ex.authenticationEntryPoint(jwtAuthenticationEntryPoint))
                // 配置会话策略:无状态(JWT是无状态认证)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置请求授权规则
                .authorizeHttpRequests(auth -> auth
                        // Swagger3相关路径允许匿名访问
                        .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
                        // 登录、部门列表接口允许匿名访问
                        .requestMatchers("/api/client/department/login").permitAll()
                        .requestMatchers("/api/client/department/getDepartmentList").permitAll()
                        // 关键配置:/api/client/**路径允许匿名访问(注释后跨域报错)
                        .requestMatchers("/api/client/**").permitAll()
                        // 其他静态资源允许匿名访问
                        .requestMatchers("/doc.html", "/webjars/**", "/favicon.ico").permitAll()
                        // 所有其他请求必须认证
                        .anyRequest().authenticated()
                )
                // 设置认证提供者
                .authenticationProvider(authenticationProvider)
                // 在UsernamePasswordAuthenticationFilter前添加JWT认证过滤器
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}
(3)认证失败处理类(JwtAuthenticationEntryPoint.java)
package com.sq.twinbee.config.web.point;
import com.alibaba.fastjson2.JSON;
import com.sq.twinbee.config.exception.ExceptionEnum;
import com.sq.twinbee.model.ApiResult;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
 * JWT认证入口点,处理未认证的请求
 * 当用户访问受保护资源但未提供有效认证信息时被调用
 * @author ken
 */
@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    /**
     * 当认证失败时调用此方法
     * 返回401未授权响应给前端
     * @param request 请求对象
     * @param response 响应对象
     * @param authException 认证异常(包含认证失败原因)
     * @throws IOException IO异常(如响应流写入失败)
     */
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 记录认证失败日志,包含请求URL和异常信息(便于问题排查)
        log.warn("访问未授权资源,URL: {}, 原因: {}", request.getRequestURI(), authException.getMessage());
        // 设置响应内容类型和编码(确保前端能正确解析JSON)
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        // 设置响应状态码:401未授权
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        // 构建统一响应结果(符合项目接口规范)
        ApiResult result = ApiResult.error(
                ExceptionEnum.NO_PERMISSION.getResultCode(),
                ExceptionEnum.NO_PERMISSION.getResultMsg(),
                "访问被拒绝,请先进行认证: " + authException.getMessage()
        );
        // 将响应结果写入输出流(返回给前端)
        response.getWriter().write(JSON.toJSONString(result));
    }
}

问题现象

  • 保留 .requestMatchers("/api/client/**").permitAll() 时,前端调用 /api/client/work/order/list 接口正常,无跨域报错;
  • 注释掉该配置后,前端立即报错:Access to fetch at 'http://10.10.10.13:8866/twinbee/api/client/work/order/list' from origin 'http://10.10.10.237:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.,同时 Network 面板显示 Failed to load response data: No data found for resource with given identifier

1.2 场景 2:OPTIONS 预检请求被拦截,跨域直接失败

问题现象:前端发送带自定义头(如 token: Bearer xxx)的请求时,浏览器先发送 OPTIONS 预检请求,但该请求被后端拦截,返回 403 或 401 状态码,导致后续业务请求无法执行。

Network 面板关键信息

  • 请求方法:OPTIONS
  • 请求地址:http://10.10.10.13:8866/twinbee/api/client/work/order/list
  • 状态码:403 Forbidden
  • Response Headers:空(无任何 CORS 相关头)

1.3 场景 3:配置了 CorsFilter,但响应始终无 CORS 头

问题现象:明明在 WebDefaultConfig 中配置了 CorsFilter,但前端请求的响应头中始终没有 Access-Control-Allow-OriginAccess-Control-Allow-Credentials 等字段,跨域报错无法解决。

Network 面板关键信息

  • 状态码:200 OK(业务逻辑正常)
  • Response Headers:仅包含 Content-Type: application/json;charset=UTF-8Date: Tue, 14 May 2024 08:00:00 GMT 等基础头,无任何 CORS 头。

二、底层原理:搞懂跨域问题的 “根”

要解决跨域问题,必须先搞懂 浏览器同源策略 和 CORS(跨域资源共享)机制 的底层逻辑 —— 这是所有跨域配置的理论基础,也是避免 “头痛医头” 的关键。

2.1 同源策略:浏览器的 “安全门卫”

2.1.1 什么是同源?

“同源” 指的是 协议、域名、端口三者完全一致。例如:

  • http://a.com:8080 与 http://a.com:8080/api:同源(协议、域名、端口均一致);
  • http://a.com:8080 与 https://a.com:8080:不同源(协议不同:http vs https);
  • http://a.com:8080 与 http://b.com:8080:不同源(域名不同:a.com vs b.com);
  • http://a.com:8080 与 http://a.com:8081:不同源(端口不同:8080 vs 8081)。
2.1.2 同源策略的作用

同源策略是浏览器的核心安全机制,目的是 防止恶意网站窃取其他网站的敏感数据。它会限制以下行为:

  1. 无法读取不同源网页的 Cookie、
posted @ 2025-11-09 08:15  clnchanpin  阅读(105)  评论(0)    收藏  举报