返回介绍

5.3 PasswordEncoder 详解

发布于 2025-04-26 13:16:44 字数 8795 浏览 0 评论 0 收藏

Spring Security 中通过 PasswordEncoder 接口定义了密码加密和比对的相关操作:

    public interface PasswordEncoder {
       String encode(CharSequence rawPassword);
       boolean matches(CharSequence rawPassword, String encodedPassword);
       default boolean upgradeEncoding(String encodedPassword) {
           return false;
       }
    }

可以看到,PasswordEncoder 接口中一共有三个方法:

(1)encode:该方法用来对明文密码进行加密。

(2)matches:该方法用来进行密码比对。

(3)upgradeEncoding:该方法用来判断当前密码是否需要升级,默认返回 false 表示不需要升级。

针对密码的所有操作,PasswordEncoder 接口中都定义好了,不同的实现类将采用不同的密码加密方案对密码进行处理。

5.3.1 PasswordEncoder 常见实现类

BCryptPasswordEncoder

BCryptPasswordEncoder 使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt 算法故意降低运行速度,以增强密码破解的难度。同时 BCryptPasswordEncoder “为自己带盐”,开发者不需要额外维护一个“盐”字段,使用 BCryptPasswordEncoder 加密后的字符串就已经“带盐”了,即使相同的明文每次生成的加密字符串都不相同。

BCryptPasswordEncoder 的默认强度为 10,开发者可以根据自己的服务器性能进行调整,以确保密码验证时间约为 1 秒钟(官方建议密码验证时间为 1 秒钟,这样既可以提高系统安全性,又不会过多影响系统运行性能)。

Argon2PasswordEncoder

Argon2PasswordEncoder 使用 Argon2 算法对密码进行加密,Argon2 曾在 Password Hashing Competition 竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题,Argon2 也是故意降低运算速度,同时需要大量内存,以确保系统的安全性。

Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder 使用 PBKDF2 算法对密码进行加密,和前面几种类似,PBKDF2 算法也是一种故意降低运算速度的算法,当需要 FIPS(Federal Information Processing Standard,美国联邦信息处理标准)认证时,PBKDF2 算法是一个很好的选择。

SCryptPasswordEncoder

SCryptPasswordEncoder 使用 scrypt 算法对密码进行加密,和前面的几种类似,scrypt 也是一种故意降低运算速度的算法,而且需要大量内存。

这四种就是我们前面所说的自适应单向函数加密。除了这几种,还有一些基于消息摘要算法的加密方案,这些方案都已经不再安全,但是出于兼容性考虑,Spring Security 并未移除相关类,主要有 LdapShaPasswordEncoder、MessageDigestPasswordEncoder、Md4Password Encoder、StandardPasswordEncoder 以及 NoOpPasswordEncoder(密码明文存储),这五种皆已废弃,这里对这些类也不做过多介绍。

除了上面介绍的这几种之外,还有一个非常重要的密码加密工具类,那就是 DelegatingPasswordEncoder。

5.3.2 DelegatingPasswordEncoder

根据前文的介绍,读者可能会认为 Spring Security 中默认的密码加密方案应该是四种自适应单向加密函数中的一种,其实不然,在 Spring Security 5.0 之后,默认的密码加密方案其实是 DelegatingPasswordEncoder。

从名字上来看,DelegatingPasswordEncoder 是一个代理类,而并非一种全新的密码加密方案。DelegatingPasswordEncoder 主要用来代理上面介绍的不同的密码加密方案。为什么采用 DelegatingPasswordEncoder 而不是某一个具体加密方式作为默认的密码加密方案呢?主要考虑了如下三方面的因素:

(1)兼容性:使用 DelegatingPasswordEncoder 可以帮助许多使用旧密码加密方式的系统顺利迁移到 Spring Security 中,它允许在同一个系统中同时存在多种不同的密码加密方案。

(2)便捷性:密码存储的最佳方案不可能一直不变,如果使用 DelegatingPasswordEncoder 作为默认的密码加密方案,当需要修改加密方案时,只需要修改很小一部分代码就可以实现。

(3)稳定性:作为一个框架,Spring Security 不能经常进行重大更改,而使用 Delegating PasswordEncoder 可以方便地对密码进行升级(自动从一个加密方案升级到另外一个加密方案)。

那么 DelegatingPasswordEncoder 到底是如何代理其他密码加密方案的?又是如何对加密方案进行升级的?我们就从 PasswordEncoderFactories 类开始看起,因为正是由它里边的静态方法 createDelegatingPasswordEncoder 提供了默认的 DelegatingPasswordEncoder 实例:

    public class PasswordEncoderFactories {
       public static PasswordEncoder createDelegatingPasswordEncoder() {
           String encodingId = "bcrypt";
           Map<String, PasswordEncoder> encoders = new HashMap<>();
           encoders.put(encodingId, new BCryptPasswordEncoder());
           encoders.put("ldap", new org.springframework.security.crypto
                                              .password.LdapShaPasswordEncoder());
           encoders.put("MD4", new org.springframework.security.crypto
                                                   .password.Md4PasswordEncoder());
           encoders.put("MD5", new org.springframework.security.crypto
                                 .password.MessageDigestPasswordEncoder("MD5"));
           encoders.put("noop", org.springframework.security.crypto.password
                                              .NoOpPasswordEncoder.getInstance());
           encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
           encoders.put("scrypt", new SCryptPasswordEncoder());
           encoders.put("SHA-1", new org.springframework.security.crypto
                               .password.MessageDigestPasswordEncoder("SHA-1"));
           encoders.put("SHA-256", new org.springframework.security.crypto
                            .password.MessageDigestPasswordEncoder("SHA-256"));
           encoders.put("sha256", new org.springframework.security.crypto
                                             .password.StandardPasswordEncoder());
           encoders.put("argon2", new Argon2PasswordEncoder());
           return new DelegatingPasswordEncoder(encodingId, encoders);
       }
       private PasswordEncoderFactories() {}
    }

可以看到,在 createDelegatingPasswordEncoder 方法中,首先定义了 encoders 变量,encoders 中存储了每一种密码加密方案的 id 和所对应的加密类,例如 bcrypt 对应着 BcryptPassword Encoder、argon2 对应着 Argon2PasswordEncoder、noop 对应着 NoOpPasswordEncoder。

encoders 创建完成后,最终新建一个 DelegatingPasswordEncoder 实例,并传入 encodingId 和 encoders 变量,其中 encodingId 默认值为 bcrypt,相当于代理类中默认使用的加密方案是 BCryptPasswordEncoder。

我们来分析一下 DelegatingPasswordEncoder 类的源码,由于源码比较长,我们就先从它的属性开始看起:

    public class DelegatingPasswordEncoder implements PasswordEncoder {
       private static final String PREFIX = "{";
       private static final String SUFFIX = "}";
       private final String idForEncode;
       private final PasswordEncoder passwordEncoderForEncode;
       private final Map<String, PasswordEncoder> idToPasswordEncoder;
       private PasswordEncoder defaultPasswordEncoderForMatches =
                                                   new UnmappedIdPasswordEncoder();
    }

(1)首先定义了前缀 PREFIX 和后缀 SUFFIX,用来包裹将来生成的加密方案的 id。

(2)idForEncode 表示默认的加密方案 id。

(3)passwordEncoderForEncode 表示默认的加密方案(BCryptPasswordEncoder),它的值是根据 idForEncode 从 idToPasswordEncoder 集合中提取出来的。

(4)idToPasswordEncoder 用来保存 id 和加密方案之间的映射。

(5)defaultPasswordEncoderForMatches 是指默认的密码比对器,当根据密码加密方案的 id 无法找到对应的加密方案时,就会使用默认的密码比对器。defaultPasswordEncoderForMatches 的默认类型是 UnmappedIdPasswordEncoder,在 UnmappedIdPasswordEncoder 的 matches 方法中并不会做任何密码比对操作,直接抛出异常。

(6)最后看到的 DelegatingPasswordEncoder 也是 PasswordEncoder 接口的子类,所以接下来我们就来重点分析 PasswordEncoder 接口中三个方法在 DelegatingPasswordEncoder 中的具体实现。

首先来看 encode 方法:

    @Override
    public String encode(CharSequence rawPassword) {
       return PREFIX + this.idForEncode + SUFFIX
                            + this.passwordEncoderForEncode.encode(rawPassword);
    }

encode 方法的实现逻辑很简单,具体的加密工作还是由加密类来完成,只不过在密码加密完成后,给加密后的字符串加上一个前缀{id},用来描述所采用的具体加密方案。因此,encode 方法加密出来的字符串格式类似如下形式:

    {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
    {noop}123
    {pbkdf2}23b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4

不同的前缀代表了后面的字符串采用了不同的加密方案。

再来看密码比对方法 matches:

    @Override
    public boolean matches(CharSequence rawPassword,
                               String prefixEncodedPassword) {
       if (rawPassword == null && prefixEncodedPassword == null) {
           return true;
       }
       String id = extractId(prefixEncodedPassword);
       PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
       if (delegate == null) {
           return this.defaultPasswordEncoderForMatches
               .matches(rawPassword, prefixEncodedPassword);
       }
       String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
       return delegate.matches(rawPassword, encodedPassword);
    }
    private String extractId(String prefixEncodedPassword) {
       if (prefixEncodedPassword == null) {
           return null;
       }
       int start = prefixEncodedPassword.indexOf(PREFIX);
       if (start != 0) {
           return null;
       }
       int end = prefixEncodedPassword.indexOf(SUFFIX, start);
       if (end < 0) {
           return null;
       }
       return prefixEncodedPassword.substring(start + 1, end);
    }

在 matches 方法中,首先调用 extractId 方法从加密字符串中提取出具体的加密方案 id,也就是{}中的字符,具体的提取方式就是字符串截取。拿到 id 之后,再去 idToPasswordEncoder 集合中获取对应的加密方案,如果获取到的为 null,说明不存在对应的加密实例,那么就会采用默认的密码匹配器 defaultPasswordEncoderForMatches;如果根据 id 获取到了对应的加密实例,则调用其 matches 方法完成密码校验。

可以看到,这里的 matches 方法非常灵活,可以根据加密字符串的前缀,去查找到不同的加密方案,进而完成密码校验。同一个系统中,加密字符串可以使用不同的前缀而互不影响。

最后,我们再来看一下 DelegatingPasswordEncoder 中的密码升级方法 upgradeEncoding:

    @Override
    public boolean upgradeEncoding(String prefixEncodedPassword) {
       String id = extractId(prefixEncodedPassword);
       if (!this.idForEncode.equalsIgnoreCase(id)) {
           return true;
       }
       else {
           String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
           return this.idToPasswordEncoder.get(id)
                                                 .upgradeEncoding(encodedPassword);
       }
    }

可以看到,如果当前加密字符串所采用的加密方案不是默认的加密方案(BcryptPassword Encoder),就会自动进行密码升级,否则就调用默认加密方案的 upgradeEncoding 方法判断密码是否需要升级。

至此,我们将 Spring Security 中的整个加密体系向读者简单介绍了一遍,接下来我们通过几个实际的案例来看一下加密方案要怎么用。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。