深入解析:从 “跨域报错到彻底解决”: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-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、
浙公网安备 33010602011771号