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

    • 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
    • 最佳实践

最佳实践

定义与作用

Spring Security 最佳实践是一套面向生产环境的配置准则与检查清单,旨在通过纵深防御(Defense in Depth)策略,将安全漏洞的发生概率和攻击影响面降至最低。它不局限于框架本身的 API 使用,而是涵盖密码策略、传输层安全、权限最小化、日志审计、安全响应头五大维度,确保应用在部署到生产环境后具备抵御常见 Web 攻击(如凭证泄露、中间人攻击、权限提升、会话劫持、点击劫持)的能力。

本清单的核心原则是:框架默认配置已经提供了良好的基础,但生产环境必须根据业务场景进行显式加固,绝不可依赖默认值。


核心原理

生产级安全配置遵循 "最小权限 + 显式声明 + 纵深防御" 三位一体的设计思想:

  1. 密码策略:使用自适应哈希算法(BCrypt/Argon2)存储密码,禁止明文和可逆加密,通过 DelegatingPasswordEncoder 支持平滑算法迁移;
  2. 传输层安全:强制 HTTPS(HSTS),禁止敏感流量在明文通道传输,防止中间人攻击和会话 Cookie 窃取;
  3. 最小权限:URL 授权与方法级别授权双重校验,角色层级设计避免权限膨胀,所有未明确放行的请求默认拒绝;
  4. 日志审计:记录认证失败、授权拒绝、异常访问模式,保留足够字段(时间戳、源 IP、目标资源、操作结果)用于事后溯源;
  5. 安全响应头:通过 HeaderWriterFilter 写入 X-Frame-Options、Content-Security-Policy、Strict-Transport-Security 等头,从浏览器层削弱 XSS、Clickjacking、MIME 嗅探等攻击面。

生产级安全配置检查清单图


示例一:从弱密码策略到生产级 BCrypt 升级

场景说明

项目在开发阶段为简化调试使用 {noop} 明文存储密码,计划上线前必须升级为生产级密码策略。目标是:① 新密码使用 BCrypt 编码;② 存量明文密码平滑迁移,不影响已有用户登录;③ 禁止任何配置回退到明文。

操作前配置(开发阶段,不可用于生产)

@Configuration
@EnableWebSecurity
public class DevSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("alice")
            .password("{noop}alice123") // 明文,仅开发调试
            .roles("USER")
            .and()
            .withUser("bob")
            .password("{noop}bob123")
            .roles("ADMIN");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance(); // 严重安全隐患
    }
}

操作后配置(生产级)

@Configuration
@EnableWebSecurity
public class ProdSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(auth -> auth
                .antMatchers("/public/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().denyAll() // 未明确匹配的路径默认拒绝
            )
            .formLogin(Customizer.withDefaults())
            .csrf().and()
            .sessionManagement(session -> session
                .sessionFixation().migrateSession()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true)
            );
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("alice")
            .password("{bcrypt}$2a$10$N9qo8uLOickgx2ZMRZoMy.MqrjE5z1LkV5yXJ3yX7QZ5J3QZ5J3QZ") // 已迁移的密文
            .roles("USER")
            .and()
            .withUser("bob")
            .password("{bcrypt}$2a$10$X5J3QZ5J3QZ5J3QZ5J3QZ5J3QZ5J3QZ5J3QZ5J3QZ5J3QZ5J3QZ5J3")
            .roles("ADMIN");
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 默认使用 BCrypt 编码新密码,同时支持 {noop} {bcrypt} {pbkdf2} 前缀验证旧密码
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

结果分析

  • PasswordEncoderFactories.createDelegatingPasswordEncoder() 返回的 DelegatingPasswordEncoder 默认使用 BCryptPasswordEncoder 编码新密码,同时通过密码前缀识别存量密码的编码方式。前缀为 {noop} 的明文密码仍能被验证,但新注册用户的密码将以 {bcrypt} 前缀存储。
  • 在生产上线前,应通过脚本将数据库中所有 {noop} 前缀的密码强制要求用户重置,或在用户首次登录时通过 UserDetailsPasswordService 自动升级为 BCrypt。绝对禁止在 application.properties 或配置类中保留 NoOpPasswordEncoder 的 Bean 定义。
  • anyRequest().denyAll() 是安全配置的兜底原则:任何未被显式匹配规则覆盖的请求,默认拒绝访问,防止因路径遗漏导致未授权访问。

示例二:安全响应头从零散配置到统一策略

场景说明

应用最初未显式配置安全响应头,依赖 Spring Security 的默认值。安全审计发现:① 需要更严格的 CSP 策略防止 XSS 加载外部脚本;② 需要允许同域 iframe 嵌入但禁止跨域嵌套;③ 需要 HSTS 强制浏览器始终使用 HTTPS 访问。

操作前配置(默认/松散)

@Configuration
@EnableWebSecurity
public class LooseHeaderConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults())
            .headers(); // 默认仅写入 X-Frame-Options: DENY 等基础头,CSP 未配置
    }
}

操作后配置(生产级安全头策略)

@Configuration
@EnableWebSecurity
public class StrictHeaderConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(auth -> auth
                .antMatchers("/public/**").permitAll()
                .antMatchers("/api/**").authenticated()
                .anyRequest().denyAll()
            )
            .formLogin(Customizer.withDefaults())
            .csrf().and()
            .headers(headers -> headers
                .frameOptions(frame -> frame.sameOrigin()) // 仅允许同源 iframe 嵌入
                .contentTypeOptions() // 启用 X-Content-Type-Options: nosniff
                .xssProtection(xss -> xss.disable()) // 禁用旧版 X-XSS-Protection(现代浏览器依赖 CSP)
                .contentSecurityPolicy(csp -> csp
                    .policyDirectives(
                        "default-src 'self'; " +
                        "script-src 'self' https://cdn.example.com; " +
                        "style-src 'self' https://cdn.example.com 'unsafe-inline'; " +
                        "img-src 'self' data: https://cdn.example.com; " +
                        "frame-ancestors 'self'; " +
                        "base-uri 'self'; " +
                        "form-action 'self'"
                    )
                )
                .httpStrictTransportSecurity(hsts -> hsts
                    .includeSubDomains(true)
                    .maxAgeInSeconds(31536000) // 1 年
                    .preload(true)
                )
                .cacheControl(cache -> cache.disable()) // 或根据业务定制,禁止缓存敏感页面
            )
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .sessionFixation().migrateSession()
            );
    }
}

结果分析

  • X-Frame-Options: SAMEORIGIN 替代默认的 DENY,允许同域 iframe 嵌入(适用于需要内嵌自身页面的场景),同时阻止跨域 Clickjacking 攻击。
  • Content-Security-Policy 通过 default-src 'self' 将默认资源加载限制在同源,通过 script-src 和 style-src 白名单控制外部 CDN。frame-ancestors 'self' 是 CSP 层面对 iframe 嵌套的二次防护,与 X-Frame-Options 形成互补。
  • Strict-Transport-Security 头告知浏览器在接下来 1 年内始终通过 HTTPS 访问当前域名及其子域,防止 SSL 剥离攻击。preload(true) 表示申请加入浏览器厂商的 HSTS 预加载列表。
  • 默认的 X-XSS-Protection: 1; mode=block 已被现代浏览器废弃,且可能引入新的副作用(如阻止合法响应)。推荐显式禁用,将 XSS 防御完全交给 CSP 和输入输出编码。

易错场景:生产环境禁用 CSRF 与开放端点的双重陷阱

现象

开发者为 "简化前后端分离项目的对接",在配置中做了如下两个改动:

@Configuration
@EnableWebSecurity
public class DangerousConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(auth -> auth
                .antMatchers("/api/**").permitAll() // 陷阱一:开放所有 API 路径
                .anyRequest().authenticated()
            )
            .csrf().disable() // 陷阱二:全局禁用 CSRF
            .formLogin(Customizer.withDefaults());
    }
}

根因分析

陷阱一:/api/** 的 permitAll() 过于宽泛。若 /api/users 是修改用户信息的接口,攻击者无需任何认证即可调用。正确的做法是:即便公开 API,也应区分读操作与写操作,写操作必须要求认证或 API Token。

// 错误的宽泛授权
.antMatchers("/api/**").permitAll()

// 正确的精细化授权
.antMatchers(HttpMethod.GET, "/api/public/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/orders/**").hasRole("USER")
.antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
.anyRequest().denyAll()

陷阱二:全局禁用 CSRF。在 Spring Security 5.x 中,csrf().disable() 应在极小范围内使用,例如纯无状态 JWT API(通过 Bearer Token 认证,Cookie 不参与认证)。如果应用中仍存在基于 Session 的表单登录或 Cookie 认证,全局禁用 CSRF 意味着任何恶意网站都可以诱导已登录用户发起 POST/PUT/DELETE 请求。

// 错误:全局禁用
http.csrf().disable();

// 正确:仅对特定路径禁用,其余路径保持防护
http.csrf(csrf -> csrf
    .ignoringAntMatchers("/api/jwt/**") // 无状态 JWT 路径忽略 CSRF
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    // SPA 前端从 Cookie 读取 Token 并写入 Header
);

正确做法

生产环境中的安全配置必须遵循 "默认拒绝,最小开放" 原则:

  1. 每新增一条 antMatchers(...).permitAll() 都必须经过安全评审,确认该路径下不存在任何敏感操作;
  2. 只有在完全无状态、不依赖 Cookie 或 Session 的 API 中,才考虑忽略 CSRF,且应使用 ignoringAntMatchers 做局部排除而非全局禁用;
  3. 写操作(POST / PUT / DELETE / PATCH)必须关联认证与授权规则,禁止默认放行。

面试考点

Q:生产环境中为什么推荐使用 DelegatingPasswordEncoder 而不是直接使用 BCryptPasswordEncoder?

BCryptPasswordEncoder 只能处理 BCrypt 格式的密码。实际生产环境常面临算法迁移场景:早期系统可能使用 SHA-256、PBKDF2 或更老的算法存储密码,直接替换编码器会导致所有存量用户无法登录。DelegatingPasswordEncoder 通过密码前缀 {bcrypt}、{pbkdf2}、{sha256} 识别编码算法,验证时自动选择对应的 PasswordEncoder,而编码新密码时默认使用 BCrypt。这实现了 "平滑迁移":存量密码继续可用,新密码逐步升级为更安全的算法,无需强制用户立即重置密码。

Q:Spring Security 的会话固定攻击防护默认是什么策略?为什么推荐保持默认?

默认策略是 migrateSession(),即用户认证成功后更换 Session ID,但保留原 Session 的所有属性。这比 newSession() 更轻量(无需复制属性),又比 none() 安全(防止攻击者先获取 Session ID 再诱骗用户登录)。保持默认的原因在于:Session Fixation 攻击的核心危害是攻击者预先知道用户登录后的 Session ID。migrateSession() 在登录后立即使旧 ID 失效,攻击者持有的预登录 Session ID 无法再关联到已认证的安全上下文。

Q:为什么 Content-Security-Policy 中的 frame-ancestors 比 X-Frame-Options 更推荐使用?

X-Frame-Options 仅支持 DENY、SAMEORIGIN 和 ALLOW-FROM(后者已被废弃且兼容性差),无法精细控制允许嵌套的具体域名。CSP: frame-ancestors 支持 self、特定域名、甚至通配子域,语义更丰富,且与 CSP 的其他指令统一配置。Spring Security 中两者可同时启用,但现代安全审计更关注 frame-ancestors 的完整性。推荐配置为 X-Frame-Options: SAMEORIGIN + CSP frame-ancestors 'self' 的双重防护,兼顾旧浏览器兼容与现代浏览器策略。


版本说明:本教程基于 Spring Security 5.x(对应 Spring Boot 2.x)。WebSecurityConfigurerAdapter、antMatchers、.csrf().disable() 等写法为 5.x 经典风格。生产环境实践中,无论 5.x 还是 6.x,"最小权限 + 默认拒绝 + 纵深防御" 的设计原则始终不变。

上一页
@WithMockUser