乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

CORS

跨域资源共享(Cross-Origin Resource Sharing,CORS)是浏览器实施的同源安全策略的放宽机制。Spring Security 通过 CorsFilter 与 CorsConfigurationSource 配合,允许服务器精确声明哪些外部源可以访问本域资源,是前后端分离架构的必备配置。


定义与作用

一句话定义:浏览器默认阻止跨域 AJAX 请求,CORS 通过一组 HTTP 响应头(Access-Control-Allow-*)告知浏览器:服务器允许特定域名的脚本访问本域资源。

场景是否跨域示例
同域否https://a.com/page → https://a.com/api
不同域名是https://app.com → https://api.com
不同端口是https://a.com → https://a.com:8080
不同协议是http://a.com → https://a.com

两类跨域请求:

  • 简单请求:GET / HEAD / POST(Content-Type 为 application/x-www-form-urlencoded、multipart/form-data、text/plain),直接发送,浏览器通过响应头判断是否放行;
  • 预检请求(Preflight):非简单请求(如 PUT / DELETE、自定义 Header、Content-Type 为 application/json)先发送 OPTIONS 请求,服务器返回许可后,浏览器才发送实际请求。

Spring Security 的 CorsFilter 位于过滤器链第 5 位(在 HeaderWriterFilter 之后、CsrfFilter 之前),负责拦截 CORS 相关请求并写入响应头。


攻击原理与防护流程

核心原理:服务器通过 CorsConfigurationSource 声明允许的源(Origin)、方法(Methods)、头(Headers)、凭证(Credentials)等规则;CorsFilter 根据请求 Origin 头匹配配置,将对应规则写入响应头返回给浏览器;浏览器依据响应头决定是否放行。跨域请求的拦截权在浏览器,而非服务器——服务器始终会收到请求,CORS 防止的是恶意站点读取响应内容。


核心配置:CorsConfigurationSource

1. 基于 URL 映射的精细化配置(推荐)

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors()
                .configurationSource(corsConfigurationSource())
            .and()
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/**").authenticated();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Arrays.asList("https://app.example.com", "https://admin.example.com"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
        config.setExposedHeaders(Arrays.asList("X-Total-Count")); // 允许前端读取的响应头
        config.setAllowCredentials(true); // 允许携带 Cookie / Authorization Header
        config.setMaxAge(3600L); // 预检请求缓存 1 小时

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config); // 仅对 API 路径生效
        return source;
    }
}

2. 使用 CorsRegistry 简化配置(Spring MVC 层,Spring Security 可复用)

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://app.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("Authorization", "Content-Type")
            .allowCredentials(true)
            .maxAge(3600);
    }
}

注意:若使用 WebMvcConfigurer 配置,Spring Security 中只需 http.cors() 而不需要显式 configurationSource(),Spring 会自动检测并委托给 MVC 层的 CORS 配置。


完整示例一:前后端分离项目配置精确跨域

场景说明

电商系统前端部署在 https://mall.example.com,后端 API 在 https://api.mall.com。前端需要调用 /api/orders 获取订单数据,且登录态通过 Cookie 维持。要求仅允许该前端域名访问,禁止其他域名的脚本窃取数据。

操作前配置(无 CORS 或配置错误,浏览器报错)

@EnableWebSecurity
public class MallSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable() // 错误:不应禁用,同时前端需要 Cookie
            // 未配置 cors(),浏览器默认阻止所有跨域请求
            .authorizeRequests()
                .antMatchers("/api/orders/**").authenticated()
                .anyRequest().permitAll()
            .and()
            .formLogin()
                .loginProcessingUrl("/api/login")
                .permitAll();
    }
}

浏览器控制台报错:Access to XMLHttpRequest at 'https://api.mall.com/api/orders' from origin 'https://mall.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

前端无法获取任何响应数据,即使后端服务器已正常处理请求(可通过后端日志确认)。

操作后配置(精确源 + 凭证 + 预检缓存)

@EnableWebSecurity
public class MallSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors()
                .configurationSource(corsConfigurationSource())
            .and()
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/orders/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginProcessingUrl("/api/login")
                .permitAll();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(Collections.singletonList("https://mall.example.com"));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-CSRF-TOKEN"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

结果分析:前端 axios.get('https://api.mall.com/api/orders') 携带 Origin: https://mall.example.com 和 Cookie: JSESSIONID=...。CorsFilter 匹配到 /api/** 配置,返回 Access-Control-Allow-Origin: https://mall.example.com 和 Access-Control-Allow-Credentials: true。浏览器校验通过,前端成功获取订单 JSON。恶意站点 https://evil.com 发起请求时,Origin 不匹配,响应中无 Access-Control-Allow-Origin,浏览器拒绝其读取响应。


完整示例二:多环境动态 CORS 配置

场景说明

SaaS 平台支持客户自定义前端域名,后端需要根据当前租户白名单动态判断是否允许跨域。不能使用写死的 allowedOrigins,必须从数据库查询当前租户配置的域名。

操作前配置(硬编码多源,维护困难)

@EnableWebSecurity
public class SaasSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors()
                .configurationSource(request -> {
                    CorsConfiguration config = new CorsConfiguration();
                    // ❌ 硬编码:每新增一个客户域名都要改代码重启
                    config.setAllowedOrigins(Arrays.asList(
                        "https://tenant-a.com",
                        "https://tenant-b.com",
                        "https://tenant-c.com"
                    ));
                    config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
                    config.setAllowCredentials(true);
                    return config;
                })
            .and()
            .authorizeRequests()
                .antMatchers("/api/tenant/**").authenticated();
    }
}

操作后配置(动态从数据库读取白名单)

@EnableWebSecurity
public class SaasSecurityConfig extends WebSecurityConfigurerAdapter {

    private final TenantDomainService tenantDomainService;

    public SaasSecurityConfig(TenantDomainService tenantDomainService) {
        this.tenantDomainService = tenantDomainService;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .cors()
                .configurationSource(tenantCorsConfigurationSource())
            .and()
            .csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .authorizeRequests()
                .antMatchers("/api/tenant/**").authenticated();
    }

    @Bean
    public CorsConfigurationSource tenantCorsConfigurationSource() {
        return request -> {
            String origin = request.getHeader("Origin");
            String tenantId = request.getHeader("X-Tenant-ID");
            List<String> allowedDomains = tenantDomainService.findAllowedDomains(tenantId);

            CorsConfiguration config = new CorsConfiguration();
            if (origin != null && allowedDomains.contains(origin)) {
                config.setAllowedOrigins(Collections.singletonList(origin));
            }
            config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
            config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Tenant-ID", "X-CSRF-TOKEN"));
            config.setAllowCredentials(true);
            config.setMaxAge(3600L);
            return config;
        };
    }
}

结果分析:每次跨域请求到达时,CorsFilter 调用自定义 CorsConfigurationSource,根据 X-Tenant-ID 查询数据库获取该租户白名单。只有白名单内的域名才会被写入 Access-Control-Allow-Origin。新增客户域名只需在管理后台配置,无需修改代码或重启服务。若攻击者使用伪造 Origin 或未知域名,返回的 CorsConfiguration 不包含该 Origin,浏览器拒绝响应。


易错场景:允许 Credentials 时 Origin 不能为 *

面试高频考点:配置 allowCredentials(true) 时,如果同时设置 allowedOrigins(Collections.singletonList("*")),会导致 CORS 配置冲突,浏览器仍会报错。

错误场景还原:

@Bean
public CorsConfigurationSource brokenCorsSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(Collections.singletonList("*")); // ❌ 通配符
    config.setAllowedMethods(Arrays.asList("GET", "POST"));
    config.setAllowCredentials(true); // ❌ 同时允许凭证
    // ...
}

浏览器报错:The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

原因分析:W3C CORS 规范明确规定,当请求携带凭证(Cookie / Authorization Header)时,服务器返回的 Access-Control-Allow-Origin 必须是具体的 Origin 值,不能是通配符 *。这是浏览器的安全限制,防止凭证泄露给任意域。

正确做法:必须显式列出具体域名,或使用 setAllowedOriginPatterns(Spring 5.3+ 支持模式匹配,但仍不能为 * 搭配 Credentials):

config.setAllowedOrigins(Arrays.asList("https://app.example.com", "https://admin.example.com"));
config.setAllowCredentials(true); // ✅ 具体 Origin 与 Credentials 搭配

面试追问:如果确实有任意域名访问需求且需要 Cookie,怎么办?

答:这在安全上是不允许的。若业务确实需要,应放弃 Cookie 凭证机制,改用无状态 Token 认证(如 JWT 放在 Header),此时可以关闭 allowCredentials(false) 并使用 allowedOrigins("*")。但这样等同于放弃 CORS 的凭证安全屏障,需确保 Token 本身有足够的防盗用措施。


核心类速查

类说明
CorsFilter核心过滤器,拦截 CORS 请求并写入响应头
CorsConfigurationCORS 配置对象,封装 Origin / Methods / Headers / Credentials 等规则
CorsConfigurationSource配置源接口,按请求返回对应的 CorsConfiguration
UrlBasedCorsConfigurationSource基于 URL 路径映射的配置源实现
CorsRegistrySpring MVC 层简化配置入口,与 WebMvcConfigurer 配合使用
上一页
CSRF 跨站请求伪造防护
下一页
Clickjacking 点击劫持防护