赞
踩
任何一个登录系统的密码不能明文存储,万一发生数据库泄漏事故(不管是内部人员导出数据库数据还是被黑客攻击破解数据库实例节点拿到数据库数据等,又或者是其他情况造成的),将产生巨大的损失。因此明文密码在存储到数据库之前需要加密处理。
加密算法有很多,大致有如下分类:
上文提到一些已经不推荐使用、逐渐被淘汰的算法,如MD5、SHA-1。因为不管是MD5还是SHA-1算法,对于给定的某个字符串(密码),经过哈希函数计算之后得到的结果都是固定的。比如admin
经过MD5计算(有16位和32位之分,这里用的是16位)结果始终是7a57a5a743894a0e
,root
经过SHA-1计算后结果始终是dc76e9f0c0006e8f919e0c515c66dbba3982f785
。
那黑客们就可以维护一个数据库,其字段包括加密后的密文、加密算法、明文密码,意味着可以根据密文反查明文密码。这就是反查表。
基于反查表,黑客们后来发明更高级的彩虹表。
在Java Web开发中,我们会遇到各种各样的安全问题。作为最基本的,数据库密码的安全性如何得到保证呢?此时Spring Security隆重登场,可以帮助我们解决这个问题。
加密密码的配置类(代码片段):
import jakarta.annotation.Resource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 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; @Configuration @EnableWebSecurity @EnableMethodSecurity() public class WebSecurityConfig implements SecurityFilterChain { @Resource private UserDetailsService userDetailsService; @Autowired public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder .userDetailsService(this.userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
PasswordEncoder接口定义如下:
public interface PasswordEncoder {
// 用来对明文密码进行加密
String encode(CharSequence rawPassword);
// 用来进行密码比对
boolean matches(CharSequence rawPassword, String encodedPassword);
// 用来判断当前密码是否需要升级,默认返回false表示不需要升级
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
尚未废弃的实现类,都是自适应单向函数(Adaptive One-way Functions)来处理密码问题,这种函数在进行密码匹配时,会有意占用大量系统资源(例如CPU、内存等),可以增加恶意用户攻击系统的难度。包括:bcrypt、PBKDF2、scrypt以及argon2。
因此实现类包括:
几个已经被废弃的基于消息摘要算法的实现类:
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
String salt = getSalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
private String getSalt() {
if (this.random != null) {
return BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
}
return BCrypt.gensalt(this.version.getVersion(), this.strength);
}
使用Spring Security提供的BCrypt工具类生成盐(salt);然后,根据盐和明文密码生成最终的密文。所谓加盐,就是在初始化明文数据时,由系统自动向该明文里添加一些附加数据,然后散列。引入加盐机制的目的是进一步提高加密数据的安全性,单向散列加密及加盐思想广泛应用于系统登录过程中的密码生成和校验。
构造方法:
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;
this.strength = (strength == -1) ? 10 : strength;
this.random = random;
}
从构造函数可知,strength长度默认为10,最小值为BCrypt.MIN_LOG_ROUNDS=4
,最大值为BCrypt.MAX_LOG_ROUNDS=31
。显而易见,长度越长,加密算法越复杂,被恶意破解攻击的难度越大,但是也会增加系统负载,增加加密计算时长和存储空间。因此需要取得权衡,默认情况下使用Spring Security建议的长度10即可。
浏览一下spring-security-crypto-6.2.3
源码结构:
不难发现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); }
静态方法createDelegatingPasswordEncoder,encoders中存储每一种密码加密方案的id和所对应的加密类,如bcrypt对应BcryptPassword。最后,返回代理类DelegatingPasswordEncoder实例,并且默认使用的加密方案是BCryptPasswordEncoder。
DelegatingPasswordEncoder,采用代理模式,Spring Security 5.0版本后默认的密码加密方案,主要考虑如下三方面的因素:
属性如下:
// 默认的前缀和后缀,用于包裹将来生成的加密方案的id
private static final String DEFAULT_ID_PREFIX = "{";
private static final String DEFAULT_ID_SUFFIX = "}";
// 构造方法里支持传入用户自定义的前缀和后缀
private final String idPrefix;
private final String idSuffix;
// 默认的加密方案id
private final String idForEncode;
// 根据idForEncode从idToPasswordEncoder map中提取出来的
private final PasswordEncoder passwordEncoderForEncode;
// 保存id和加密方案之间的映射
private final Map<String, PasswordEncoder> idToPasswordEncoder;
// 默认的密码比对器,当根据密码加密方案的id无法找到对应的加密方案时,就会使用默认的密码比对器。默认类型是UnmappedIdPasswordEncoder
private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
UnmappedIdPasswordEncoder是一个内部私有类:
private class UnmappedIdPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
// 直接抛出异常
throw new UnsupportedOperationException("encode is not supported");
}
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
// 并不会做任何密码比对操作,直接抛出异常
String id = extractId(prefixEncodedPassword);
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
}
}
核心方法encode
:
public String encode(CharSequence rawPassword) {
return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
}
作为一个代理类,不负责具体的加密工作,由加密类来完成,最后加上类似于{bcrypt}
这样的前缀,不同的前缀表示使用不同的加密算法,即不同的PasswordEncoder实现类,当然也包括自定义的加密类。
核心方法matches
:
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
}
String id = extractId(prefixEncodedPassword);
PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
if (delegate == null) {
return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
}
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return delegate.matches(rawPassword, encodedPassword);
}
extractId方法用于从加密字符串中提取出具体的加密方案id,也就是前缀和后缀包裹的字符串,如bcrypt,此方法就不贴出来了。根据加密方案id从map集合查找对应的加密算法实现类,查找失败则使用默认的加密类,即UnmappedIdPasswordEncoder,然后就会抛出异常。
核心方法upgradeEncoding
:
public boolean upgradeEncoding(String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
if (!this.idForEncode.equalsIgnoreCase(id)) {
return true;
} else {
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
}
}
如果当前加密字符串所采用的加密方案不是默认的BcryptPasswordEncoder ,就会自动进行密码升级,否则就调用默认加密方案的upgradeEncoding方法判断密码是否需要升级。
业务开发中,如果Spring Security自带的几个加密类都不能满足需求,或者业务场景比较复杂,需要兼容数据库历史未加密字段或加密算法不够好的字段,则可能需要自定义加密类。
具体来说,实现PasswordEncoder接口类,并重写3个方法。比如自定义一个使用SHA-512加密算法的加密类:
public class Sha512PasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { return hashWithSha512(rawPassword.toString()); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { String hashedPassword = encode(rawPassword); return encodedPassword.equals(hashedPassword); } @Override public boolean upgradeEncoding(String prefixEncodedPassword) { // 不需要升级 return false; } private String hashWithSha512(String input) { StringBuilder result = new StringBuilder(); try { MessageDigest md = MessageDigest.getInstance("SHA-512"); byte [] digested = md.digest(input.getBytes()); for (int i = 0; i < digested.length; i++) { result.append(Integer.toHexString(0xFF & digested[i])); } } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Bad algorithm"); } return result.toString(); } }
最后需要配置一下使用此自定义类,使其生效。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。