PasswordEncoder 是 Spring Security 框架里用于处理密码编码的核心接口。在实际应用中,为了保障用户密码的安全,不能以明文形式存储密码。PasswordEncoder 还提供了一系列的方法来对密码进行加密、匹配等操作。
注意,Spring Security 框架要求容器(IoC)中必须有 PasswordEncoder 的实例(客户端密码和数据库密码是否匹配是由 Spring Security 去完成的,Security 中默认没有配置密码解析器)。所以当自定义登录逻辑时要求必须给容器注入 PaswordEncoder 的 bean 对象。
PasswordEncoder 接口定义了三个核心方法,如下:
encode(CharSequence rawPassword) 该方法用于对明文密码进行加密处理,返回加密后的密码字符串。
matches(CharSequence rawPassword, String encodedPassword) 此方法用于比较明文密码和加密后的密码是否匹配,返回一个布尔值表示是否匹配成功。
upgradeEncoding(String encodedPassword) 这是一个默认方法,用于判断当前加密后的密码是否需要升级加密方式,默认返回 false。
源码如下:
package org.springframework.security.crypto.password; /** * 用于对密码进行编码的服务接口。 * * 首选的实现是 {@code BCryptPasswordEncoder}。 * * @author Keith Donald */ public interface PasswordEncoder { /** * 对原始密码进行编码。 * 通常,一个好的编码算法会采用SHA-1或更高级的哈希算法, * 并结合一个8字节或更长的随机生成的盐值。 */ String encode(CharSequence rawPassword); /** * 验证从存储中获取的已编码密码与提交的原始密码(在对其进行编码后)是否匹配。 * 如果密码匹配则返回true,如果不匹配则返回false。 * 注意:存储的密码本身永远不会被解码,即密码密文不可逆,如果可逆就存在安全问题。 * @param rawPassword 要进行编码和匹配的原始密码 * @param encodedPassword 从存储中获取的、要进行比较的已编码密码 * @return 如果经过编码后的原始密码与存储中的已编码密码匹配,则返回 true */ boolean matches(CharSequence rawPassword, String encodedPassword); /** * 如果为了更高的安全性,已编码的密码应再次进行编码,则返回true, * 否则返回false。默认实现始终返回false。 * @param encodedPassword 要检查的已编码密码 * @return 如果为了更高的安全性,已编码的密码应再次进行编码,则返回true, * 否则返回false。 */ default boolean upgradeEncoding(String encodedPassword) { return false; } }
在 Spring Security 中,内置了很多 PasswordEncoder 解析器的实现,如下图:
下面将对这些内置实现进行简单介绍。
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,常使用这个解析器。它是对 bcrypt 强散列方法的具体实现,是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认10。
注意,strength 参数取值范围为 4~31,数字越大加密强度越高,速度越慢。请根据自己业务数据的私密性设置强度,大部分情况下,使用默认值即可。
源码如下:
public BCryptPasswordEncoder() { this(-1); // 强度为 -1 } /** * @param version the version of bcrypt, can be 2a,2b,2y * @param strength the log rounds to use, between 4 and 31 * @param random the secure random instance to use */ public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) { if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) { throw new IllegalArgumentException("Bad strength"); } this.version = version; // 如果强度为 -1,则默认设置为 10 this.strength = (strength == -1) ? 10 : strength; this.random = random; }
BCrypt 是一种自适应的哈希算法,它会自动生成盐值并将其包含在加密后的密码中,每次加密相同的明文密码都会得到不同的结果,提高了密码的安全性。
示例代码:
package com.hxstrive.spring_security.example; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; public class BCryptPasswordEncoderExample { public static void main(String[] args) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); String rawPassword = "test123"; // 明文 // 加密后 String encodedPassword = encoder.encode(rawPassword); System.out.println("Encoded Password: " + encodedPassword); //Encoded Password: $2a$10$xJzddcc6hH0Ysnbliag5iewLOKH3w1xQx/Y/5NKi4yh6hU.Sem1b2 String encodedPassword2 = encoder.encode(rawPassword); System.out.println("Encoded Password: " + encodedPassword2); //Encoded Password: $2a$10$cWt9z1UwXbQXgbtMenSRf.d4vSfTVIhCOBHDCsvWVlr9z7aoA8ul6 // 验证加密后的密文和明文是否匹配 boolean isMatch = encoder.matches(rawPassword, encodedPassword); System.out.println("Password Match: " + isMatch); //Password Match: true isMatch = encoder.matches(rawPassword, encodedPassword2); System.out.println("Password Match: " + isMatch); //Password Match: true } }
从上述示例可知,对明文“test123”两次加密,得到的可见密文是不一样的,但是通过 matches() 方法均能和明文“test123”匹配成功。这样就可以避免相同密码,在数据库中密文一样的问题,提高了安全性。
使用 PBKDF2(Password-Based Key Derivation Function 2)算法进行加密,该算法通过多次迭代哈希函数来增加破解的难度,适用于对安全性要求较高的场景。
示例代码:
package com.hxstrive.spring_security.example; import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; public class Pbkdf2PasswordEncoderExample { public static void main(String[] args) { Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder("9d4e34ae288c47549", 16, 5, Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1); String rawPassword = "test123"; String encodedPassword = encoder.encode(rawPassword); System.out.println("Encoded Password: " + encodedPassword); //Encoded Password: 770f4ecff081cc9d83481fbe2c448bb9a6594a791f48f35a9b964fccc2613828800ce814 String encodedPassword2 = encoder.encode(rawPassword); System.out.println("Encoded Password: " + encodedPassword2); //Encoded Password: 8525db55d51182cce2d618fdcb41d16b02bf18ec506813859dfc397941480311ac914aa4 boolean isMatch = encoder.matches(rawPassword, encodedPassword); System.out.println("Password Match: " + isMatch); //Password Match: true isMatch = encoder.matches(rawPassword, encodedPassword2); System.out.println("Password Match: " + isMatch); //Password Match: true } }
Pbkdf2PasswordEncoder 构造函数接受四个参数:
9d4e34ae288c47549:密文
16:盐(salt)长度(以字节为单位),用于增加密码加密的安全性。盐值是一个随机生成的字符串,会与密码一起进行哈希计算,即使相同的密码使用不同的盐值也会生成不同的加密结果。
5:指定迭代次数,即对密码和盐值进行哈希计算的次数。迭代次数越多,破解密码的难度就越大,但加密和解密的性能也会相应降低。
Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA1:指定使用的哈希算法,这里使用的是 PBKDF2WithHmacSHA1 算法。
基于 scrypt 算法,scrypt 是一种内存硬函数,它在计算过程中需要大量的内存,增加了攻击者使用 GPU 等设备进行暴力破解的难度,安全性较高。
示例代码:
package com.hxstrive.spring_security.example; import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; public class SCryptPasswordEncoderExample { public static void main(String[] args) { SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8(); String rawPassword = "test123"; String encodedPassword = encoder.encode(rawPassword); System.out.println("Encoded Password: " + encodedPassword); //Encoded Password: $100801$21xEBj9AqGKupCMAh3GLYA==$aWXT7XfQnHqSYsMYTnUXh64JTMtTcTR6OWY23foPG/U= String encodedPassword2 = encoder.encode(rawPassword); System.out.println("Encoded Password: " + encodedPassword2); //Encoded Password: $100801$+uvQR8aUnYvnnbdJypQxfw==$62ynC52itR12vL3hjkHMGVFhZwh7yhrK85l60pD3Y1Q= boolean isMatch = encoder.matches(rawPassword, encodedPassword); System.out.println("Password Match: " + isMatch); //Password Match: true isMatch = encoder.matches(rawPassword, encodedPassword2); System.out.println("Password Match: " + isMatch); //Password Match: true } }
运行上述代码还需要单独添加 Bouncy Castle 库的 maven 依赖,如下:
<!-- Bouncy Castle 核心库 --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency> <!-- Bouncy Castle 实用工具库 --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> <version>1.70</version> </dependency>
SCryptPasswordEncoder 构造构造函数接受 5 个参数:
cpuCost 该参数代表 SCrypt 算法中的 CPU 成本,在 SCrypt 算法里用 N 表示。CPU 成本决定了算法的计算复杂度,值越大,加密过程需要的 CPU 计算资源和时间就越多,从而增加了暴力破解密码的难度。这个值必须是大于 1 的 2 的幂次方,例如 2、4、8、16 等。当前默认值为 65536,也就是 2^16。
memoryCost 表示 SCrypt 算法中的内存成本,在 SCrypt 算法里用 r 表示。内存成本决定了算法在执行过程中所需的内存量,值越大,需要的内存就越多,这使得攻击者难以使用专门的硬件(如 GPU)进行暴力破解。默认值:当前默认值为 8。
parallelization 代表 SCrypt 算法的并行化参数,在 SCrypt 算法里用 p 表示。该参数指定了算法可以并行执行的线程数量,通过并行计算可以提高加密和解密的速度。当前默认值为 1。注意:当前的实现并未利用并行化,也就是说,即使设置了大于 1 的并行化参数,实际上也不会并行执行。
keyLength 指的是 SCrypt 算法生成的密钥长度,在 SCrypt 算法里用 dkLen 表示。密钥长度决定了加密后密码的长度,较长的密钥长度通常意味着更高的安全性。当前默认值为 32。
saltLength 表示 SCrypt 算法使用的盐值长度,在 SCrypt 算法里用 S 表示盐值。盐值是一个随机生成的字符串,会与密码一起进行加密,这样即使两个用户的密码相同,加密后的结果也会不同,增加了密码的安全性。当前默认值为 16。
源码如下:
private static final int DEFAULT_CPU_COST = 65536; private static final int DEFAULT_MEMORY_COST = 8; private static final int DEFAULT_PARALLELISM = 1; private static final int DEFAULT_KEY_LENGTH = 32; private static final int DEFAULT_SALT_LENGTH = 16; /** * Constructs a SCrypt password encoder with the provided parameters. * @param cpuCost cpu cost of the algorithm (as defined in scrypt this is N). must be * power of 2 greater than 1. Default is currently 65,536 or 2^16) * @param memoryCost memory cost of the algorithm (as defined in scrypt this is r) * Default is currently 8. * @param parallelization the parallelization of the algorithm (as defined in scrypt * this is p) Default is currently 1. Note that the implementation does not currently * take advantage of parallelization. * @param keyLength key length for the algorithm (as defined in scrypt this is dkLen). * The default is currently 32. * @param saltLength salt length (as defined in scrypt this is the length of S). The * default is currently 16. */ public SCryptPasswordEncoder(int cpuCost, int memoryCost, int parallelization, int keyLength, int saltLength) { if (cpuCost <= 1) { throw new IllegalArgumentException("Cpu cost parameter must be > 1."); } if (memoryCost == 1 && cpuCost > 65536) { throw new IllegalArgumentException("Cpu cost parameter must be > 1 and < 65536."); } if (memoryCost < 1) { throw new IllegalArgumentException("Memory cost must be >= 1."); } int maxParallel = Integer.MAX_VALUE / (128 * memoryCost * 8); if (parallelization < 1 || parallelization > maxParallel) { throw new IllegalArgumentException("Parallelisation parameter p must be >= 1 and <= " + maxParallel + " (based on block size r of " + memoryCost + ")"); } if (keyLength < 1 || keyLength > Integer.MAX_VALUE) { throw new IllegalArgumentException("Key length must be >= 1 and <= " + Integer.MAX_VALUE); } if (saltLength < 1 || saltLength > Integer.MAX_VALUE) { throw new IllegalArgumentException("Salt length must be >= 1 and <= " + Integer.MAX_VALUE); } this.cpuCost = cpuCost; this.memoryCost = memoryCost; this.parallelization = parallelization; this.keyLength = keyLength; this.saltGenerator = KeyGenerators.secureRandom(saltLength); } /** * Constructs a SCrypt password encoder with cpu cost of 16,384, memory cost of 8, * parallelization of 1, a key length of 32 and a salt length of 64 bytes. * @return the {@link SCryptPasswordEncoder} * @since 5.8 * @deprecated Use {@link #defaultsForSpringSecurity_v5_8()} instead */ @Deprecated public static SCryptPasswordEncoder defaultsForSpringSecurity_v4_1() { return new SCryptPasswordEncoder(16384, 8, 1, 32, 64); } /** * Constructs a SCrypt password encoder with cpu cost of 65,536, memory cost of 8, * parallelization of 1, a key length of 32 and a salt length of 16 bytes. * @return the {@link SCryptPasswordEncoder} * @since 5.8 */ public static SCryptPasswordEncoder defaultsForSpringSecurity_v5_8() { return new SCryptPasswordEncoder(DEFAULT_CPU_COST, DEFAULT_MEMORY_COST, DEFAULT_PARALLELISM, DEFAULT_KEY_LENGTH, DEFAULT_SALT_LENGTH); }
注意,我们可以使用 defaultsForSpringSecurity_v4_1() 和 defaultsForSpringSecurity_v5_8() 静态方法创建 SCryptPasswordEncoder 的实例。
在 @Configuration 类中,使用 @Bean 注解将 BCryptPasswordEncoder 注入到 Spring Security 中,如下:
package com.hxstrive.spring_security.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import static org.springframework.security.config.Customizer.withDefaults; /** * Spring Security 配置类 * @author hxstrive.com */ @Configuration public class SecurityConfig { //... // 看这里,配置使用 BCryptPasswordEncoder @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } //... }
省略部分可参考前面章节。