帳戶密碼存儲的安全性是一個很老的話題,但仍是會頻頻發生,通常的作法是 SHA256(userInputpwd+globalsalt+usersalt) 並設置密碼時時要求長度與大小寫組合,通常這樣設計能夠知足絕大部分的安全性需求。更復雜一些的方案有組合算法簽名(好比:SHA256 + BCRYPT 組合 ) , 兩步認證,Password Hash 等。 html
在以前集成 spring-security-oauth2 搭建 OAuth2.0 服務,依賴項 Spring Security 5 默認引入了更安全的加/解密機制,若是以前程序使用純文本的方式存儲用戶密碼與 Client 的密鑰或低版本升級到 Spring Security 5 後可能會出現以下錯誤。 java
Encoded password does not look like BCrypt java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233) at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
Spring Security 5 對 PasswordEncoder 作了相關的重構,提供了 Password Hash 算法的實現(bCrypt, PBKDF2, SCrypt 等是最經常使用的幾種密碼 Hash 算法),將密碼編碼以後的 hash 值和加密方式一塊兒存儲,原先默認配置的 PlainTextPasswordEncoder 明文密碼被移除了(自己明文存儲密碼也是不合適的一種方式,只適用與測試環境)。 git
@Bean public static NoOpPasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
createDelegatingPasswordEncoder 方法定義了衆多密碼密碼編碼方式的集合,能夠經過使用 PasswordEncoderFactories 類建立一個 DelegatingPasswordEncoder 的方式來解決這個問題。github
Reverting to
NoOpPasswordEncoder
is not considered to be secure. You should instead migrate to usingDelegatingPasswordEncoder
to support secure password encoding. https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#troubleshooting算法
/** * Used for creating {@link PasswordEncoder} instances * @author Rob Winch * @since 5.0 */ public class PasswordEncoderFactories { /** * Creates a {@link DelegatingPasswordEncoder} with default mappings. Additional * mappings may be added and the encoding will be updated to conform with best * practices. However, due to the nature of {@link DelegatingPasswordEncoder} the * updates should not impact users. The mappings current are: * * <ul> * <li>bcrypt - {@link BCryptPasswordEncoder} (Also used for encoding)</li> * <li>ldap - {@link LdapShaPasswordEncoder}</li> * <li>MD4 - {@link Md4PasswordEncoder}</li> * <li>MD5 - {@code new MessageDigestPasswordEncoder("MD5")}</li> * <li>noop - {@link NoOpPasswordEncoder}</li> * <li>pbkdf2 - {@link Pbkdf2PasswordEncoder}</li> * <li>scrypt - {@link SCryptPasswordEncoder}</li> * <li>SHA-1 - {@code new MessageDigestPasswordEncoder("SHA-1")}</li> * <li>SHA-256 - {@code new MessageDigestPasswordEncoder("SHA-256")}</li> * <li>sha256 - {@link StandardPasswordEncoder}</li> * </ul> * * @return the {@link PasswordEncoder} to use */ 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()); return new DelegatingPasswordEncoder(encodingId, encoders); } private PasswordEncoderFactories() {} }
使用 BCryptPasswordEncoder 編碼(默認) spring
// @Bean // public PasswordEncoder passwordEncoder(){ // return new BCryptPasswordEncoder(); // } @Bean PasswordEncoder passwordEncoder(){ return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } /** * 配置受權的用戶信息 * @param authenticationManagerBuilder * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { // authenticationManagerBuilder.inMemoryAuthentication() // .withUser("irving") // .password(passwordEncoder().encode("123456")) // .roles("read"); authenticationManagerBuilder.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); }
使用 PasswordEncoderFactories 類提供的默認編碼器,存儲密碼的格式以下所示( {id}encodedPassword ),而後在加密後的密碼前添加 Password Encoder 各自的標識符安全
{bcrypt}$2a$10$oenCzSR.yLibYMDwVvuCaeIlSIqsx0TBY1094.jQ3wgPEXzTrA52.
public class TestBCryptPwd { @Bean PasswordEncoder passwordEncoder(){ return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Bean public PasswordEncoder bcryptPasswordEncoder(){ return new BCryptPasswordEncoder(); } @Bean public PasswordEncoder pbkdf2PasswordEncoder(){ return new Pbkdf2PasswordEncoder(); } @Bean public PasswordEncoder scryptPasswordEncoder(){ return new SCryptPasswordEncoder(); } @Test public void testPasswordEncoder() { String pwd = passwordEncoder().encode("123456"); String bcryptPassword = bcryptPasswordEncoder().encode("123456"); String pbkdf2Password = pbkdf2PasswordEncoder().encode("123456"); String scryptPassword = scryptPasswordEncoder().encode("123456"); System.out.println(pwd +"\n"+bcryptPassword +"\n"+pbkdf2Password+"\n"+scryptPassword); /* {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 1 {noop}password 2 {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc 3 {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= 4 {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 5 */ } }
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder; String result = encoder.encode("123456"); assertTrue(encoder.matches("123456", result)); Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder(); String result = encoder.encode("123456"); assertTrue(encoder.matches("123456", result)); SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); String result = encoder.encode("123456"); assertTrue(encoder.matches("123456", result));
示列 app
/** * 修改用戶密碼 * * @param oldPwd * @param newPwd * @param userName * @return */ @Override public Users modifyPwd(String oldPwd, String newPwd, String userName) { Users user = this.userRepository.findByUsername(userName); //驗證用戶是否存在 if (user == null) { throw new UserFriendlyException("用戶不存在!"); } //驗證原密碼是否正確 if (!passwordEncoder.matches(oldPwd, user.getPassword())) { throw new UserFriendlyException("原密碼不正確!"); } //修改密碼 user.setPassword(passwordEncoder.encode(newPwd)); return this.userRepository.save(user); }
應該使用哪種Password Hash?[引用]ide
PBKDF二、BCRYPT、SCRYPT 曾經是最經常使用的三種密碼Hash算法,至於哪一種算法最好,多年以來密碼學家們並沒有定論。但能夠肯定的是,這三種算法都不完美,各有缺點。其中PBKDF2由於計算過程須要內存少因此可被GPU/ASIC加速,BCRYPT不支持內存佔用調整且容易被FPGA加速,而SCRYPT不支持單獨調整內存或計算時間佔用且可能被ASIC加速並有被旁路攻擊的可能。 oop
2013年NIST(美國國家標準與技術研究院)邀請了一些密碼學家一塊兒,舉辦了密碼hash算法大賽(Password Hashing Competition),意在尋找一種標準的用來加密密碼的hash算法,並藉此在業界宣傳加密存儲用戶密碼的重要性。大賽列出了參賽算法可能面臨的攻擊手段:
最終在2015年7月,Argon2算法贏得了這項競賽,被NIST認定爲最好的密碼hash算法。不過由於算法過新,目前還沒據說哪家大公司在用Argon2作密碼加密。
REFER:
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#core-services-password-encoding
https://stackoverflow.com/questions/49582971/encoded-password-does-not-look-like-bcrypt
https://www.baeldung.com/spring-security-5-default-password-encoder
https://www.cnkirito.moe/spring-security-6/
https://stackoverflow.com/questions/6832445/how-can-bcrypt-have-built-in-salts
http://www.javashuo.com/article/p-ntqhuzok-dk.html
http://www.cnblogs.com/cnblogsfans/p/5112167.html
https://github.com/KaiZhang890/store-password-safely