Java-Security(三):加密的用法、PasswordEncoder类源码分析
在第一篇文章,我们展示了一个demo,其中讲到了对用户的密码进行了明文展示的用法,其实那么做是不安全的,在实际项目中往往会采用各种加密方法(比如:bcrypt,md5,sha1,sha2等)来实现对密码的保护。
本片文章将会主要讲解如何在Spring Security实现对密码加密的各种用法,以及对BCrypt的用法进一步分析。
概念
Spring Security 为我们提供了一套加密规则和密码比对规则,org.springframework.security.crypto.password.PasswordEncoder 接口,该接口里面定义了三个方法。
public interface PasswordEncoder { //加密(外面调用一般在注册的时候加密前端传过来的密码保存进数据库) String encode(CharSequence rawPassword); //加密前后对比(一般用来比对前端提交过来的密码和数据库存储密码, 也就是明文和密文的对比) boolean matches(CharSequence rawPassword, String encodedPassword); //是否需要再次进行编码, 默认不需要 default boolean upgradeEncoding(String encodedPassword) { return false; } }
在Spring Security下 PasswordEncoder 的实现类包含:
其中常用到的分别有下面这么几个:
BCryptPasswordEncoder:Spring Security 推荐使用的,使用BCrypt强哈希方法来加密。
MessageDigestPasswordEncoder:用作传统的加密方式加密(支持 MD5、SHA-1、SHA-256...)
DelegatingPasswordEncoder:最常用的,根据加密类型id进行不同方式的加密,兼容性强
NoOpPasswordEncoder:明文, 不做加密
其他
Spring Security中加密的用法:
使用bcrypt bean
applicationContext-shiro.xml中配置:
<bean id="secureRandom" class="java.security.SecureRandom"/> <bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"> <constructor-arg name="version" value="$2A" /> <!-- salt随机生成版本 默认$2A--> <constructor-arg name="strength" value="10"/> <!-- 使用salt进行加密迭代次数,默认10--> <constructor-arg name="random" ref="secureRandom"/> <!-- 随机算法 --> </bean> <security:authentication-manager> <security:authentication-provider> <security:user-service> <security:user name="user" password="$2a$10$LCe6jsoHUrEvWI1KURrqbu/xfuPU5aZj2RkPTVS0d7MUJiT55Lt/y" authorities="ROLE_USER"/> <security:user name="admin" password="$2a$10$BR3Np37NbmtWHqpSZE6AMeCMG4Rm.UOUEZ3dYrW3oUXHNuSBXjDwi" authorities="ROLE_USER, ROLE_ADMIN"/> </security:user-service> <security:password-encoder ref="bCryptPasswordEncoder"/> </security:authentication-provider> </security:authentication-manager>
说明:
1)需要配置 bCryptPasswordEncoder的bean,在该bean配置时,可以指定其构造函数相关参数:
version:salt随机生成版本,默认:采用 BCryptVersion.$2A.getVersion();
strength:使用salt进行加密迭代次数,默认:10;
random:随机算法,默认:new SecureRandom()。
2)需要在<authentication-provider>标签下的<password-encoder ref=''/>指定该bean。
密码加密用法:
// BCrypt加密与验证,内部默认: PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456")); System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456")); // BCrypt密文解析 //参数解释 //1)2a:加密算法版本号。 //2)10:加密轮次,默认为10,数值越大,加密时间和越难破解呈指数增长。可在BCryptPasswordEncoder构造参数传入。 //3)密码加密:前面的内容是盐,后面的内容才是真正的密文。 //以下方式可以更清晰的看出盐和全文。 String salt = BCrypt.gensalt(BCryptPasswordEncoder.BCryptVersion.$2A.getVersion(), 10, new SecureRandom()); String result = BCrypt.hashpw("123456", salt);//全文 System.out.println("salt:" + salt + ",salt's length:" + salt.length()); // salt长度是29 System.out.println("result:" + result);
在对密码加密时,可以采用上边这3种方法:
1)BCryptPasswordEncoder的实例,直接调用 encode方法,此时version,strlength,random都采用默认值。
2)也可以使用BCrypt来实现,实际上上边BCypt的操作就是BCryptPasswordEncoder#encode内部的方法实现。
3)另外,也可以直接在代码中引入applicaitonContext-security.xml中的md5 bean到代码中 @Resources("bCryptPasswordEncoder") private PasswordEncoder bCryptPasswordEncoder;。
使用md5 bean
applicationContext-shiro.xml中配置
<bean id="md5" class="org.springframework.security.crypto.password.MessageDigestPasswordEncoder"> <constructor-arg name="algorithm" value="MD5"/> <property name="iterations" value="10"/> </bean> <security:authentication-manager> <security:authentication-provider> <security:user-service> <security:user name="user" password="{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8" authorities="ROLE_USER"/> <security:user name="admin" password="{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8" authorities="ROLE_USER, ROLE_ADMIN"/> </security:user-service> <security:password-encoder ref="md5"/> </security:authentication-provider> </security:authentication-manager>
说明:
1)需要配置md5 bean,在配置bean时,必须指定MessageDigestPasswordEncoder的构造函数参数:algorithm:指定算法类型,这里是MD5;
2)另外,md5#iterations参数:迭代次数如果不指定,默认为1,这里指定为10;
2)需要在<authentication-provider>标签下的<password-encoder ref=''/>指定该bean。
密码加密用法:
MessageDigestPasswordEncoder md5 = new MessageDigestPasswordEncoder("MD5"); md5.setIterations(10); md5Password = "{MD5}" + md5.encode("password"); System.out.println("MD5密码:" + md5Password); System.out.println("MD5密码对比:" + passwordEncoder.matches("password", md5Password));
在对密码加密时,可以采用上边方法:
1)MessageDigestPasswordEncoder的实例,可以设置其迭代次数。
2)另外,也可以直接在代码中引入applicaitonContext-security.xml中的md5 bean到代码中 @Resources("md5") private PasswordEncoder md5;。
缺省password-encoder(DelegatingPasswordEncoder)
当缺省<security:password-encoder ref="xxx"/>时,Spring Security会使用系统内置的DelegatingPasswordEncoder,自动动适配 PasswordEncoder。
applicationContext-shiro.xml中配置:
<security:authentication-manager> <security:authentication-provider> <security:user-service> <!-- noop NoOpPasswordEncoder.getInstance()--> <security:user name="user" password="{noop}userpwd" authorities="ROLE_USER"/> <security:user name="admin" password="{noop}adminpwd" authorities="ROLE_USER, ROLE_ADMIN"/> <!-- bcrypt new BCryptPasswordEncoder() --> <security:user name="user1" password="{bcrypt}$2a$10$LCe6jsoHUrEvWI1KURrqbu/xfuPU5aZj2RkPTVS0d7MUJiT55Lt/y" authorities="ROLE_USER"/> <security:user name="admin1" password="{bcrypt}$2a$10$BR3Np37NbmtWHqpSZE6AMeCMG4Rm.UOUEZ3dYrW3oUXHNuSBXjDwi" authorities="ROLE_USER, ROLE_ADMIN"/> <!-- MD5 new MessageDigestPasswordEncoder("MD5") --> <security:user name="user2" password="{MD5}{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8" authorities="ROLE_USER"/> <security:user name="admin2" password="{MD5}{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8" authorities="ROLE_USER, ROLE_ADMIN"/> </security:user-service> <security:password-encoder ref="md5"/> </security:authentication-provider> </security:authentication-manager>
1)如果在<security:authentication-provider>下指定了<security:password-encoder ref="xxx"/>就不需要在<security:user name="xxx" password="yyy"authorities="zzz"/>中的 password 前边加上加密类型({noop}、{bcrypt}、{MD5}等),否则会导致密码验证失败;
2)如果在<security:authentication-provider>下未指定<security:password-encoder ref="xxx"/>就必须要在<security:user name="xxx" password="yyy" authorities="zzz"/>中的 password 前边加上加密类型({noop}、{bcrypt}、{MD5}等),否则会导致密码验证失败。因为此时验证密码是否成功,会调用org.springframework.security.crypto.password.DelegatingPasswordEncoder.java中的#encode方法、#matches方法,而DelegatingPasswordEncoder中查找密码加密对应的PasswordEncoder时,会根据密码前缀的加密类型查找:如果查找失败,会导致查找不到delegate,也就是delegate为null。
密码加密、解密代码示例:
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); // 此时encode内部使用的就是 BCryptPasswordEncoder String encode = passwordEncoder.encode("password"); System.out.println("bcrypt密码对比:" + passwordEncoder.matches("password", encode)); // 不带salt,迭代 String md5NoSaltPassword = "{MD5}" + DigestUtils.md5DigestAsHex("password".getBytes()); System.out.println("MD5(不含salt、iterations)密码对比:" + passwordEncoder.matches("password", md5NoSaltPassword)); // 待salt,迭代 MessageDigestPasswordEncoder md5SaltIterationsPassword = new MessageDigestPasswordEncoder("MD5"); md5SaltIterationsPassword.setIterations(1); String md5Password = "{MD5}" + md5SaltIterationsPassword.encode("password"); System.out.println("MD5(包含salt、iterations)密码对比:" + passwordEncoder.matches("password", md5Password)); String noopPassword = "{noop}password"; System.out.println("noop密码对比:" + passwordEncoder.matches("password", noopPassword));
输出结果:
bcrypt密码对比:true MD5(不含salt、iterations)密码对比:true MD5(包含salt、iterations)密码对比:true noop密码对比:true
DelegatingPasswordEncoder类讲解
构造函数初始化
DelegatingPasswordEncoder本身就是继承了 PasswordEncoder 类,因此也可以在applicationContext-shiro.xml中定义为bean,在<security:authentication-provider>下指定<security:password-encoder ref="xxx"/>的 ref 为该bean。
但是,实际上这么做是没有意义,因为在<security:authentication-provider>下不指定<security:password-encoder ref="xxx"/>时,系统会缺省的采用DelegatingPasswordEncoder作为PasswordEncoder的实现。
public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
if (idForEncode == null) {
throw new IllegalArgumentException("idForEncode cannot be null");
} else if (!idToPasswordEncoder.containsKey(idForEncode)) {
throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
} else {
Iterator var3 = idToPasswordEncoder.keySet().iterator();
while(var3.hasNext()) {
String id = (String)var3.next();
if (id != null) {
if (id.contains("{")) {
throw new IllegalArgumentException("id " + id + " cannot contain " + "{");
}
if (id.contains("}")) {
throw new IllegalArgumentException("id " + id + " cannot contain " + "}");
}
}
}
this.idForEncode = idForEncode;
this.passwordEncoderForEncode = (PasswordEncoder)idToPasswordEncoder.get(idForEncode);
this.idToPasswordEncoder = new HashMap(idToPasswordEncoder);
}
}
说明:
1)调用DelegatingPasswordEncoder#constructor()的类是PasswordEncoderFactories.java:
public class 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", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1")); encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256")); encoders.put("sha256", new StandardPasswordEncoder()); encoders.put("argon2", new Argon2PasswordEncoder()); return new DelegatingPasswordEncoder(encodingId, encoders); } private PasswordEncoderFactories() { } }
上边代码是spring security系统中唯一用来初始化DelegatingPasswordEncoder的地方。
- 1)在Spring Security系统将AuthenticationProvider的bean初始化到Spring容器时,会调用PasswordEncoderFactories#createDelegatingPasswordEncoder()方法初始化DelegatingPasswordEncoder;
- 2)这个过程也就是给AutheticationProvider#passwordEncoder赋值的触发点;
- 3)当然如果在<security:authentication-provider>下指定<security:password-encoder ref="xxx"/>的 ref 不为DelegatingPasswordEncoder时,也将不会调用PasswordEncoderFactories#createDelegatingPasswordEncoder()方法。
2)idToPasswordEncoder属性:DelegatingPasswordEncoder是一个能适配多种PasswordEncoder的委托类,其内部定义了一个Map<String,PasswordEncoder>集合:
key为:PasswordEncoder的别名;
value为:PasswordEncoder的具体实现类。
private final Map<String, PasswordEncoder> idToPasswordEncoder;
idToPasswordEncoder用来托管PassswordEncoder的实现,这个类是在DelegatingPasswordEncoder#constructor中被传递初始化的。
3)idForEncode属性:通过PasswordEncoderFactories#createDelegatingPasswordEncoder()中初始化DelegatingPasswordEncoder的代码,可以知道idForEncode的值是“bcrypt”;
4)passwordEncoderForEncode属性:就是BCryptPasswordEncoder对象。
encode加密
public String encode(CharSequence rawPassword) { return "{" + this.idForEncode + "}" + this.passwordEncoderForEncode.encode(rawPassword); }
说明:
1)通过上边DelegatingPasswordEncoder#constructor()代码可以知道:passwordEncoderForEncode属性就是BCryptPasswordEncoder对象;
2)DelegatingPasswordEncoder#encode()方法:实际上就是"bcrypt"加密算法。这点十分重要,往往也是其特殊之处,需要使用者牢记。
3)rawPassword参数:待加密密码明文。
matches匹配密码
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { if (rawPassword == null && prefixEncodedPassword == null) { return true; } else { // 根据密文前缀查找 delegate String id = this.extractId(prefixEncodedPassword); PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id); if (delegate == null) { // delegate查找失败 return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword); } else { String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword); return delegate.matches(rawPassword, encodedPassword); } } }
说明:
1)rawPassword参数:密码明文;
2)prefixEncodedPassword参数:带有加密类型的密码密文,必须带有使用的PasswordEncoder类型(PasswordEncoderFactories#createDelegatingPasswordEncoder()中map#key);
格式举例:
{noop}password {bcypt}$2a$10$IK/02aEUVRBaeoQsvN.VluPLqNKZ2ZwwTRmAAWXmlnCU5DAjmjtRC {MD5}5f4dcc3b5aa765d61d8327deb882cf99 {MD5}{L5M7tjEyGdBtyFCyk0pBXOLLFi3AOMEBZqdRDTAwV6c=}c05b48c699659f56462bbed387485cc6
3)当没有指定密码加密类型({bcypt}等)时,会抛出异常:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:250) org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:198) org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks(DaoAuthenticationProvider.java:90) org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:166) org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:175) 。。。
BCryptPasswordEncoder类讲解
属性:
private Pattern BCRYPT_PATTERN; private final Log logger; private final int strength; private final BCryptPasswordEncoder.BCryptVersion version; private final SecureRandom random;
说明:
1)BCRYPT_PATTERN:bcrypt密文格式验证正则表达式;
2)logger:日志操作类;
3)strlength:生成salt迭代次数;
4)version:生成salt采用的版本;
5)random:随机生成slat实现。
构造函数:
public BCryptPasswordEncoder() { this(-1); } public BCryptPasswordEncoder(int strength) { this(strength, (SecureRandom)null); } public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version) { this(version, (SecureRandom)null); } public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, SecureRandom random) { this(version, -1, random); } public BCryptPasswordEncoder(int strength, SecureRandom random) { this(BCryptPasswordEncoder.BCryptVersion.$2A, strength, random); } public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength) { this(version, strength, (SecureRandom)null); } public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength, SecureRandom random) { this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}"); this.logger = LogFactory.getLog(this.getClass()); if (strength == -1 || strength >= 4 && strength <= 31) { this.version = version; this.strength = strength == -1 ? 10 : strength; this.random = random; } else { throw new IllegalArgumentException("Bad strength"); } }
构造函数重构的比较多,在DelegatingPasswordEncoder中使用的就是第一个构造函数,此时属性会赋值默认值:
1)BCRYPT_PATTERN:bcrypt密文格式验证正则表达式,默认值:Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
2)strlength:生成salt迭代次数,默认值:10;
3)version:生成salt采用的版本,默认值:BCryptPasswordEncoder.BCryptVersion.$2A;
4)random:随机生成slat实现,默认值:空。
encode方法:
public String encode(CharSequence rawPassword) { String salt; if (this.random != null) { salt = BCrypt.gensalt(this.version.getVersion(), this.strength, this.random); } else { salt = BCrypt.gensalt(this.version.getVersion(), this.strength); } return BCrypt.hashpw(rawPassword.toString(), salt); }
BCryptPasswordEncoder实际内部是使用 BCrypt 实现;
matches方法:
public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword != null && encodedPassword.length() != 0) { if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) { this.logger.warn("Encoded password does not look like BCrypt"); return false; } else { return BCrypt.checkpw(rawPassword.toString(), encodedPassword); } } else { this.logger.warn("Empty encoded password"); return false; } }
从BCRYPT_PATTERN的值"\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}",可以发现另外一些密文分为3部分:
- 第一部分:以$2a、$2y、$2b开头;
- 第二部分:以$数字
- 第三部分:以$开头后边附加.、/、数字、大写字母、小写字母组成的,且长度为53的字符串。
测试代码:
@Test public void testPwdEncoder() { // // BCrypt加密与验证 PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456")); System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456")); // BCrypt密文解析 //在密文中包含3段内容,$是分隔符。 //1)2a:加密算法版本号。 //2)10:加密轮次,默认为10,数值越大,加密时间和越难破解呈指数增长。可在BCryptPasswordEncoder构造参数传入。 //3)第3个$之后:前面的内容是盐,后面的内容才是真正的密文。 //以下方式可以更清晰的看出盐和全文。 String salt = BCrypt.gensalt(BCryptPasswordEncoder.BCryptVersion.$2A.getVersion(), 10, new SecureRandom()); String result = BCrypt.hashpw("123456", salt);//全文 System.out.println("salt:" + salt + ",salt's length:" + salt.length()); // salt长度是29 System.out.println("result:" + result); salt = BCrypt.gensalt(BCryptPasswordEncoder.BCryptVersion.$2B.getVersion(), 11, new SecureRandom()); result = BCrypt.hashpw("123456", salt);//全文 System.out.println("salt:" + salt + ",salt's length:" + salt.length()); // salt长度是29 System.out.println("result:" + result); }
打印:
passwordEncoder 123456:$2a$10$XSpVd/lavtejOXHeDGNMOe1zxgblnsXWoTi0DFD/vN4Z6EjH1r97q passwordEncoder 123456:$2a$10$I9zV9AbsEdi36s7ovTQ2hOhUczFP5CXybnyJv9aNY6Ae6qky9oouu salt:$2a$10$yg5TNGzmyNe0di70exM.vO,salt's length:29 result:$2a$10$yg5TNGzmyNe0di70exM.vOWLx.lMiniZ/BOCoecIc5tF/Q0CvYUJa salt:$2b$11$ncuDpd17nju3d6auOrQAr.,salt's length:29 result:$2b$11$ncuDpd17nju3d6auOrQAr.BZqNeyyVgqhb3gncQyRUvuKHzA2.FOS
从上边代码测试会发现,BCryptPasswordEncoder 实际内部是使用 BCrypt 实现,另外从测试可以发现使用SpringSecurity缺省password encoder生成密文有以下规则:
1)在密文中包含3段内容,
2)2a:salt生成算法版本号;
3)10:salt迭代次数,默认为10(取值范围是:[4,31]),数值越大,加密时间和越难破解呈指数增长。可在BCryptPasswordEncoder构造参数传入;
4)第3个$之后:前面的内容是盐,后面的内容才是真正的密文;
5)随机生成salt,且salt的长度为29;
基础才是编程人员应该深入研究的问题,比如:
1)List/Set/Map内部组成原理|区别
2)mysql索引存储结构&如何调优/b-tree特点、计算复杂度及影响复杂度的因素。。。
3)JVM运行组成与原理及调优
4)Java类加载器运行原理
5)Java中GC过程原理|使用的回收算法原理
6)Redis中hash一致性实现及与hash其他区别
7)Java多线程、线程池开发、管理Lock与Synchroined区别
8)Spring IOC/AOP 原理;加载过程的。。。
【+加关注】。