23. Spring Security Web 安全纵深防御

从 CSRF、CORS 到 XSS 防护,从 Content Security Policy 到安全响应头,构建 Java Web 应用纵深防御体系

Web 安全是应用安全的基石。OWASP Top 10 中绝大多数漏洞与 Web 层密切相关。Spring Security 提供了丰富的安全功能,但默认配置往往不够——需要针对具体场景进行纵深防御配置。

1. 安全响应头(Security Headers)

1.1 核心响应头速查

响应头用途风险
Content-Security-Policy限制资源加载来源XSS、数据注入
X-Frame-Options防止页面被嵌入 iframeClickjacking
X-Content-Type-Options禁止 MIME 嗅探文件类型混淆攻击
Referrer-Policy控制 Referrer 信息敏感 URL 泄露
Permissions-Policy限制浏览器 API摄像头/位置等隐私
Strict-Transport-Security强制 HTTPSSSL Strip 攻击

1.2 Spring Security 配置

@Configuration
@EnableWebSecurity
public class SecurityHeadersConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .headers(headers -> headers
                // Content Security Policy
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives(
                        "default-src 'self'; " +
                        "script-src 'self' 'unsafe-inline' https://cdn.example.com; " +
                        "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
                        "img-src 'self' data: https://images.example.com; " +
                        "font-src 'self' https://fonts.gstatic.com; " +
                        "connect-src 'self' https://api.example.com; " +
                        "frame-ancestors 'none'; " +
                        "form-action 'self'; " +
                        "base-uri 'self';"
                    )
                )
                // 不再支持 iframe 嵌入
                .frameOptions(frame -> frame.deny())
                // 禁止 MIME 类型嗅探
                .contentTypeOptions(contentType -> contentType.enable())
                // Referrer 策略
                .referrerPolicy(referrer -> referrer
                    .policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)
                )
                // Permissions Policy
                .permissionsPolicy(permissions -> permissions
                    .policy("camera=(), microphone=(), geolocation=(), payment=()")
                )
                // HSTS
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000)  // 1 year
                    .preload(true)
                )
            );
        return http.build();
    }
}

1.3 报告模式(Report-Only)

// 先使用 report-only 模式,观察 CSP 违规报告,确认无误后再强制启用
http.headers(headers -> headers
    .contentSecurityPolicy(csp -> csp
        .policyDirectives("default-src 'self'; report-uri /csp-report")
        .reportOnly()  // 只报告,不拦截
    )
);

2. CSRF 防护

2.1 CSRF 原理

用户已登录 bank.com(Cookie 中有 session)
  │
  ├── 访问 bank.com/transfer?to=attacker&amount=10000  ← 正常操作
  │
  └── 访问恶意网站 evil.com
         └── 页面自动提交表单:
             <form action="https://bank.com/transfer" method="POST">
               <input name="to" value="attacker">
               <input name="amount" value="10000">
             </form>
             <script>document.forms[0].submit()</script>
         → 浏览器自动带上 bank.com 的 Cookie
         → 请求验证通过(因为 Cookie 是有效的)

2.2 Spring Security CSRF 配置

@Configuration
public class CsrfConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                // SPA/API 场景: 使用 Cookie 存储 Token
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                // 或: Header 存储(前后端分离)
                // .csrfTokenRepository(new HttpSessionCsrfTokenRepository())

                // 忽略某些路径(如 Webhook)
                .ignoringRequestMatchers("/webhook/**", "/api/public/**")

                // 部分请求使用 CSRF
                .requireCsrfProtectionMatcher(
                    new AndRequestMatcher(
                        CsrfFilter.DEFAULT_CSRF_MATCHER,
                        new NegatedRequestMatcher(
                            new AntPathRequestMatcher("/api/**", "GET")
                        )
                    )
                )
            );
        return http.build();
    }
}

2.3 前后端分离的 CSRF

// 后端: 启用 Cookie 传输 CSRF Token
@Bean
public CsrfTokenRepository csrfTokenRepository() {
    CookieCsrfTokenRepository repository = CookieCsrfTokenRepository.withHttpOnlyFalse();
    repository.setCookieName("XSRF-TOKEN");
    repository.setHeaderName("X-XSRF-TOKEN");
    return repository;
}
// 前端 (Axios): 自动从 Cookie 读取并发送到 Header
axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
// 更现代的做法: 如果 API 使用 JWT(无 Cookie Session),可以完全禁用 CSRF
// 因为 JWT 不会自动随请求发送,攻击者无法获取
@Configuration
@Profile("api-only")
public class ApiSecurityConfig {
    @Bean
    public SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
        http
            .securityMatcher("/api/**")
            .csrf(csrf -> csrf.disable())  // API 无 Cookie Session,无需 CSRF
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .oauth2ResourceServer(oauth2 ->
                oauth2.jwt(jwt -> jwt.decoder(jwtDecoder())));
        return http.build();
    }
}

3. CORS 配置

3.1 CORS 原理

浏览器同源策略:
  - 相同: protocol + host + port
  - 不同源访问 → 浏览器拦截

CORS 流程:
  1. 预检请求: OPTIONS /api/data
     Origin: https://app.example.com
     Access-Control-Request-Method: POST
     Access-Control-Request-Headers: X-Custom-Header

  2. 服务器响应:
     Access-Control-Allow-Origin: https://app.example.com
     Access-Control-Allow-Methods: GET, POST, PUT, DELETE
     Access-Control-Allow-Headers: X-Custom-Header
     Access-Control-Max-Age: 3600

  3. 正式请求发送

3.2 Spring Security CORS 配置

@Configuration
public class CorsConfig {

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        // 明确指定允许的源,不要泛化为 *
        configuration.setAllowedOrigins(Arrays.asList(
            "https://app.example.com",
            "https://admin.example.com"
        ));
        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(Arrays.asList(
            "Authorization", "Content-Type", "X-Requested-With", "X-Trace-Id"
        ));
        configuration.setExposedHeaders(Arrays.asList("X-Total-Count", "X-Trace-Id"));
        configuration.setAllowCredentials(true);  // 允许 Cookie
        configuration.setMaxAge(3600L);           // 预检缓存 1 小时

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));
        return http.build();
    }
}

CORS 安全红线

  • ❌ 不要 setAllowedOrigins(Arrays.asList("*")) + setAllowCredentials(true) 同时设置
  • ❌ 不要基于请求的 Origin 动态反射(除非严格校验域名)
  • ✅ 使用明确的域名白名单
  • ✅ 生产环境设置 Vary: Origin

4. XSS 防护

4.1 输入过滤与输出编码

@Component
public class XssFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        XssHttpServletRequestWrapper wrappedRequest =
            new XssHttpServletRequestWrapper((HttpServletRequest) request);
        chain.doFilter(wrappedRequest, response);
    }
}

public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        return value != null ? HtmlUtils.htmlEscape(value) : null;
    }
}

4.2 富文本过滤

@Service
public class HtmlSanitizerService {
    private final PolicyFactory policy = new HtmlPolicyBuilder()
        .allowElements("p", "br", "strong", "em", "u", "h1", "h2", "h3", "ul", "ol", "li", "a")
        .allowAttributes("href").onElements("a")
        .requireRelNofollowOnLinks()
        .toFactory();

    public String sanitize(String html) {
        return policy.sanitize(html);
    }
}

4.3 Spring 自动转义

@Controller
public class CommentController {
    @GetMapping("/comment")
    public String comment(Model model) {
        // Thymeleaf 自动转义: ${comment} → HTML 实体编码
        // 仅明确需要时才使用 th:utext (unescaped text)
        model.addAttribute("comment", userInput);  // 自动安全
        return "comment";
    }
}

5. 会话安全

5.1 会话固定攻击防护

@Configuration
public class SessionSecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .sessionManagement(session -> session
                .sessionFixation().migrateSession()  // 登录后更换 Session ID
                .maximumSessions(1)                   // 单点登录
                .maxSessionsPreventsLogin(false)      // 后登录者踢掉前者
                .expiredUrl("/login?expired")
            );
        return http.build();
    }
}
@Configuration
public class CookieConfig {
    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("SESSION");
        serializer.setCookiePath("/");
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        serializer.setUseSecureCookie(true);      // 仅 HTTPS 传输
        serializer.setSameSite("Strict");          // SameSite 严格模式
        return serializer;
    }
}

6. 安全审计与监控

@Component
public class SecurityAuditListener {
    private static final Logger auditLog = LoggerFactory.getLogger("AUDIT");

    @EventListener
    public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
        auditLog.info("LOGIN_SUCCESS|user={}|ip={}",
            event.getAuthentication().getName(),
            getClientIp());
    }

    @EventListener
    public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
        auditLog.warn("LOGIN_FAILURE|user={}|reason={}|ip={}",
            event.getAuthentication().getName(),
            event.getException().getMessage(),
            getClientIp());
    }
}

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java-enterprise」更多文章

  1. 限流算法深度解析:令牌桶、漏桶与滑动窗口计数
  2. Java 代码质量:SonarQube、Checkstyle 与 SpotBugs 工程化实践
  3. Spring IoC 容器与依赖注入原理深度剖析