从 “跨域报错到彻底解决”: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 跨域过滤器实例*/@Beanpublic 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 资源处理器注册器*/@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/**").addResourceLocations("classpath:/static/").addResourceLocations("classpath:/META-INF/resources/");}/*** 路径匹配配置:忽略URL大小写* 提高接口访问的容错性* @param configurer 路径匹配配置器*/@Overridepublic 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 配置过程中可能抛出的异常*/@Beanpublic 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异常(如响应流写入失败)*/@Overridepublic 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<Void> 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-Origin
、Access-Control-Allow-Credentials
等字段,跨域报错无法解决。
Network 面板关键信息:
- 状态码:
200 OK
(业务逻辑正常) - Response Headers:仅包含
Content-Type: application/json;charset=UTF-8
、Date: 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 同源策略的作用
同源策略是浏览器的核心安全机制,目的是 防止恶意网站窃取其他网站的敏感数据。它会限制以下行为:
- 无法读取不同源网页的 Cookie、