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);
    }
}
相关推荐
假装我不帅28 分钟前
asp.net framework从webform开始创建mvc项目
后端·asp.net·mvc
神仙别闹31 分钟前
基于ASP.NET+SQL Server实现简单小说网站(包括PC版本和移动版本)
后端·asp.net
计算机-秋大田1 小时前
基于Spring Boot的船舶监造系统的设计与实现,LW+源码+讲解
java·论文阅读·spring boot·后端·vue
货拉拉技术1 小时前
货拉拉-实时对账系统(算盘平台)
后端
掘金酱2 小时前
✍【瓜分额外奖金】11月金石计划附加挑战赛-活动命题发布
人工智能·后端
代码之光_19802 小时前
保障性住房管理:SpringBoot技术优势分析
java·spring boot·后端
ajsbxi2 小时前
苍穹外卖学习记录
java·笔记·后端·学习·nginx·spring·servlet
颜淡慕潇3 小时前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
尘浮生4 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料4 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理