1. 同源策略
浏览器有一个强制的安全策略,叫同源策略,它规定:
只有同源的网页,才能访问彼此的数据。
同源的 3 个条件:协议 + 域名 + 端口 相同,只要任意一项不同就不是同源,如:
www.abc.com:80 和 www.abc.com:443 就不是同源;
在实际开发中,一个前后端分离项目,前端项目运行在 http://localhost:8080 ,后端项目运行在 http://localhost:9090 ,两个项目就跨域了,需要处理跨域问题。
跨域请求是被浏览器拦截的,通过命令(curl)后者 postman 访问则不会出现跨域。
2. 预检请求
简单请求在响应阶段做 CORS 校验,失败则浏览器不暴露结果给 JS;非简单请求会先进行预检(OPTIONS),预检通过后才发送正式请求,但正式响应仍需再次通过 CORS 校验。
预检请求是浏览器自动发送的、独立的 OPTIONS HTTP 请求,用于在正式跨域请求前确认是否被服务器允许,预检失败时正式请求不会被发送。
只要满足下列任意一点都会触发预检请求:
- 请求方法不是 GET、HEAD、POST
- 带 Authorization 请求头
Content-Type: application/json- 存在自定义 header(请求头)
- 设定了 credentials/withCredentials (是否允许携带 cookie)
3. 解决跨域
如何处理跨域?一句话总结为:
后端服务告诉浏览器:> 这个来源、这种请求方式、这些 Header、是否带 Cookie,我是允许的,可以获取接口返回数据。
- Access-Control-Allow-Origin:允许哪些来源
- 协议+域名+端口,如
http://localhost:3000(浏览器请求:Origin 头会携带) - 允许配置 * ,表示任何源都能访问
- 协议+域名+端口,如
- Access-Control-Allow-Methods:配置允许的请求方式
- GET, POST, PUT, PATCH、 DELETE, OPTIONS
- 允许配置 * ,表示所有请求方式
- Access-Control-Allow-Headers:允许哪些请求头
- Content-Type, Authorization, X-Requested-With, 自定义请求头
- 允许配置 * ,表示任何请求头
- Access-Control-Allow-Credentials:是否允许携带凭证(Cookie)
- Access-Control-Allow-Credentials: true
- Access-Control-Max-Age:预检缓存时间(可选但推荐)
- Access-Control-Max-Age: 3600
dang > 当 Access-Control-Allow-Credentials: true 时, **Access-Control-Allow-Origin 绝对不能是 * (这是 W3C CORS 标准的强制规定)
4. 实战
1. 抽象成配置内容
@ConfigurationProperties(prefix = "ark.web.cors")
@Getter
@Setter
public class CorsProperties {
/**
* 是否启用全局 CORS 配置。
* 如果是微服务项目,应该在网关层做配置,避免下沉到每个服务。
*/
private boolean enabled = false;
private List<String> allowedOrigins = new ArrayList<>();
private List<String> allowedOriginPatterns = new ArrayList<>();
private List<String> allowedMethods = List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS");
private List<String> allowedHeaders = List.of("*");
private List<String> exposedHeaders = new ArrayList<>();
/**
* 是否允许携带凭证(Cookie/Authorization 等)。
*
* <p>当该值为 true 时,标准 CORS 不允许 {@code allowedOrigins} 使用 {@code *};
* 如需放开通配,请使用 {@code allowedOriginPatterns=*}。
*/
private boolean allowCredentials = true;
/**
* 预检请求结果缓存时长(秒)。
*/
private Duration maxAge = Duration.ofHours(1);
}
allowedOriginPatterns 是 Spring 的规则,可以配置如下内容:
- 任意:
*配置直接回显浏览器的 Origin 源 - 子域通配:
https://*.example.com - 精确:协议 + 域 + 端口
- 多个 pattern:可以配置多个规则
2. 配置
/**
* Web MVC 全局 CORS 自动配置。
*
* <p>
* 仅在 {@code ark.web.cors.enabled=true} 时启用,避免默认放开跨域策略。
*
* @author XF
* @since 2026/01/10
*/
@AutoConfiguration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(WebMvcConfigurer.class)
@ConditionalOnProperty(prefix = "ark.web.cors", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(CorsProperties.class)
public class CorsWebMvcConfiguration {
/**
* 注册 WebMvcConfigurer,由 WebMvcAutoConfiguration 调用并注册 cors 配置。
*/
@Bean
public WebMvcConfigurer arkCorsWebMvcConfigurer(CorsProperties properties) {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
CorsRegistration registration = registry.addMapping("/**")
.allowedMethods(toArray(properties.getAllowedMethods()))
.allowedHeaders(toArray(properties.getAllowedHeaders()))
.exposedHeaders(toArray(properties.getExposedHeaders()))
.allowCredentials(properties.isAllowCredentials())
// Spring MVC API 这里使用秒为单位
.maxAge(properties.getMaxAge().getSeconds());
if (!properties.getAllowedOrigins().isEmpty()) {
if (properties.isAllowCredentials() && properties.getAllowedOrigins().contains("*")) {
// allowCredentials=true 时 allowedOrigins 不允许使用 "*",用 patterns 兜底兼容
registration.allowedOriginPatterns("*");
} else {
registration.allowedOrigins(toArray(properties.getAllowedOrigins()));
}
}
if (!properties.getAllowedOriginPatterns().isEmpty()) {
registration.allowedOriginPatterns(toArray(properties.getAllowedOriginPatterns()));
}
}
};
}
private static String[] toArray(List<String> list) {
return list.toArray(String[]::new);
}
}
掌握以上知识,一个生产级的跨域配置就 OK 啦!