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();
}
//...
}省略部分可参考前面章节。