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

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

BCryptPasswordEncoder

定义与作用

BCryptPasswordEncoder 是 Spring Security 推荐使用的默认密码编码器,基于 BCrypt 哈希算法实现。BCrypt 算法由 Niels Provos 和 David Mazières 设计,基于 Blowfish 加密算法,核心特点是内置随机盐值和自适应计算强度。

public class BCryptPasswordEncoder implements PasswordEncoder {
    private final int strength;        // 计算强度(默认10,范围4-31)
    private final SecureRandom random; // 随机数生成器
    // ...
}

手写过滤器的痛点

开发者手写密码哈希时,常见的错误包括:

// 痛点1:使用 MD5/SHA1,无盐,彩虹表秒破
String hash = DigestUtils.md5Hex(password);  // 几分钟就能反查

// 痛点2:盐值硬编码或全局共享,失去意义
String salt = "myapp_salt";  // 所有用户用同一个盐,泄露后一锅端
String hash = DigestUtils.sha256Hex(password + salt);

// 痛点3:计算强度固定,无法随硬件升级而增强
// 今天的安全强度,明年 GPU 性能翻倍后就不安全了

// 痛点4:没有标准格式,盐值和哈希分开存储,容易丢失或混淆

BCryptPasswordEncoder 一次性解决了以上所有问题:自动盐值、强度可调、标准格式、与 Spring Security 认证流程无缝集成。


核心原理

BCrypt 哈希格式解析

典型输出示例:

$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
\__/\_/\_____________________\_____________________________/
  |  |           |                        |
算法 强度      22位盐值                  31位哈希值

计算强度(Cost Factor)

strength 参数决定哈希计算次数为 2^strength:

strength计算轮数单次哈希耗时(参考)适用场景
416< 1ms仅测试
101,024~50-100ms默认,推荐
124,096~200-400ms高安全场景
1416,384~800ms-1.5s极高安全场景
312,147,483,648极长不建议,用户体验极差
// 默认强度 10
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();  // strength=10

// 自定义强度 12
BCryptPasswordEncoder strongEncoder = new BCryptPasswordEncoder(12);

为什么抗暴力破解?

  1. 随机盐:每次哈希生成不同盐值,相同密码的结果完全不同,无法批量比对彩虹表
  2. 慢计算:2^10 = 1024 轮故意放慢哈希速度,使 GPU/ASIC 暴力破解成本剧增
  3. 自适应:strength 可随硬件性能提升而增大,保持安全裕度

示例一:配置不同强度的 BCrypt 编码器

场景说明

普通用户使用默认强度(10),管理员账户使用更高强度(12),体现安全分级。

操作前配置

所有用户共用同一种编码器,无法区分安全级别:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();  // 强度10,全员一致
}

操作后配置

为普通用户和管理员分别配置:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean(name = "userPasswordEncoder")
    public PasswordEncoder userPasswordEncoder() {
        return new BCryptPasswordEncoder(10);  // 默认强度
    }

    @Bean(name = "adminPasswordEncoder")
    public PasswordEncoder adminPasswordEncoder() {
        return new BCryptPasswordEncoder(12);  // 更高强度
    }

    @Autowired
    @Qualifier("userPasswordEncoder")
    private PasswordEncoder userPasswordEncoder;

    @Autowired
    @Qualifier("adminPasswordEncoder")
    private PasswordEncoder adminPasswordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 普通用户 DAO
        DaoAuthenticationProvider userProvider = new DaoAuthenticationProvider();
        userProvider.setUserDetailsService(userDetailsService);
        userProvider.setPasswordEncoder(userPasswordEncoder);

        // 管理员 DAO
        DaoAuthenticationProvider adminProvider = new DaoAuthenticationProvider();
        adminProvider.setUserDetailsService(adminDetailsService);
        adminProvider.setPasswordEncoder(adminPasswordEncoder);

        auth.authenticationProvider(userProvider);
        auth.authenticationProvider(adminProvider);
    }
}
@Service
public class AdminDetailsService implements UserDetailsService {

    @Autowired
    private AdminMapper adminMapper;

    @Autowired
    @Qualifier("adminPasswordEncoder")
    private PasswordEncoder adminPasswordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) {
        AdminEntity admin = adminMapper.findByUsername(username);
        return User.builder()
            .username(admin.getUsername())
            .password(admin.getPassword())  // 数据库中已用强度12编码
            .roles("ADMIN")
            .build();
    }
}

结果分析

  • 管理员密码哈希强度更高,暴力破解成本是普通用户的约 4 倍(2^12 / 2^10 = 4)
  • 即使数据库泄露,攻击者优先破解普通用户账户,管理员账户有更长的安全窗口期

示例二:BCrypt 编码器与密码自动升级

场景说明

系统早期使用 BCryptPasswordEncoder(10),现安全策略要求升级到 BCryptPasswordEncoder(12)。需要让用户在下次登录时自动重新哈希密码。

操作前配置

旧编码器强度为 10:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(10);
}

数据库中存储的旧密码:

$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

操作后配置

切换为 DelegatingPasswordEncoder 管理两种 BCrypt 强度,并启用自动升级:

@Bean
public PasswordEncoder passwordEncoder() {
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put("bcrypt", new BCryptPasswordEncoder(12));        // 新默认
    encoders.put("bcrypt10", new BCryptPasswordEncoder(10));        // 兼容旧密码

    return new DelegatingPasswordEncoder("bcrypt", encoders);
}

数据库中的密码格式:

-- 旧用户(强度10)
UPDATE users SET password = '{bcrypt10}$2a$10$...' WHERE created_at < '2024-01-01';

-- 新用户(强度12)
INSERT INTO users (username, password) VALUES ('new_user', '{bcrypt}$2a$12$...');

UserDetailsService 返回带前缀的密码:

@Service
public class UpgradeUserDetailsService implements UserDetailsService, UserDetailsPasswordService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) {
        UserEntity entity = userMapper.findByUsername(username);
        return User.builder()
            .username(entity.getUsername())
            .password(entity.getPassword())  // 含 {bcrypt10} 或 {bcrypt} 前缀
            .roles("USER")
            .build();
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        // 认证成功后,DelegatingPasswordEncoder 检测到旧强度,自动调用此方法
        userMapper.updatePassword(user.getUsername(), newPassword);
        return User.builder()
            .username(user.getUsername())
            .password(newPassword)  // 新的 {bcrypt}$2a$12$... 格式
            .authorities(user.getAuthorities())
            .build();
    }
}

结果分析

  • 旧用户登录时,DelegatingPasswordEncoder 识别 {bcrypt10} 前缀,使用强度10 的编码器验证通过
  • upgradeEncoding() 检测到当前默认是 bcrypt(强度12),旧密码需要升级
  • DaoAuthenticationProvider 自动调用 updatePassword(),将密码重新哈希为 {bcrypt}$2a$12$...
  • 用户无感知完成密码升级,无需强制重置密码

易错场景:BCrypt 最大密码长度截断

问题描述

用户设置了 80 位的超长密码,发现系统只识别前 72 字节,后面的字符不影响验证结果。

原因分析

BCrypt 算法的底层限制:输入明文被截断为 72 字节。这是算法本身的特性,不是 Spring Security 的 bug。

// 以下两个密码在 BCrypt 看来是一样的(如果前72字节相同)
String pwd1 = "a".repeat(72) + "secret_suffix";
String pwd2 = "a".repeat(72) + "different_suffix";

String hash = encoder.encode(pwd1);
System.out.println(encoder.matches(pwd2, hash));  // true!因为后半截被忽略了

正确做法

方式一:在应用层限制密码长度不超过 72 字节(推荐)

public void validatePassword(String password) {
    if (password.getBytes(StandardCharsets.UTF_8).length > 72) {
        throw new IllegalArgumentException("密码长度超过 72 字节限制");
    }
}

方式二:超长密码先做 SHA-256 预处理,再传给 BCrypt(但改变了标准流程,需谨慎)

// 不推荐,除非有明确需求
String preHashed = DigestUtils.sha256Hex(password);
String bcryptHash = bcryptEncoder.encode(preHashed);

面试考点:

  • BCrypt 哈希中的 $2a$10$ 各段含义?
  • 为什么 strength 增加 1,计算时间就翻倍?
  • BCrypt 的 72 字节限制是什么?对密码策略有何影响?
  • 为什么 BCrypt 比 MD5/SHA256 更适合存储密码?(盐值、慢计算、自适应)
上一页
PasswordEncoder
下一页
DelegatingPasswordEncoder