33-Spring Security CSRF与CORS详解

4 阅读5分钟

Spring Security CSRF与CORS详解

一、知识概述

CSRF(跨站请求伪造)和 CORS(跨源资源共享)是 Web 安全中的两个重要概念。Spring Security 提供了对这两种安全机制的完善支持,帮助开发者构建安全的 Web 应用。

CSRF 防护和 CORS 配置的核心概念:

  • CSRF:防止恶意网站冒充用户发起请求
  • CORS:控制浏览器跨域访问权限
  • 同源策略:浏览器安全限制
  • 预检请求:CORS 的 OPTIONS 请求

理解 CSRF 和 CORS 的原理,是构建安全 Web 应用的必要知识。

二、知识点详细讲解

2.1 CSRF 原理

正常请求:
用户 → 浏览器 → 目标网站(已登录)→ 执行操作

CSRF 攻击:
用户 → 恶意网站 → 浏览器(携带目标网站 Cookie)→ 目标网站 → 执行恶意操作
CSRF 防护原理
1. 服务端生成 CSRF Token
2. Token 嵌入表单或响应头
3. 用户提交请求时携带 Token
4. 服务端验证 Token 有效性
5. Token 不匹配则拒绝请求

2.2 CORS 原理

浏览器同源策略:
- 相同协议(http/https)
- 相同域名
- 相同端口

跨域请求:
前端(http://localhost:3000)→ 后端(http://localhost:8080)
CORS 流程
简单请求:
1. 浏览器发送请求,携带 Origin 头
2. 服务端检查 Origin,返回 Access-Control-Allow-Origin
3. 浏览器检查响应头,决定是否允许

预检请求:
1. 浏览器发送 OPTIONS 预检请求
2. 服务端返回允许的方法、头等
3. 浏览器发送实际请求

2.3 简单请求 vs 预检请求

简单请求条件
  • 方法:GET、POST、HEAD
  • 头:Accept、Accept-Language、Content-Language、Content-Type
  • Content-Type:text/plain、multipart/form-data、application/x-www-form-urlencoded
预检请求触发条件
  • 方法:PUT、DELETE、PATCH
  • 自定义请求头
  • Content-Type:application/json

三、代码示例

3.1 CSRF 防护配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.*;

@Configuration
@EnableWebSecurity
public class CsrfConfig {
    
    // 启用 CSRF 防护(默认启用)
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                // 使用 Cookie 存储 CSRF Token
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            );
        
        return http.build();
    }
    
    // 禁用 CSRF(仅用于无状态 API)
    @Bean
    public SecurityFilterChain disableCsrf(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable());
        
        return http.build();
    }
    
    // 自定义 CSRF 配置
    @Bean
    public SecurityFilterChain customCsrf(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(csrfTokenRepository())
                .ignoringRequestMatchers("/api/public/**")
                .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
            );
        
        return http.build();
    }
    
    @Bean
    public CsrfTokenRepository csrfTokenRepository() {
        CookieCsrfTokenRepository repository = new CookieCsrfTokenRepository();
        repository.setCookieName("XSRF-TOKEN");
        repository.setHeaderName("X-XSRF-TOKEN");
        repository.setParameterName("_csrf");
        return repository;
    }
}

3.2 前端 CSRF Token 处理

<!-- Thymeleaf 表单 -->
<form th:action="@{/transfer}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    <!-- 其他表单字段 -->
    <button type="submit">提交</button>
</form>
// JavaScript 获取 CSRF Token
function getCsrfToken() {
    const cookieValue = document.cookie
        .split('; ')
        .find(row => row.startsWith('XSRF-TOKEN='))
        ?.split('=')[1];
    
    return decodeURIComponent(cookieValue);
}

// 发送请求时携带 Token
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-XSRF-TOKEN': getCsrfToken()
    },
    body: JSON.stringify(data)
});

// Axios 配置
axios.defaults.headers.common['X-XSRF-TOKEN'] = getCsrfToken();

// 或使用拦截器
axios.interceptors.request.use(config => {
    config.headers['X-XSRF-TOKEN'] = getCsrfToken();
    return config;
});

3.3 CORS 配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.*;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
    
    // 方式1:全局 CORS 配置
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        
        // 允许的源
        configuration.setAllowedOriginPatterns(Arrays.asList(
            "http://localhost:*",
            "https://*.example.com"
        ));
        
        // 允许的方法
        configuration.setAllowedMethods(Arrays.asList(
            "GET", "POST", "PUT", "DELETE", "OPTIONS"
        ));
        
        // 允许的头
        configuration.setAllowedHeaders(Arrays.asList("*"));
        
        // 允许携带凭证
        configuration.setAllowCredentials(true);
        
        // 预检请求缓存时间
        configuration.setMaxAge(3600L);
        
        // 暴露的响应头
        configuration.setExposedHeaders(Arrays.asList(
            "Authorization", "X-Total-Count"
        ));
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        
        return source;
    }
    
    // 方式2:使用 CorsFilter
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOriginPattern("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        
        return new CorsFilter(source);
    }
}

3.4 Spring Security 集成 CORS

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityCorsConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 启用 CORS
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            
            // 其他配置
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );
        
        return http.build();
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        
        return source;
    }
}

3.5 控制器级别 CORS

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
@CrossOrigin(
    origins = "http://localhost:3000",
    methods = {RequestMethod.GET, RequestMethod.POST},
    allowedHeaders = "*",
    allowCredentials = "true",
    maxAge = 3600
)
public class ApiController {
    
    @GetMapping("/users")
    public List<User> getUsers() {
        return userService.findAll();
    }
    
    // 方法级别覆盖
    @PostMapping("/users")
    @CrossOrigin(origins = "http://localhost:3000")
    public User createUser(@RequestBody UserDTO dto) {
        return userService.create(dto);
    }
}

3.6 自定义 CORS 处理

import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;

@Component
public class CustomCorsFilter implements Filter {
    
    @Override
    public void doFilter(
            ServletRequest request, 
            ServletResponse response, 
            FilterChain chain) 
            throws IOException, ServletException {
        
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        
        String origin = httpRequest.getHeader("Origin");
        
        // 设置 CORS 头
        httpResponse.setHeader("Access-Control-Allow-Origin", 
            getAllowedOrigin(origin));
        httpResponse.setHeader("Access-Control-Allow-Methods", 
            "GET, POST, PUT, DELETE, OPTIONS");
        httpResponse.setHeader("Access-Control-Allow-Headers", 
            "Authorization, Content-Type");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Max-Age", "3600");
        
        // 处理预检请求
        if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) {
            httpResponse.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        
        chain.doFilter(request, response);
    }
    
    private String getAllowedOrigin(String origin) {
        // 动态验证源
        List<String> allowedOrigins = Arrays.asList(
            "http://localhost:3000",
            "https://example.com"
        );
        
        return allowedOrigins.contains(origin) ? origin : "";
    }
}

3.7 CSRF 异常处理

import org.springframework.security.web.csrf.*;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;

@Component
public class CustomCsrfTokenRepository implements CsrfTokenRepository {
    
    private final CookieCsrfTokenRepository delegate = 
        CookieCsrfTokenRepository.withHttpOnlyFalse();
    
    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        return delegate.generateToken(request);
    }
    
    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, 
                         HttpServletResponse response) {
        delegate.saveToken(token, request, response);
    }
    
    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        CsrfToken token = delegate.loadToken(request);
        
        // 自定义加载逻辑
        if (token == null) {
            // 生成新 Token
            token = generateToken(request);
            saveToken(token, request, null);
        }
        
        return token;
    }
}

3.8 REST API CSRF 处理

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.*;

@Configuration
public class RestApiCsrfConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // REST API 通常禁用 CSRF(使用 Token 认证)
            .csrf(csrf -> csrf.disable())
            
            // 或使用无状态的 CSRF Token
            // .csrf(csrf -> csrf
            //     .csrfTokenRepository(new StatelessCsrfTokenRepository())
            // )
            
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            );
        
        return http.build();
    }
}

// 无状态 CSRF Token 仓库
class StatelessCsrfTokenRepository implements CsrfTokenRepository {
    
    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        String token = UUID.randomUUID().toString();
        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
    }
    
    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, 
                         HttpServletResponse response) {
        // 无状态,不保存
    }
    
    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        String token = request.getHeader("X-CSRF-TOKEN");
        if (token != null) {
            return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
        }
        return null;
    }
}

四、实战应用场景

4.1 前后端分离 CORS 配置

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.*;

@Configuration
public class SpaCorsConfig {
    
    @Value("${app.frontend.url}")
    private String frontendUrl;
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        
        // 前端地址
        configuration.setAllowedOrigins(Arrays.asList(frontendUrl));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", configuration);
        
        return source;
    }
}

4.2 多环境 CORS 配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.*;
import java.util.*;

@Configuration
public class EnvironmentCorsConfig {
    
    @Value("${spring.profiles.active:dev}")
    private String activeProfile;
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        
        // 根据环境配置不同的 CORS 策略
        switch (activeProfile) {
            case "prod":
                // 生产环境:严格的 CORS 策略
                configuration.setAllowedOrigins(Arrays.asList(
                    "https://www.example.com"
                ));
                break;
                
            case "staging":
                // 测试环境
                configuration.setAllowedOriginPatterns(Arrays.asList(
                    "https://*.staging.example.com"
                ));
                break;
                
            default:
                // 开发环境:宽松的 CORS 策略
                configuration.setAllowedOriginPatterns(Arrays.asList("*"));
                break;
        }
        
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        
        return source;
    }
}

五、总结与最佳实践

CSRF 防护策略

场景推荐
传统 Web 应用启用 CSRF
REST API禁用 CSRF(使用 Token 认证)
单页应用使用 Cookie CSRF Token

CORS 配置原则

场景推荐
开发环境允许所有源
生产环境限制具体域名
公开 API不允许凭证

最佳实践

  1. CSRF

    • 敏感操作必须防护
    • Token 绑定会话
    • 关键操作二次验证
  2. CORS

    • 最小权限原则
    • 白名单机制
    • 不要使用 * 允许凭证

CSRF 和 CORS 是 Web 安全的基础,理解其原理并正确配置,能够有效防止跨站攻击和跨域安全问题。