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

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

DelegatingPasswordEncoder

定义与作用

DelegatingPasswordEncoder 是 Spring Security 提供的委托式密码编码器,它通过密码前缀 {algorithm} 识别不同的编码算法,将验证任务委派给对应的子编码器。它是系统从旧密码编码迁移到新编码的桥梁方案。

public class DelegatingPasswordEncoder implements PasswordEncoder {
    private final String idForEncode;                    // 编码新密码时使用的默认算法ID
    private final Map<String, PasswordEncoder> idToPasswordEncoder;  // 算法ID → 编码器映射
    private final PasswordEncoder defaultPasswordEncoderForMatches; // 无前缀时的默认编码器
    // ...
}

手写过滤器的痛点

系统升级密码算法时,手写方案通常面临两难:

// 痛点:数据库中既有旧 MD5 密码,又有新 BCrypt 密码
// 方案A:一次性要求所有用户重置密码 → 用户体验极差,流失率高
// 方案B:维护两套登录逻辑,先判断密码长度再分支处理 → 代码混乱,易出错
// 方案C:写一堆正则匹配前缀,自己路由到不同编码器 → 重复造轮子
public boolean checkPassword(String input, String stored) {
    if (stored.startsWith("$2a$")) {
        return bcrypt.matches(input, stored);
    } else if (stored.length() == 32) {
        return md5(input).equals(stored);
    }
    return false;
}

DelegatingPasswordEncoder 通过标准化前缀协议和内置路由逻辑,让多算法共存变得透明。


核心原理

前缀识别与路由机制

支持的算法前缀

前缀编码器说明
{bcrypt}BCryptPasswordEncoder默认编码新密码
{noop}NoOpPasswordEncoder明文,仅测试使用
{pbkdf2}Pbkdf2PasswordEncoderPBKDF2
{scrypt}SCryptPasswordEncoderSCrypt
{sha256}StandardPasswordEncoderSHA-256(已废弃)
{MD5}MessageDigestPasswordEncoderMD5(需手动注册)

工厂方法创建

// 使用 Spring Security 提供的工厂方法,默认以 BCrypt 编码新密码
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

// 手动构建(自定义算法组合)
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder(12));
encoders.put("bcrypt10", new BCryptPasswordEncoder(10));
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());

DelegatingPasswordEncoder delegatingEncoder =
    new DelegatingPasswordEncoder("bcrypt", encoders);

示例一:从 SHA256 平滑迁移到 BCrypt

场景说明

旧系统使用 SHA256 编码密码,现需迁移到 BCrypt。要求在迁移期间,所有用户都能正常登录,无需强制重置密码。

操作前配置

数据库中只有 SHA256 哈希,无算法前缀:

-- 旧数据格式(无前缀,系统默认知道是 SHA256)
SELECT username, password FROM users;
-- admin | 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8

旧配置只支持一种编码器:

@Bean
public PasswordEncoder passwordEncoder() {
    return new MessageDigestPasswordEncoder("SHA-256");  // 无法处理 BCrypt
}

操作后配置

步骤1:为数据库中所有旧密码添加 {SHA-256} 前缀

UPDATE users SET password = CONCAT('{SHA-256}', password)
WHERE password NOT LIKE '{%}';

步骤2:配置 DelegatingPasswordEncoder 支持两种算法

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));

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

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);
        provider.setUserDetailsPasswordService((UserDetailsPasswordService) userDetailsService);
        auth.authenticationProvider(provider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorize -> authorize
                .antMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
    }
}
@Service
public class MigrationUserDetailsService 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())  // 格式:{SHA-256}xxx 或 {bcrypt}xxx
            .roles("USER")
            .build();
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        userMapper.updatePassword(user.getUsername(), newPassword);
        return User.builder()
            .username(user.getUsername())
            .password(newPassword)
            .authorities(user.getAuthorities())
            .build();
    }
}

结果分析

  • 旧用户登录时,密码前缀为 {SHA-256},DelegatingPasswordEncoder 路由到 SHA-256 编码器验证通过
  • 验证成功后,upgradeEncoding() 发现当前默认是 {bcrypt},旧密码需要升级
  • DaoAuthenticationProvider 调用 updatePassword(),将密码重新编码为 {bcrypt}$2a$10$...
  • 新注册用户直接以 {bcrypt} 格式存储,无需经历迁移
  • 随着时间推移,所有活跃用户逐步完成密码升级,旧算法可以最终下线

示例二:多系统合并后的密码兼容

场景说明

公司并购后,两个系统的用户库合并。系统A使用 BCrypt,系统B使用 PBKDF2,合并后需要统一认证入口。

操作前配置

两个系统各自维护自己的编码器,用户需要分别登录:

// 系统A
@Bean
public PasswordEncoder encoderA() { return new BCryptPasswordEncoder(); }

// 系统B
@Bean
public PasswordEncoder encoderB() { return new Pbkdf2PasswordEncoder(); }

操作后配置

统一使用 DelegatingPasswordEncoder 兼容两种格式:

@Bean
public PasswordEncoder passwordEncoder() {
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put("bcrypt", new BCryptPasswordEncoder());
    encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
    encoders.put("pbkdf2_sha256", new Pbkdf2PasswordEncoder("sha256", 185000, 256));

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

数据迁移脚本:

-- 为系统A用户添加前缀(如原本已无前缀,默认视为 BCrypt)
UPDATE users SET password = CONCAT('{bcrypt}', password)
WHERE source_system = 'A' AND password NOT LIKE '{%}';

-- 为系统B用户添加前缀
UPDATE users SET password = CONCAT('{pbkdf2}', password)
WHERE source_system = 'B' AND password NOT LIKE '{%}';

UserDetailsService 统一查询:

@Service
public class MergedUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) {
        UserEntity entity = userMapper.findByUsername(username);
        if (entity == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        return User.builder()
            .username(entity.getUsername())
            .password(entity.getPassword())  // 自动携带 {bcrypt} 或 {pbkdf2} 前缀
            .roles(entity.getRole().split(","))
            .build();
    }
}

结果分析

  • 合并后的用户库中,两种密码格式共存,前缀自动区分来源
  • DelegatingPasswordEncoder 根据前缀路由到正确的编码器,单一认证入口处理所有用户
  • 新用户统一使用 {bcrypt} 注册,长期目标是将 PBKDF2 用户逐步迁移

易错场景:忘记给旧密码添加前缀

问题描述

迁移时直接切换为 DelegatingPasswordEncoder,但数据库中的旧密码没有 {algorithm} 前缀,导致所有旧用户登录失败。

错误配置

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

数据库中的旧密码:

-- 无前缀!DelegatingPasswordEncoder 无法识别
SELECT password FROM users WHERE username = 'alice';
-- 返回: $2a$10$xxxxxxxx...

原因分析

PasswordEncoderFactories.createDelegatingPasswordEncoder() 默认的新编码格式是 {bcrypt}...。当密码没有前缀时,DelegatingPasswordEncoder 尝试查找空字符串 id,找不到对应的编码器,直接抛出 IllegalArgumentException。

正确做法

迁移时必须为历史密码补充前缀:

-- 假设旧密码全部是 BCrypt 格式(以 $2a$ 开头)
UPDATE users SET password = CONCAT('{bcrypt}', password)
WHERE password LIKE '$2a$%';

或者在创建 DelegatingPasswordEncoder 时指定一个默认编码器用于处理无前缀密码:

Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());

// 用 BCrypt 作为默认编码器,处理没有前缀的密码
DelegatingPasswordEncoder delegating = new DelegatingPasswordEncoder("bcrypt", encoders);
// 但这样无法区分不同旧算法,仅适用于所有旧密码都是同一种算法的情况

面试考点:

  • DelegatingPasswordEncoder 的密码前缀格式是什么?如何提取和路由?
  • 为什么 PasswordEncoderFactories.createDelegatingPasswordEncoder() 默认使用 BCrypt?
  • 密码迁移期间,旧密码无前缀怎么办?
  • upgradeEncoding() 在什么条件下返回 true?DaoAuthenticationProvider 如何触发自动升级?
上一页
BCryptPasswordEncoder
下一页
表单登录