一. 密码加密简介
1. 散列加密概述
我们开发时进行密码加密,可用的加密手段有很多,比如对称加密、非对称加密、信息摘要等。在一般的项目里,常用的就是信息摘要算法,也可以被称为散列加密函数,或者称为散列算法、哈希函数。这是一种可以从任何数据中创建数字“指纹”的方法,常用的散列函数有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)等。
2. 散列加密原理
散列函数通过把消息或数据压缩成摘要信息,使得数据量变小,将数据的格式固定下来,然后将数据打乱混合,再重新创建成一个散列值,从而达到加密的目的。散列值通常用一个短的随机字母和数字组成的字符串来代表,一个好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理时,如果我们不抑制冲突来区别数据,会使得数据库中的记录很难找到。
但是仅仅使用散列函数还不够,如果我们只是单纯的使用散列函数而不做特殊处理,其实是有风险的!比如在两个用户密码明文相同时,生成的密文也会相同,这样就增加了密码泄漏的风险。
所以为了增加密码的安全性,一般在密码加密过程中还需要“加盐”,而所谓的“盐”可以是一个随机数,也可以是用户名。”加盐“之后,即使密码的明文相同,用户生成的密码密文也不相同,这就可以极大的提高密码的安全性。
传统的加盐方式需要在数据库中利用专门的字段来记录盐值,这个字段可以是用户名字段(因为用户名唯一),也可以是一个专门记录盐值的字段,但这样的配置比较繁琐。
二、SpringSecurity 中的密码源码分析
当我们项目只引入springsecurity依赖之后,接下来什么事情都不用做,我们直接来启动项目。
在项目启动过程中,我们会看到如下一行日志:
Using generated security password: 10abfb2j-36e1-446a-jh9b-f70024fc89ab
这就是 Spring Security 为默认用户 user 生成的临时密码,是一个 UUID 字符串。这个密码和用户相关的自动化配置类在 UserDetailsServiceAutoConfiguration
里边,在该类的 getOrDeducePassword
方法中,我们看到如下一行日志:
if (user.isPasswordGenerated()) {
logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
毫无疑问,我们在控制台看到的日志就是从这里打印出来的。打印的条件是 isPasswordGenerated 方法返回 true,即密码是默认生成的。
进而我们发现,user.getPassword 出现在 SecurityProperties 中,在 SecurityProperties 中我们看到如下定义:
/**
* Default user name.
*/
private String name = "user";
/**
* Password for the default user name.
*/
private String password = UUID.randomUUID().toString();
private boolean passwordGenerated = true;
可以看到,默认的用户名就是 user,默认的密码则是 UUID,而默认情况下,passwordGenerated 也为 true。
SecurityProperties默认的用户就定义在它里边,是一个静态内部类,我们如果要定义自己的用户名密码,必然是要去覆盖默认配置,我们先来看下 SecurityProperties 的定义:
@ConfigurationProperties(prefix = "spring.security")
publicclass SecurityProperties {}
这就很清晰了,我们只需要以 spring.security.user 为前缀,去定义用户名密码即可:
spring.security.user.name=admin
spring.security.user.password=123456
这就是我们新定义的用户名密码。
在 properties 中定义的用户名密码最终是通过 set 方法注入到属性中去的,这里我们顺便来看下 SecurityProperties.User#setPassword 方法:
public void setPassword(String password) {
if (!StringUtils.hasLength(password)) {
return;
}
this.passwordGenerated = false;
this.password = password;
}
从这里我们可以看到,application.properties 中定义的密码在注入进来之后,还顺便设置了 passwordGenerated 属性为 false,这个属性设置为 false 之后,控制台就不会打印默认的密码了。
此时重启项目,就可以使用自己定义的用户名/密码登录了
除了上面的配置文件这种方式之外,我们也可以在配置类中配置用户名/密码。
@Configuration
publicclass SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("123456").roles("admin");
}
}
在配置类中配置,我们就要指定 PasswordEncoder 了,这是一个非常关键的东西
三、PasswordEncoder
1、PasswordEncoder
security中用于加密的接口就是PasswordEncoder,接口用于执行密码的单向转换,以便安全地存储密码,源码如下
public interface PasswordEncoder {
//该方法提供了明文密码的加密处理,加密后密文的格式主要取决于PasswordEncoder接口实现类实例。
String encode(CharSequence rawPassword);
//匹配存储的密码以及登录时传递的密码(登录密码是经过加密处理后的字符串)是否匹配,如果匹配该方法则会返回true,第一个参数表示需要被解析的密码 第二个参数表示存储的密码
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
PasswordEncoder 中的 encode 方法是我们在用户注册的时候手动调用,而matches 方法,则是由系统调用,默认是在 DaoAuthenticationProvider#additionalAuthenticationChecks 方法中调用的。
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
可以看到,密码比对就是通过 passwordEncoder.matches 方法来进行的。
Spring Security 提供了多种密码加密方案,官方推荐使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 越大,密钥的迭代次数越多,密钥迭代次数为 2^strength。strength 取值在 4~31 之间,默认为 10。
不同于 Shiro 中需要自己处理密码加盐,在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来非常方便。而 BCryptPasswordEncoder 就是 PasswordEncoder 接口的实现类。其他实现类列表如下
举例使用
三、hutool 工具 BCrypt 进行加密和匹配
( cn.hutool.crypto.digest.BCrypt )
import cn.hutool.crypto.digest.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class UmsMemberController {
public static void main(String[] args) {
String pas="123456";
String pas1="$2a$10$mMslOUUCblGnotpq5G3j2er8OuqsIU08YF.x50//YOB6vLrGNd7Wq";
BCryptPasswordEncoder n=new BCryptPasswordEncoder();
System.out.println("加密前密码:"+pas);
System.out.println("加密后密码:"+pas1);
System.out.println("重新进行加密后密码:"+n.encode(pas));
if (n.matches(pas,pas1)){
System.out.println("True - 匹配:"+"11111111111111111111111");
}else {
System.out.println("False - 未匹配:"+"2222222222222222222222");
}
System.out.println("======================= 两种方法类似都可进行加密和匹配 =======================");
if (BCrypt.checkpw(pas,pas1)){
System.out.println("True - 匹配:"+"11111111111111111111111");
}else {
System.out.println("False - 未匹配:"+"2222222222222222222222");
}
}
}