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);
    }
}
相关推荐
isolusion1 小时前
Springboot的创建方式
java·spring boot·后端
zjw_rp1 小时前
Spring-AOP
java·后端·spring·spring-aop
TodoCoder2 小时前
【编程思想】CopyOnWrite是如何解决高并发场景中的读写瓶颈?
java·后端·面试
凌虚3 小时前
Kubernetes APF(API 优先级和公平调度)简介
后端·程序员·kubernetes
机器之心3 小时前
图学习新突破:一个统一框架连接空域和频域
人工智能·后端
.生产的驴4 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲4 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
机器之心4 小时前
AAAI 2025|时间序列演进也是种扩散过程?基于移动自回归的时序扩散预测模型
人工智能·后端
hanglove_lucky5 小时前
本地摄像头视频流在html中打开
前端·后端·html
皓木.7 小时前
(自用)配置文件优先级、SpringBoot原理、Maven私服
java·spring boot·后端