java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Security跨域请求处理

Spring Security跨域请求的处理与配置方案

作者:知远漫谈

在现代 Web 开发中,前后端分离架构已成为主流,前端应用通常运行在不同的域名或端口上,而后端服务则部署在另一个地址,本文将深入探讨 Spring Security 中跨域请求的处理机制、常见问题及多种配置方案,需要的朋友可以参考下

前言

在现代 Web 开发中,前后端分离架构已成为主流。前端应用(如 React、Vue、Angular)通常运行在不同的域名或端口上,而后端服务则部署在另一个地址。这种架构虽然提升了开发效率和系统可维护性,但也引入了一个关键问题:跨域资源共享(CORS, Cross-Origin Resource Sharing)。如果不正确处理 CORS 请求,浏览器出于安全考虑会阻止这些请求,导致前端无法正常调用后端 API。

Spring Security 作为 Java 生态中最主流的安全框架,在保护应用的同时也对 CORS 请求施加了额外限制。因此,如何在启用 Spring Security 的项目中正确配置 CORS 支持,成为开发者必须掌握的核心技能之一。

本文将深入探讨 Spring Security 中跨域请求的处理机制、常见问题及多种配置方案,并通过丰富的代码示例帮助你构建一个既安全又兼容跨域访问的后端服务。

什么是跨域请求?

跨域请求是指浏览器从一个(Origin)向另一个不同源的服务器发起的 HTTP 请求。这里的“源”由协议(scheme)、主机名(host)和端口(port)三部分组成。例如:

根据 同源策略(Same-Origin Policy),浏览器默认禁止页面脚本向非同源地址发起某些类型的请求(尤其是带凭据的请求或非简单请求),以防止 CSRF、XSS 等安全攻击。

但现代 Web 应用往往需要跨域通信。为此,W3C 制定了 CORS 标准,允许服务器通过特定的响应头声明哪些外部源可以访问其资源。

CORS 请求的类型

CORS 请求分为两类:

简单请求(Simple Requests)
满足以下条件的请求被视为简单请求:

对于简单请求,浏览器直接发送请求,并在响应中检查 Access-Control-Allow-Origin 头。

预检请求(Preflight Requests)
不满足上述条件的请求(如使用 PUT、DELETE 方法,或携带自定义头如 AuthorizationX-Requested-With)会被浏览器先发送一个 OPTIONS 请求(即预检请求)到服务器,询问是否允许实际请求。服务器必须正确响应 OPTIONS 请求,浏览器才会发送真正的请求。

Spring Security 与 CORS 的冲突

Spring Security 默认会对所有请求进行安全拦截,包括 OPTIONS 预检请求。然而,预检请求不应携带身份验证信息(如 Cookie、Authorization 头),因为它的目的是探测服务器是否允许后续请求,而非执行业务逻辑。

如果未正确配置 CORS,可能出现以下问题:

因此,必须让 Spring Security 正确放行 OPTIONS 请求,并与 CORS 配置协同工作

Spring 中的 CORS 支持基础

在深入 Spring Security 之前,先了解 Spring Framework 本身对 CORS 的支持。

Spring 提供了三种配置 CORS 的方式:

  1. 全局 CORS 配置(推荐用于统一策略)
  2. 控制器方法级别注解@CrossOrigin,适用于细粒度控制)
  3. 通过 CorsConfigurationSource 编程式配置

方式一:使用@CrossOrigin注解

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    @CrossOrigin(origins = "https://app.example.com")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }

    @PostMapping
    @CrossOrigin(origins = {"https://app.example.com", "https://admin.example.com"})
    public User createUser(@RequestBody User user) {
        return userService.save(user);
    }
}

优点:简单直观,适合个别接口开放跨域。
缺点:难以统一管理,重复代码多。

方式二:全局 CORS 配置(WebMvcConfigurer)

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://app.example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600); // 预检缓存时间(秒)
    }
}

这种方式适用于整个应用的 CORS 策略统一管理,是生产环境推荐做法。

注意:此配置仅对 Spring MVC 控制器生效,不会自动应用于 Spring Security 的过滤器链

Spring Security 中的 CORS 配置方案

现在进入核心部分:如何在 Spring Security 中正确集成 CORS。

关键原则:Spring Security 必须知道并使用你定义的 CORS 配置,尤其是要放行 OPTIONS 请求

方案一:启用 Spring Security 内置的 CORS 支持(推荐)

Spring Security 提供了 .cors() 方法,用于启用 CORS 支持。它会自动查找 Spring 上下文中定义的 CorsConfigurationSource Bean,并将其应用于安全过滤器链。

步骤 1:定义全局 CORS 配置

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        
        // 允许的源(生产环境应避免使用 "*",尤其当 allowCredentials=true 时)
        configuration.setAllowedOriginPatterns(Arrays.asList("https://*.example.com", "http://localhost:*"));
        
        // 允许的 HTTP 方法
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        
        // 允许的请求头
        configuration.setAllowedHeaders(Arrays.asList("*"));
        
        // 是否允许携带凭据(如 Cookie、Authorization 头)
        configuration.setAllowCredentials(true);
        
        // 预检请求缓存时间(秒)
        configuration.setMaxAge(3600L);

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

注意:当 allowCredentials=true 时,allowedOrigins 不能设为 "*",必须明确指定源。因此我们使用 allowedOriginPatterns(Spring 5.3+)来支持通配符模式。

步骤 2:在 Security 配置中启用 CORS

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource())) // 启用 CORS 并指定配置源
            .csrf(csrf -> csrf.disable()) // 前后端分离通常禁用 CSRF(若使用 JWT 等无状态认证)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults()); // 或其他认证方式

        return http.build();
    }

    // 如果已在 CorsConfig 中定义为 @Bean,此处可注入使用
    @Autowired
    private CorsConfigurationSource corsConfigurationSource;
}

或者更简洁地(自动查找 Bean):

http.cors(Customizer.withDefaults());

只要上下文中存在 CorsConfigurationSource 类型的 Bean,Spring Security 会自动使用它。

为什么这样有效?

当启用 .cors() 后,Spring Security 会在过滤器链中添加一个 CorsFilter,该过滤器:

  1. 拦截所有请求(包括 OPTIONS)
  2. 根据 CorsConfigurationSource 判断是否为 CORS 请求
  3. 若是 OPTIONS 预检请求,直接返回 CORS 相关头,不进入后续安全过滤器
  4. 若是实际请求,则继续安全流程,但确保响应包含 CORS 头

这保证了预检请求能被正确处理,而无需绕过安全机制。

方案二:手动添加 CorsFilter(高级控制)

如果你需要更精细的控制(例如不同路径不同 CORS 策略),可以手动注册 CorsFilter

@Configuration
public class CorsFilterConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE) // 确保在 SecurityFilterChain 之前执行
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        
        // API 路径的宽松策略
        CorsConfiguration apiConfig = new CorsConfiguration();
        apiConfig.setAllowedOriginPatterns(Arrays.asList("https://*.example.com"));
        apiConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        apiConfig.setAllowedHeaders(Arrays.asList("*"));
        apiConfig.setAllowCredentials(true);
        apiConfig.setMaxAge(3600L);
        source.registerCorsConfiguration("/api/**", apiConfig);
        
        // 管理后台路径的严格策略
        CorsConfiguration adminConfig = new CorsConfiguration();
        adminConfig.setAllowedOrigins(Arrays.asList("https://admin.example.com"));
        adminConfig.setAllowedMethods(Arrays.asList("GET", "POST"));
        adminConfig.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
        adminConfig.setAllowCredentials(true);
        source.registerCorsConfiguration("/admin/**", adminConfig);

        return new CorsFilter(source);
    }
}

然后在 Security 配置中不再使用 .cors(),因为 CORS 已由独立的 CorsFilter 处理:

http
    .csrf(AbstractHttpConfigurer::disable)
    .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    .authorizeHttpRequests(authz -> authz
        .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 显式放行 OPTIONS
        .requestMatchers("/api/public/**").permitAll()
        .anyRequest().authenticated()
    );

注意:必须显式放行 OPTIONS 请求,否则 Spring Security 会要求认证,导致预检失败。

这种方式的优点是灵活,缺点是配置复杂,容易出错。除非有特殊需求,否则推荐使用方案一

方案三:结合 @CrossOrigin 与 Spring Security

你也可以在控制器使用 @CrossOrigin,同时在 Security 中启用 CORS 支持:

@RestController
@CrossOrigin(origins = "https://app.example.com", allowCredentials = "true")
public class ProductController {
    // ...
}

Security 配置:

http.cors(Customizer.withDefaults());

此时,Spring Security 的 CorsFilter 会优先使用全局配置,但若全局未覆盖某路径,则回退到 @CrossOrigin 的设置。

不过,混合使用可能导致策略不一致,建议统一使用全局配置

常见问题与解决方案

问题 1:预检请求返回 403 Forbidden

原因:Spring Security 拦截了 OPTIONS 请求,要求认证。

解决方案

问题 2:带凭据的请求失败(credentials: include)

错误信息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'.

原因:当请求携带凭据(Cookie、Authorization)时,Access-Control-Allow-Origin 不能为 *,必须是具体源。

解决方案

configuration.setAllowedOriginPatterns(Arrays.asList("https://*.yourdomain.com"));
configuration.setAllowCredentials(true);

问题 3:自定义头(如 Authorization)被拒绝

原因:预检请求中 Access-Control-Request-Headers 包含自定义头,但服务器未在 Access-Control-Allow-Headers 中声明。

解决方案

configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));

问题 4:CORS 头未出现在响应中

排查步骤

  1. 检查是否启用了 Spring Security 的 .cors()
  2. 检查 CorsConfigurationSource Bean 是否被正确加载
  3. 使用浏览器开发者工具查看网络请求,确认是否为预检请求
  4. 确保请求路径匹配 CORS 配置的映射(如 /api/**

生产环境最佳实践

1. 避免使用通配符*(尤其当 allowCredentials=true)

// ❌ 危险!当 allowCredentials=true 时无效
configuration.setAllowedOrigins(Arrays.asList("*"));

// ✅ 安全做法
configuration.setAllowedOriginPatterns(Arrays.asList(
    "https://app.yourcompany.com",
    "https://staging.app.yourcompany.com",
    "http://localhost:*" // 仅用于开发
));

2. 限制允许的方法和头

不要盲目允许所有方法和头:

configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));

3. 设置合理的 maxAge

减少预检请求频率:

configuration.setMaxAge(3600); // 缓存 1 小时

4. 开发与生产环境分离配置

@Profile("dev")
@Configuration
public class DevCorsConfig {
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        // 允许 localhost 所有端口
        config.setAllowedOriginPatterns(Arrays.asList("http://localhost:*"));
    }
}

@Profile("prod")
@Configuration
public class ProdCorsConfig {
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        // 仅允许生产域名
        config.setAllowedOriginPatterns(Arrays.asList("https://app.yourcompany.com"));
    }
}

5. 日志与监控

记录 CORS 相关日志,便于排查:

@Component
public class CorsLoggingFilter extends OncePerRequestFilter {

    private static final Logger log = LoggerFactory.getLogger(CorsLoggingFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        
        if (CorsUtils.isCorsRequest(request)) {
            log.info("CORS request from origin: {}", request.getHeader("Origin"));
        }
        filterChain.doFilter(request, response);
    }
}

并在 Security 配置中注册:

http.addFilterBefore(corsLoggingFilter, CorsFilter.class);

完整示例项目结构

以下是一个典型的配置组合:

// CorsConfig.java
@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOriginPatterns(Arrays.asList(
            "https://*.myapp.com",
            "http://localhost:*"
        ));
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

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

// SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .cors(Customizer.withDefaults()) // 启用 CORS
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/auth/login").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic(withDefaults());

        return http.build();
    }
}

// UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        // 业务逻辑
        return ResponseEntity.ok(userService.findById(id));
    }
}

前端调用示例(JavaScript):

fetch('https://api.myapp.com/api/users/123', {
  method: 'GET',
  credentials: 'include', // 携带 Cookie
  headers: {
    'Authorization': 'Bearer ' + token
  }
})
.then(response => response.json())
.then(data => console.log(data));

CORS 与 CSRF 的关系

在前后端分离架构中,常有人疑惑:是否还需要 CSRF 保护

答案取决于你的认证方式:

Spring Security 默认启用 CSRF,因此在无状态 API 中应显式禁用:

http.csrf(AbstractHttpConfigurer::disable);

可视化:CORS 请求处理流程

理解 CORS 在 Spring Security 中的处理流程至关重要。以下是请求从浏览器到后端的完整路径:

从图中可见,CorsFilter 是第一道关卡,它确保预检请求被快速响应,而实际请求则在通过安全验证后才返回 CORS 头。

高级话题:动态 CORS 配置

有时,允许的源可能存储在数据库中(如多租户 SaaS 应用),需要动态加载。

实现动态 CorsConfigurationSource

@Component
public class DynamicCorsConfigurationSource implements CorsConfigurationSource {

    @Autowired
    private AllowedOriginService allowedOriginService; // 从 DB 加载允许的源

    @Override
    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
        CorsConfiguration config = new CorsConfiguration();
        
        List<String> origins = allowedOriginService.getAllowedOrigins();
        config.setAllowedOriginPatterns(origins);
        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
        
        return config;
    }
}

然后在 Security 配置中引用:

http.cors(cors -> cors.configurationSource(dynamicCorsConfigurationSource));

注意:动态查询可能影响性能,建议缓存结果并设置刷新机制。

测试 CORS 配置

编写集成测试确保 CORS 行为符合预期:

@SpringBootTest
@AutoConfigureTestDatabase
@Import({SecurityConfig.class, CorsConfig.class})
class CorsIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void preflightRequest_shouldReturnCorsHeaders() throws Exception {
        mockMvc.perform(options("/api/users")
                .header("Origin", "https://app.example.com")
                .header("Access-Control-Request-Method", "POST")
                .header("Access-Control-Request-Headers", "Authorization"))
            .andExpect(status().isOk())
            .andExpect(header().string("Access-Control-Allow-Origin", "https://app.example.com"))
            .andExpect(header().string("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS"))
            .andExpect(header().string("Access-Control-Allow-Credentials", "true"));
    }

    @Test
    void actualRequest_shouldIncludeCorsHeaders() throws Exception {
        mockMvc.perform(post("/api/users")
                .header("Origin", "https://app.example.com")
                .header("Authorization", "Bearer dummy-token")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{}"))
            .andExpect(status().isUnauthorized()) // 假设未认证
            .andExpect(header().string("Access-Control-Allow-Origin", "https://app.example.com"));
    }
}

总结与建议

跨域请求在前后端分离架构中不可避免,而 Spring Security 的默认行为会阻碍 CORS 的正常工作。通过本文的详细解析,你应该掌握了以下关键点:

  1. CORS 的基本原理:理解简单请求与预检请求的区别
  2. Spring Security 与 CORS 的集成方式:优先使用 .cors(Customizer.withDefaults()) + CorsConfigurationSource Bean
  3. 生产环境安全实践:避免通配符、限制方法与头、区分环境
  4. 常见问题排查:403 预检失败、凭据请求错误等
  5. 高级场景支持:动态 CORS、细粒度控制

记住:安全与可用性需平衡。开放 CORS 是为了功能实现,但绝不能牺牲安全性。始终遵循最小权限原则,只允许可信源访问你的 API。

最后,保持对标准的关注。CORS 规范虽已稳定,但浏览器实现和安全建议仍在演进。定期查阅权威文档(如 W3C CORS Spec)有助于你做出更明智的设计决策。

以上就是Spring Security跨域请求的处理与配置方案的详细内容,更多关于Spring Security跨域请求处理的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文