1. 跨域问题与CORS基础概念
第一次在Spring项目中遇到跨域问题时,我盯着浏览器控制台那个鲜红的CORS错误提示足足愣了五分钟。作为前后端分离架构下的经典难题,跨域问题几乎每个开发者都会遇到。简单来说,当你的前端页面(比如运行在http://localhost:8080)尝试访问另一个域名/端口(比如后端API在http://api.example.com)的资源时,浏览器出于安全考虑会阻止这种"跨域"请求。
CORS(Cross-Origin Resource Sharing)是现代浏览器实现的安全机制,它通过特殊的HTTP头部来协商不同源之间的资源访问权限。在Spring应用中,我们通常通过以下几种方式实现CORS支持:
- 方法级注解:在Controller方法上使用@CrossOrigin
- 全局配置:通过WebMvcConfigurer配置
- 过滤器方案:自定义Filter处理CORS头部
重要提示:生产环境中务必严格配置允许的源(Origin),避免使用通配符*,否则会带来严重的安全风险。
2. 为什么选择Filter方案
虽然@CrossOrigin注解使用简单,但在大型项目中会给每个Controller方法添加注解显得非常冗余。全局WebMvcConfigurer配置是常见选择,但在某些特殊场景下(比如需要动态判断允许的源,或者要与其他Filter配合),自定义Filter方案提供了最大的灵活性。
我在电商平台项目中就遇到过这样的需求:我们需要根据请求特征动态决定是否允许跨域。例如:
- 开发环境允许所有本地地址跨域
- 生产环境只允许信任的合作伙伴域名
- 某些特殊API需要额外头部验证
这种场景下,Filter方案的优势就显现出来了。下面是传统配置方式与Filter方案的对比:
| 特性 | @CrossOrigin | WebMvcConfigurer | 自定义Filter |
|---|---|---|---|
| 细粒度控制 | 方法级 | 全局 | 完全自定义 |
| 动态逻辑 | 不支持 | 有限支持 | 完全支持 |
| 执行顺序控制 | 不可控 | 不可控 | 可优先处理 |
| 与其他Filter的协作 | 无 | 无 | 可灵活组合 |
3. 实现自定义CORS Filter
下面是我在多个生产项目中验证过的CORS Filter实现方案。这个版本不仅处理了标准CORS头部,还包含了一些实战中积累的安全增强措施。
java复制import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
public class CustomCorsFilter implements Filter {
// 允许的源列表(生产环境应从配置读取)
private List<String> allowedOrigins = Arrays.asList(
"http://localhost:8080",
"https://trusted-domain.com"
);
@Override
public void init(FilterConfig filterConfig) {
// 初始化逻辑(如从配置加载允许的源)
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
// 获取请求源
String origin = request.getHeader("Origin");
// 检查是否在允许列表中
if (allowedOrigins.contains(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Requested-With");
response.setHeader("Access-Control-Allow-Credentials", "true");
}
// 处理预检请求
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, res);
}
}
@Override
public void destroy() {
// 清理资源
}
}
关键点解析:
- 动态源检查:只对预配置的信任源添加CORS头部,比通配符更安全
- 预检请求处理:OPTIONS请求直接返回200,不继续过滤器链
- 凭证支持:Allow-Credentials头部支持带cookie的跨域请求
- 缓存控制:Max-Age减少预检请求次数
4. Spring Boot中的Filter注册
在Spring Boot应用中注册自定义Filter有多种方式,我推荐使用FilterRegistrationBean,它可以精确控制Filter的顺序和匹配模式:
java复制import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<CustomCorsFilter> corsFilterRegistration() {
FilterRegistrationBean<CustomCorsFilter> registration =
new FilterRegistrationBean<>();
registration.setFilter(new CustomCorsFilter());
registration.addUrlPatterns("/*");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE); // 最高优先级
registration.setName("customCorsFilter");
return registration;
}
}
设置最高优先级(HIGHEST_PRECEDENCE)确保CORS Filter最先执行,这在复杂应用中尤为重要。我曾遇到一个案例:安全Filter在CORS Filter之前执行,导致预检请求被拦截,造成整个CORS机制失效。
5. 高级配置与安全考量
5.1 动态源管理
在生产环境中,硬编码允许的源列表既不灵活也不安全。我们可以通过以下方式改进:
java复制// 在Filter中添加动态源检查逻辑
private boolean isOriginAllowed(String origin) {
// 1. 检查预设白名单
if (allowedOrigins.contains(origin)) {
return true;
}
// 2. 检查动态规则(如子域名匹配)
if (origin != null && origin.endsWith(".trusted-domain.com")) {
return true;
}
// 3. 从数据库或缓存查询
return originService.isAllowed(origin);
}
5.2 安全加固措施
-
Vary头部:添加
Vary: Origin防止CDN缓存错误响应java复制response.setHeader("Vary", "Origin"); -
敏感头部限制:避免暴露不必要的头部
java复制response.setHeader("Access-Control-Expose-Headers", "X-Custom-Header"); -
CSRF防护:虽然CORS提供了一定保护,但仍需配合CSRF令牌
java复制response.setHeader("Access-Control-Allow-Headers", "X-CSRF-TOKEN, ...");
5.3 性能优化
-
预检请求缓存:合理设置Max-Age(单位秒)
java复制response.setHeader("Access-Control-Max-Age", "86400"); // 24小时 -
条件性处理:非跨域请求跳过CORS逻辑
java复制if (origin == null) { chain.doFilter(req, res); return; }
6. 常见问题排查指南
以下是我在支持多个项目时总结的CORS问题排查表:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 预检请求返回403 | 安全Filter拦截了OPTIONS请求 | 调整Filter顺序 |
| 凭证(cookie)无法发送 | Allow-Credentials未设置/源为* | 设置true+指定具体源 |
| 自定义头部被拦截 | 未在Allow-Headers中声明 | 添加头部到允许列表 |
| 跨域请求在Postman正常浏览器失败 | 浏览器安全策略 | 确保服务器返回正确CORS头部 |
| 部分方法(PUT,DELETE)不可用 | Allow-Methods未包含该方法 | 补充方法到允许列表 |
一个特别隐蔽的问题我曾在Safari上遇到过:即使服务器配置正确,Safari对 credentialed请求的缓存行为也可能导致问题。解决方案是添加:
java复制response.setHeader("Vary", "Origin");
7. 测试策略与验证方法
完善的测试是确保CORS配置正确的关键。我建议采用分层测试策略:
- 单元测试:验证Filter逻辑
java复制@Test
public void testAllowedOrigin() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Origin", "http://localhost:8080");
MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(request, response, (req, res) -> {});
assertEquals("http://localhost:8080",
response.getHeader("Access-Control-Allow-Origin"));
}
- 集成测试:使用TestRestTemplate模拟跨域请求
java复制@Test
public void testCorsHeadersInResponse() {
HttpHeaders headers = new HttpHeaders();
headers.set("Origin", "http://localhost:8080");
ResponseEntity<String> response = restTemplate.exchange(
"/api/data", HttpMethod.GET,
new HttpEntity<>(headers), String.class);
assertNotNull(response.getHeaders()
.getFirst("Access-Control-Allow-Origin"));
}
-
手动验证:使用浏览器开发者工具检查:
- 网络请求中的请求/响应头部
- 控制台错误信息
- 预检请求(OPTIONS)的流程
-
自动化监控:在生产环境日志中监控CORS相关错误,设置告警规则。
8. 与其他Spring安全组件的协作
当项目同时使用Spring Security时,CORS配置需要特别注意执行顺序。推荐以下配置方式:
java复制@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().configurationSource(corsConfigurationSource())
.and()
// 其他安全配置...
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("http://localhost:8080"));
config.setAllowedMethods(Arrays.asList("GET","POST"));
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
关键注意事项:
- 不要在Security配置和Filter中重复配置CORS
- Spring Security的cors()配置会覆盖Filter设置
- 复杂场景建议禁用Security的cors(),完全使用Filter方案
在微服务架构中,更好的做法是在API Gateway层统一处理CORS,而不是在每个微服务中重复配置。这不仅能集中管理策略,还能减少性能开销。