重点标识
密码加密的重要性这里就不说了,现在应该也没有明文存储的密码了。
这篇,我们主要了解一下Security的加密方式,所运用的思路。
Security采用单向自适应函数来进行加密,有意增加字符串比较时间,增加破解系统的难度,只能从明文变成密文,不能从密文变成明文。
我们也可以手动调整它的参数,官方一般推荐登录认证在一秒左右,性能较强的系统,可以调高一点,性能较差,可以调低一点。
原理解析
我们简单了解一下Security的加密认证方式,实际上主要就是PasswordEncoder这个接口,提供了三个方法:
encode 加密
matches 比较密码
upgradEncoding 升级密码(密码的自动升级)
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
PasswordEncoder有许多干活的,Security默认使用的就是BCryptPasswordEncoder这种加密算法。
调整密码强度
密码强度,针对于一些性能较强的系统来说,及时的响应对于用户体验来说是一种优势,但从破解层面来看,其实也是一种劣势。
Security强度的范围为小于4,大于31,其余都是不对的。
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
for (int i = 0; i < 10; i++) {
System.out.println(bCryptPasswordEncoder.encode("123"));
}
这张图的意思$2a$10,就是强度为10;看一下源码对它的定义,Security默认-的密码强度是-1 ,源码中给的就是10;,验证一下。
多种加密共存
Security提供DelegatingPasswordEncoder,对多种加密方式统一进行管理。
在PasswordEncoderFactories存了一堆,看看
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
- {bcrypt}$2a
10
10
10mwLuXtPsOczW2H4Iy2W7uOyl/kXQJpf0NR6CplPG4ZlzUniTB9Ei.
看一下它生成密文的内容,从左到依次是加密方法,强度,以及加密后的密文。
这个是他的加密源码拼接方法。
public String encode(CharSequence rawPassword) {
String var10000 = this.idPrefix;
return var10000 + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
}
可以简单的测一些,这是他默认的
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
DelegatingPasswordEncoder encodingId1 = new DelegatingPasswordEncoder(encodingId, encoders);
for (int i = 0; i < 10; i++) {
System.out.println(encodingId1.encode("123"));
}
换个SHA-256试一试
DelegatingPasswordEncoder encodingId1 = new DelegatingPasswordEncoder("SHA-256", encoders);
for (int i = 0; i < 10; i++) {
System.out.println(encodingId1.encode("123"));
}
试一下密码比对
boolean matches = encodingId1.matches("123", "{SHA-256}{xZpsNg4SRrJeGZcjAM9i0jbGW7WbECXbxOIL/Um5mPo=}06c3ea9b29dea28920d8698fbc8321d9b8995718e809b6513416a7e5643d45ec");
System.out.println(matches);
自然就是true,没问题的。
密码升级
我们知道,md5被破解,已经不安全了,如果碰到一些老旧的系统,需要升级,更改加密方式,虽然说Md5已经被破解,但是数据库存储的肯定也是密文,无法知道明文,一个个破解,不规范,也不符合现实。
这种情况下,我们就可以考虑Security的密码升级。
方案如下:
用户登录的时候,判断用户密码是否需要升级,如果需要升级,则会触发这个方法,进行升级。
但是这个,只能使用DelegatingPasswordEncoder,否则系统中只存在一种加密方式,无法进行升级。
可以在config中配置,不配置就是默认的密码加密策略,如下。
@Bean
PasswordEncoder passwordEncoder(){
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(encodingId, encoders);
return delegatingPasswordEncoder;
}
密码加密升级的策略,看他是不是默认的加密方案,是,再看密码强度是不是一样,一样则不变,不是,则改为默认的加密方案,或者强度不一致,也会进行升级。
升级的方式很简单,我们之前在整合Mybatis的时候,有一个UserService,在里面实现一个接口就行了UserDetailsPasswordService #updatePassword。
@Service
public class UserService implements UserDetailsService, UserDetailsPasswordService {
@Autowired
UserMapper userMapper;
/**
* 当用户登录的时候,会自动触发到这里,去数据库查询
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User u = userMapper.loadUserByUsername(username);
if(u == null){
throw new UsernameNotFoundException("用户名不存在!");
}
return u;
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
//根据用户名更新
userMapper.updatePassword(user.getUsername(),newPassword);
return user;
}
}
<update id="updatePassword">
UPDATE USER SET PASSWORD=#{newPassword}
WHERE USERNAME=#{username}
</update>
这样,就OK了。
结语
又掌握一招,积水成渊,蛟龙生焉!