在第一篇文章,我們展示了一個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;