通用架构组件:如何在 SpringBoot 中设计一套可配置的跨域管理插件

10 阅读3分钟

1. 同源策略

浏览器有一个强制的安全策略,叫同源策略,它规定:

只有同源的网页,才能访问彼此的数据。

同源的 3 个条件:协议 + 域名 + 端口 相同,只要任意一项不同就不是同源,如:

www.abc.com:80www.abc.com:443 就不是同源;

在实际开发中,一个前后端分离项目,前端项目运行在 http://localhost:8080 ,后端项目运行在 http://localhost:9090 ,两个项目就跨域了,需要处理跨域问题。

跨域请求是被浏览器拦截的,通过命令(curl)后者 postman 访问则不会出现跨域。

2. 预检请求

简单请求在响应阶段做 CORS 校验,失败则浏览器不暴露结果给 JS;非简单请求会先进行预检(OPTIONS),预检通过后才发送正式请求,但正式响应仍需再次通过 CORS 校验。

预检请求是浏览器自动发送的、独立的 OPTIONS HTTP 请求,用于在正式跨域请求前确认是否被服务器允许,预检失败时正式请求不会被发送。

只要满足下列任意一点都会触发预检请求:

  1. 请求方法不是 GET、HEAD、POST
  2. Authorization 请求头
  3. Content-Type: application/json
  4. 存在自定义 header(请求头)
  5. 设定了 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 的规则,可以配置如下内容:

  1. 任意:* 配置直接回显浏览器的 Origin 源
  2. 子域通配:https://*.example.com
  3. 精确:协议 + 域 + 端口
  4. 多个 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 啦!