Spring Security 教程

Spring Security PasswordEncoder 密码解析器

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 解析器的实现,如下图:

Spring Security PasswordEncoder 密码解析器

下面将对这些内置实现进行简单介绍。

BCryptPasswordEncoder

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”匹配成功。这样就可以避免相同密码,在数据库中密文一样的问题,提高了安全性。

Pbkdf2PasswordEncoder

使用 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 算法。

SCryptPasswordEncoder

基于 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 的实例。

配置 PasswordEncoder

在 @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();
    }

    //...
}

省略部分可参考前面章节。

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
其他应用
公众号