纲要
- 跨域问题背景
- 浏览器同源策略与
CORS错误 - 前后端分离开发中的典型跨域场景
- 浏览器同源策略与
- 跨域解决方案概览
- 前端开发服务器代理
- 后端
CORS配置
- Spring MVC 中的全局 CORS 配置
WebMvcConfigurer与addCorsMappings- 允许来源、方法、头部设置
- Spring Security 中的 CORS 配置
- 自定义
CorsConfigurationSourceBean - 按环境区分配允许的来源(开发 / 生产)
- 暴露自定义响应头(
X-Authenticity等) - 在安全过滤链中启用 CORS
- 自定义
- 完整可运行配置示例
- 验证与踩坑
- 响应头暴露的重要性
- 跨域请求失败时的排查思路
跨域问题背景
现代前后端分离项目中,前端应用通常运行在 localhost:4001,而后端 API 可能部署在 localhost:8080。当浏览器从不同源(协议、域名、端口任一不同)发起 HTTP 请求时,就会触发浏览器的同源策略,出现类似以下的 CORS 错误:
Access to XMLHttpRequest at 'http://localhost:8080/authorize/token' from origin 'http://localhost:4001' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
这意味着后端服务需要显式声明允许哪些源访问,否则浏览器会拦截响应。
跨域解决方案概览
解决跨域访问通常有两种思路:
- 前端代理:利用前端开发服务器(如 webpack-dev-server)将 API 请求代理到后端,从而变为同源访问。
- 后端 CORS 配置 :在服务端添加
Access-Control-Allow-Origin等响应头,让浏览器允许跨域请求。
在 Spring 生态中,后端配置 CORS 更为通用,并且不依赖前端工具链,下面重点讲解后端的配置方式。
Spring MVC 中的全局 CORS 配置
如果项目未引入 Spring Security,可以直接在 WebMvcConfigurer 中配置全局 CORS 规则:
java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 所有接口
.allowedOrigins("http://localhost:4001") // 允许的前端源
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
}
这种方式适用于纯 Spring MVC 项目,但它不会影响 Spring Security 的过滤器链。
Spring Security 中的 CORS 配置
在整合了 Spring Security 的 OAuth2 授权服务中,跨域请求会先经过 Security 过滤器链。如果仅在 MVC 层配置 CORS,Security 层的请求仍会被拦截。正确做法是在 Spring Security 中显式启用 CORS 并提供一个 CorsConfigurationSource Bean。
自定义 CorsConfigurationSource
创建一个配置类,通过 @Value 读取外部配置,动态设置允许的源,并将 CorsConfigurationSource 注册到 Spring 容器中。这里特意暴露了自定义响应头 X-Authenticity,用于二次认证场景。
java
package com.example.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
public class CorsConfig {
@Value("${cors.allowed-origins:http://localhost:4001}")
private String allowedOrigins;
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
// 重要:暴露自定义响应头,例如二次认证时返回的 X-Authenticity
configuration.setExposedHeaders(Arrays.asList("X-Authenticity", "Authorization"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
在安全配置中启用 CORS
在 Security 配置类中注入 CorsConfigurationSource,并通过 cors() 方法启用:
java
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfigurationSource;
@Configuration
public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource;
public SecurityConfig(CorsConfigurationSource corsConfigurationSource) {
this.corsConfigurationSource = corsConfigurationSource;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt()); // 示例 OAuth2 配置
return http.build();
}
}
为什么需要暴露自定义响应头?
在 OAuth2 二次验证流程中,后端可能会在 401 响应里附加 X-Authenticity 头,提示客户端需要进行下一步认证。对于跨域请求,浏览器默认只暴露 Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma 等少量简单响应头。如果不通过 setExposedHeaders() 显式声明,前端 JavaScript 代码将无法读取 X-Authenticity,导致认证流程中断,可能看到 500 错误或无响应头的情况。
跨域请求流程示意
以下时序图展示了前端发出跨域请求,后端配置 CORS 后顺利返回自定义响应头的过程:
资源服务器 Spring Security 过滤器 浏览器 (localhost:4001) 资源服务器 Spring Security 过滤器 浏览器 (localhost:4001) #mermaid-svg-dNDZsmMpdny2zjZU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dNDZsmMpdny2zjZU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dNDZsmMpdny2zjZU .error-icon{fill:#552222;}#mermaid-svg-dNDZsmMpdny2zjZU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dNDZsmMpdny2zjZU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dNDZsmMpdny2zjZU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dNDZsmMpdny2zjZU .marker.cross{stroke:#333333;}#mermaid-svg-dNDZsmMpdny2zjZU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dNDZsmMpdny2zjZU p{margin:0;}#mermaid-svg-dNDZsmMpdny2zjZU .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dNDZsmMpdny2zjZU text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-dNDZsmMpdny2zjZU .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dNDZsmMpdny2zjZU .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-dNDZsmMpdny2zjZU .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-dNDZsmMpdny2zjZU .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-dNDZsmMpdny2zjZU #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-dNDZsmMpdny2zjZU .sequenceNumber{fill:white;}#mermaid-svg-dNDZsmMpdny2zjZU #sequencenumber{fill:#333;}#mermaid-svg-dNDZsmMpdny2zjZU #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-dNDZsmMpdny2zjZU .messageText{fill:#333;stroke:none;}#mermaid-svg-dNDZsmMpdny2zjZU .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dNDZsmMpdny2zjZU .labelText,#mermaid-svg-dNDZsmMpdny2zjZU .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-dNDZsmMpdny2zjZU .loopText,#mermaid-svg-dNDZsmMpdny2zjZU .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-dNDZsmMpdny2zjZU .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dNDZsmMpdny2zjZU .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-dNDZsmMpdny2zjZU .noteText,#mermaid-svg-dNDZsmMpdny2zjZU .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-dNDZsmMpdny2zjZU .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dNDZsmMpdny2zjZU .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dNDZsmMpdny2zjZU .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dNDZsmMpdny2zjZU .actorPopupMenu{position:absolute;}#mermaid-svg-dNDZsmMpdny2zjZU .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-dNDZsmMpdny2zjZU .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dNDZsmMpdny2zjZU .actor-man circle,#mermaid-svg-dNDZsmMpdny2zjZU line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-dNDZsmMpdny2zjZU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 前端可读取 X-Authenticity 并继续流程 OPTIONS /api/authorize (预检)200 OK (Access-Control-Allow-Origin: localhost:4001)POST /api/authorize (真实请求)转发认证401 + X-Authenticity: required401 + X-Authenticity (暴露)
完整项目配置示例
以下为 Spring Boot 项目的核心配置文件及结构,可直接运行。
项目结构
text
src/main/java/com/example/config/
├── CorsConfig.java
└── SecurityConfig.java
src/main/resources/
├── application.yml
application.yml
yaml
cors:
allowed-origins: http://localhost:4001
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://your-issuer
CorsConfig.java
java
package com.example.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
public class CorsConfig {
@Value("${cors.allowed-origins:http://localhost:4001}")
private String allowedOrigins;
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(allowedOrigins.split(",")));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setExposedHeaders(Arrays.asList("X-Authenticity", "Authorization"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
SecurityConfig.java
java
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfigurationSource;
@Configuration
public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource;
public SecurityConfig(CorsConfigurationSource corsConfigurationSource) {
this.corsConfigurationSource = corsConfigurationSource;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
}
}
验证与踩坑
- 预检请求必须通过 :Spring Security 配置 CORS 后,
OPTIONS请求会被 CORS 过滤器处理并返回 200,无需额外放行。 - 响应头暴露缺失 :若未调用
setExposedHeaders,即使后端设置了X-Authenticity,浏览器也会阻止 JavaScript 读取该头,导致认证失败。可根据业务需要添加需要暴露的头名称。 - 多环境切换 :通过
@Value或@Profile灵活指定allowed-origins,开发环境使用localhost:4001,生产环境使用真实域名。 - 调试技巧 :打开浏览器开发者工具 Network 面板,检查响应头中是否包含
Access-Control-Allow-Origin和Access-Control-Expose-Headers,确认配置生效。
总结
本文详细梳理了在 Spring Security + OAuth2 项目中处理跨域访问的常见方案。
重点在于 Spring Security 侧的 CorsConfigurationSource 配置,以及暴露自定义响应头的重要性。
通过合理配置,前端可以无缝发起跨域认证请求,并完整读取后端返回的认证提示头,保障二次认证流程的顺利执行。