Spring Security 源码 - DelegatingPasswordEncoder

简介

DelegatingPasswordEncoder 的中文含义是"委托密码编码器"。其中:

  • Delegating:意为"委托",表示该类将密码的编码和验证操作委托给不同的具体密码编码器。
  • PasswordEncoder:密码编码器,用于对密码进行加密和验证。

因此,DelegatingPasswordEncoder 是一个委托机制的密码编码器,通过它可以动态选择不同的编码器来处理密码的编码与验证。

DelegatingPasswordEncoder 是 Spring Security 提供的一个类,用于处理密码编码和解码的逻辑。它的主要作用是支持多种密码编码器,并能够根据密码的存储格式自动选择使用哪种编码器。这在处理用户密码的存储和验证时非常有用,特别是在系统需要迁移或同时支持不同的编码格式时。

主要特点

  1. 多种编码器支持DelegatingPasswordEncoder 可以配置多个密码编码器,并根据密码前缀来决定使用哪个编码器。例如,你可以同时支持 BCrypt、PBKDF2、SHA 等多种编码算法。
  2. 密码格式识别 :存储密码时,通常会在密码前加上编码算法的标识符(前缀),DelegatingPasswordEncoder 会读取这个前缀并选择相应的编码器进行解码或验证。
  3. 灵活的算法配置 :在配置 DelegatingPasswordEncoder 时,可以指定默认编码器和其他编码器的映射关系,使得系统可以灵活应对密码编码的变化。

示例代码

以下是一个简单的示例,展示如何使用 DelegatingPasswordEncoder

java 复制代码
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import java.util.HashMap;
import java.util.Map;

public class PasswordEncoderExample {
    public static void main(String[] args) {
        // 创建密码编码器映射
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        // 可以添加其他编码器,如 PBKDF2, SCrypt 等

        // 创建 DelegatingPasswordEncoder,并指定默认编码器
        PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder("bcrypt", encoders);

        // 编码密码
        String rawPassword = "mysecretpassword";
        String encodedPassword = passwordEncoder.encode(rawPassword);
        System.out.println("Encoded Password: " + encodedPassword);

        // 验证密码
        boolean isMatch = passwordEncoder.matches(rawPassword, encodedPassword);
        System.out.println("Password Match: " + isMatch);
    }
}

控制台输出:

swift 复制代码
Encoded Password: {bcrypt}$2a$10$aeGTTVo/c8panHFB/KDxE.3iPWK.Znyu5rneraR3ultUDlNA10LpK
Password Match: true

核心方法

构造函数

两个关键参数:

  • idForEncode:它指定了用于编码新密码的默认编码器的 ID
  • idToPasswordEncoder:它是一个 Map,包含各种编码器的映射,键是编码器的 ID,值是相应的 PasswordEncoder 实例。
typescript 复制代码
/**
 * 创建一个新的实例
 * @param idForEncode 用于查找哪种 {@link PasswordEncoder} 将被用于 {@link #encode(CharSequence)} 的 ID
 * @param idToPasswordEncoder 一个 ID 到 {@link PasswordEncoder} 的映射,用于确定
 * 哪种 {@link PasswordEncoder} 应该用于 {@link #matches(CharSequence, String)}
 */
public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
    // 调用另一个构造函数,并传入默认的前缀和后缀
    this(idForEncode, idToPasswordEncoder, DEFAULT_ID_PREFIX, DEFAULT_ID_SUFFIX);
}

/**
 * 创建一个新的实例
 * @param idForEncode 用于查找哪种 {@link PasswordEncoder} 将被用于 {@link #encode(CharSequence)} 的 ID
 * @param idToPasswordEncoder 一个 ID 到 {@link PasswordEncoder} 的映射,用于确定
 * 哪种 {@link PasswordEncoder} 应该用于 {@link #matches(CharSequence, String)}
 * @param idPrefix 表示编码结果中 ID 开始的前缀
 * @param idSuffix 表示编码结果中 ID 结束的后缀
 */
public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder,
        String idPrefix, String idSuffix) {
    // 检查用于编码的ID是否为null
    if (idForEncode == null) {
        throw new IllegalArgumentException("idForEncode cannot be null");
    }
    // 检查前缀是否为null
    if (idPrefix == null) {
        throw new IllegalArgumentException("prefix cannot be null");
    }
    // 检查后缀是否为null或空
    if (idSuffix == null || idSuffix.isEmpty()) {
        throw new IllegalArgumentException("suffix cannot be empty");
    }
    // 确保前缀不包含后缀
    if (idPrefix.contains(idSuffix)) {
        throw new IllegalArgumentException("idPrefix " + idPrefix + " cannot contain idSuffix " + idSuffix);
    }

    // 检查指定的编码ID是否存在于编码器映射中
    if (!idToPasswordEncoder.containsKey(idForEncode)) {
        throw new IllegalArgumentException(
                "idForEncode " + idForEncode + " is not found in idToPasswordEncoder " + idToPasswordEncoder);
    }
    // 遍历所有编码器ID,确保它们不包含前缀或后缀
    for (String id : idToPasswordEncoder.keySet()) {
        if (id == null) {
            continue; // 跳过null值的ID
        }
        // 检查ID是否包含前缀
        if (!idPrefix.isEmpty() && id.contains(idPrefix)) {
            throw new IllegalArgumentException("id " + id + " cannot contain " + idPrefix);
        }
        // 检查ID是否包含后缀
        if (id.contains(idSuffix)) {
            throw new IllegalArgumentException("id " + id + " cannot contain " + idSuffix);
        }
    }
    // 初始化类成员变量
    this.idForEncode = idForEncode; // 设置用于编码的ID
    this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode); // 获取对应的PasswordEncoder
    this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder); // 创建ID到PasswordEncoder的映射副本
    this.idPrefix = idPrefix; // 设置前缀
    this.idSuffix = idSuffix; // 设置后缀
}

encode 方法

该方法负责对原始密码进行编码。它根据构造函数中指定的编码器 ID,调用相应的 PasswordEncoder 实现来对密码进行编码,并在密码前加上指定的前缀和后缀。

kotlin 复制代码
@Override
public String encode(CharSequence rawPassword) {
	return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
}

matches 方法

该方法用来验证原始密码与编码后的密码是否匹配。它提取编码密码中的 ID,并根据这个 ID 查找对应的 PasswordEncoder,然后调用该编码器的 matches 方法进行匹配。如果 ID 没有映射到任何编码器,则使用默认的密码编码器进行匹配。

kotlin 复制代码
/**
 * 验证给定的原始密码和已编码密码是否匹配。
 * 
 * <p>该方法首先检查原始密码和编码密码是否都为 null。如果是,则返回 true,表示它们相等。
 * 如果编码密码带有前缀(例如 {bcrypt} 或 {noop}),则从中提取编码器的 ID,并使用相应的密码编码器进行验证。
 * 如果没有找到合适的编码器,则使用默认的编码器进行匹配。
 * 
 * @param rawPassword 原始密码,即未编码的密码。
 * @param prefixEncodedPassword 带有前缀的已编码密码,可能包含编码器的 ID。
 * @return 如果密码匹配,则返回 true;否则返回 false。
 */
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
    // 如果原始密码和已编码密码均为 null,则返回 true,表示它们匹配
    if (rawPassword == null && prefixEncodedPassword == null) {
        return true;
    }
    
    // 从已编码密码中提取编码器 ID(例如 {bcrypt} -> "bcrypt")
    String id = extractId(prefixEncodedPassword);
    
    // 根据提取出的 ID 获取对应的密码编码器
    PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
    
    // 如果没有找到对应的编码器,使用默认编码器进行匹配
    if (delegate == null) {
        return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
    }
    
    // 从带前缀的已编码密码中提取实际的编码密码
    String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
    
    // 使用找到的密码编码器进行匹配操作
    return delegate.matches(rawPassword, encodedPassword);
}

upgradeEncoding 方法

此方法检查指定的编码密码是否使用了过时的编码格式。如果是,则返回 true,指示需要升级编码。否则,使用当前的编码器进行检查。

typescript 复制代码
/**
 * 判断是否需要对密码的编码进行升级。
 * 
 * @param prefixEncodedPassword 带有前缀的加密密码,前缀通常用于标识该密码的编码方式。
 * @return 如果需要对密码的编码进行升级,则返回 true;否则返回 false。
 */
@Override
public boolean upgradeEncoding(String prefixEncodedPassword) {
    // 从带前缀的加密密码中提取出编码方式的标识符(id)
    String id = extractId(prefixEncodedPassword);
    
    // 检查当前系统使用的编码标识符是否与密码的编码标识符不同
    // 如果不同,表示编码方式已过时,需要升级,返回 true
    if (!this.idForEncode.equalsIgnoreCase(id)) {
        return true;
    }
    else {
        // 提取实际的加密密码,不包括前缀部分
        String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
        
        // 根据提取出的编码标识符获取相应的密码编码器,并检查是否需要对密码的编码进行升级
        return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
    }
}
相关推荐
渣哥19 分钟前
从代理到切面:Spring AOP 的本质与应用场景解析
javascript·后端·面试
文心快码BaiduComate35 分钟前
文心快码3.5S实测插件开发,Architect模式令人惊艳
前端·后端·架构
5pace40 分钟前
【JavaWeb|第二篇】SpringBoot篇
java·spring boot·后端
HenryLin41 分钟前
Kronos核心概念解析
后端
oak隔壁找我42 分钟前
Spring AOP源码深度解析
java·后端
货拉拉技术44 分钟前
大规模 Kafka 消费集群调度方案
后端
oak隔壁找我44 分钟前
MyBatis Plus 源码深度解析
java·后端
oak隔壁找我1 小时前
Druid 数据库连接池源码详细解析
java·数据库·后端
剽悍一小兔1 小时前
Nginx 基本使用配置大全
后端
LCG元1 小时前
性能排查必看!当Linux服务器CPU/内存飙高,如何快速定位并"干掉"罪魁祸首进程?
linux·后端