系列導航
SpringSecurity系列
- SpringSecurity系列學習(一):初識SpringSecurity
- SpringSecurity系列學習(二):密碼驗證
- SpringSecurity系列學習(三):認證流程和源碼解析
- SpringSecurity系列學習(四):基於JWT的認證
- SpringSecurity系列學習(四-番外):多因子驗證和TOTP
- SpringSecurity系列學習(五):授權流程和源碼分析
- SpringSecurity系列學習(六):基於RBAC的授權
SpringSecurityOauth2系列
- SpringSecurityOauth2系列學習(一):初認Oauth2
- SpringSecurityOauth2系列學習(二):授權服務
- SpringSecurityOauth2系列學習(三):資源服務
- SpringSecurityOauth2系列學習(四):自定義登陸登出接口
- SpringSecurityOauth2系列學習(五):授權服務自定義異常處理
密碼存儲安全進化史
這一節我們還是積累一些知識,迫不及待想編碼的小伙伴再忍忍,打好基礎才能避免一些坑,一步一腳印。
最開始的時候,密碼是明文存儲的。黑客只需要攻破你的數據庫,就能拿到所有的賬號與密碼了
為了防止密碼泄露,后來在存儲密碼的時候對密碼進行了哈希加密,這種加密是不可逆的。這樣黑客即使攻破數據庫拿到了賬號和密碼保存的記錄,也不能獲得真正的密碼,因為存儲在數據庫里的密碼是經過哈希加密算法之后的數據。
黑客為了解決這種情況,使用了彩虹表:預先將一些字符串進行哈希加密,然后存儲起來。因為現代硬件設備的升級,他們使用性能高的設備,每秒進行多次哈希計算,將結果存儲在一個巨大的庫中,也就是彩虹表中。如果拿到了你數據庫中的密碼哈希值,就將其與彩虹表中預先計算好了的哈希值作比較,一旦相等,則說明你密碼的明文就是彩虹表中對應的字符串。
為了應付彩虹表的情況,采用了一種叫加鹽的措施,就是在進行哈希計算的時候,並不完全依賴密碼。而是先生成一個隨機數,存儲在系統的某個地方,然后再使用這個鹽值和密碼做哈希,這樣就加大了黑客破解的難度,彩虹表也就沒什么作用了。
再往后發展就變成了自適應,隨着硬件設備的發展,我們密碼的加密也變得越來越高級,這里所謂的高級,其實就是越來越多次的哈希。具體來講就是:
可以配置迭代次數(md5(md5("password")))
可以配置的隨機的鹽值
迭代次數和鹽值存儲在數據庫中
更多的編碼格式算法選擇:Bcrypt,Scrypt,pdkdf2等等
這種方式除了增加密碼存儲的復雜度,還能人為的降低認證的響應速度,這樣如果黑客使用代碼進行暴力破解,沒有用這種復雜加密的方式,黑客可能一秒鍾進行幾千次暴力破解的嘗試。采用這種復雜加密的方式,黑客一秒鍾只能做幾十次甚至一次的程度。
未來的發展趨勢有好幾種:比如多因子認證,認證不只是依賴於密碼,還依賴於三方因素,比如短信驗證碼,郵箱驗證碼等等。
又比如指紋,人臉識別,但是這種方式需要硬件支持。
但是現在來說,密碼還是互聯網的主流。
密碼編碼器
SpringSecurity對於密碼的存儲也做了安全設定,保存在數據庫的密碼都是通過編碼之后的,通常我們會采用BCryptPasswordEncoder
編碼器進行編碼,這個編碼器采用SHA-256 +隨機鹽+密鑰
對密碼進行加密。SHA
系列是Hash
算法,不是加密算法,使用加密算法意味着可以解密(這個與編碼/解碼一樣),但是采用Hash
處理,其過程是不可逆的。
在注冊用戶的時候,使用SHA-256+隨機鹽+密鑰
把用戶輸入的密碼進行hash處理,得到密碼的hash值,然后將其存入數據庫中。
用戶登錄時,密碼匹配階段並沒有進行密碼解密(因為密碼經過Hash處理,是不可逆的),而是使用相同的算法把用戶輸入的密碼進行hash處理,得到密碼的hash值,然后將其與從數據庫中查詢到的密碼hash值進行比較。如果兩者相同,說明用戶輸入的密碼正確。
我們寫一個簡單的demo,配置一下密碼的編碼器
密碼編碼器demo
定義一個密碼編碼器的bean
@Bean
public PasswordEncoder passwordEncoder(){
//默認編碼算法的Id,新的密碼編碼都會使用這個id對應的編碼器
String idForEncode = "bcrypt";
//要支持的多種編碼器
Map encoders = new HashMap();
encoders.put(idForEncode,new BCryptPasswordEncoder());
//可以使用多種編碼器,密碼匹配時只要有一種編碼器匹配上即可
//舉例:歷史原因,之前用的SHA-1編碼,現在我們希望新的密碼使用bcrypt編碼
//老用戶使用SHA-1這種老的編碼格式,新用戶使用bcrypt這種編碼格式,登錄過程無縫切換
//encoders.put("SHA-1",new MessageDigestPasswordEncoder("SHA-1"));
return new DelegatingPasswordEncoder(idForEncode,encoders);
}
這里的DelegatingPasswordEncoder
,允許以不同的格式驗證密碼,提供升級的可能性。這個東西是為了解決老數據庫中,密碼使用的編碼系統方法較老,但是隨着着計算能力的發展,如果不遷移,老的編碼系統很容易就會被破解,所以遷移到更安全的編碼標准之上是一個必要的過程。
將這個密碼編碼器配置進安全配置中
/**
* `@EnableWebSecurity` 注解 deug參數為true時,開啟調試模式,會有更多的debug輸出
*
* @author 硝酸銅
* @date 2021/6/2
*/
@EnableWebSecurity(debug = true)
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
@Resource
private PasswordEncoder passwordEncoder;
...
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置從數據庫中讀取用戶信息
auth.authenticationProvider(daoAuthenticationProvider());
}
/**
* 配置 DaoAuthenticationProvider
* @return DaoAuthenticationProvider
*/
private DaoAuthenticationProvider daoAuthenticationProvider(){
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
...
// 密碼編碼器
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
...
return daoAuthenticationProvider;
}
}
測試一下密碼加密的方法:passwordEncoder.encode()
:
加密后的密碼格式:{編碼id}xxxxx
那么問題來了,要是我們不想使用舊的編碼格式了,就要使用新的,可以將老的密碼遷移到新的編碼格式下面嗎?
答案肯定是可以的,使用UserDetailsPasswordService
中提供的updatePassword
方法,這個后面會說明,這里不深入。
其實除了使用SpringSecurity提供的,我們自己也能夠實現,具體思路就是在登錄的時候,我們會拿到明文的密碼,如果認證成功,則我們按照{id}encodedPassword
這種格式,自行進行加密組裝,存儲即可。
密碼的驗證規則
密碼的驗證規則非常復雜,比如要求密碼有大小寫,長度不能為多少等等,這些規則對於使用者和規則的制定者來說都很痛苦,好在我們可以使用Passay
框架進行驗證,其已經封裝好了一些規則。
我們將驗證的邏輯封裝在注解中,有效的剝離驗證邏輯和業務邏輯
對於2個以上屬性的復合驗證,可以寫一個應用於類的注解
自定義密碼驗證demo
引入Passay
依賴和SpringValication
依賴,我們采用SpringValication
的方式來驗證密碼
關於SpringValication
的內容,請自行了解,這是常用的參數檢驗的依賴
<properties>
...
<passay.verion>1.6.0</passay.verion>
</properties>
...
<dependency>
<groupId>org.passay</groupId>
<artifactId>passay</artifactId>
<version>${passay.verion}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
編寫密碼注解
/**
* 密碼驗證注解
* @author 硝酸銅
* @date 2021/6/7
*/
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordValidator.class)
@Documented
public @interface ValidPassword {
String message() default "Invalid Password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
然后實現ConstraintValidator
接口,實現驗證的邏輯,編寫密碼驗證器
import org.passay.*;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
/**
* 密碼驗證器
* @author 硝酸銅
* @date 2021/6/7
*/
public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword,String> {
@Override
public void initialize(ValidPassword constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
PasswordValidator validator = new PasswordValidator(Arrays.asList(
//長度規則,8-30
new LengthRule(8,30),
//字符規則 至少有一個大寫字母
new CharacterRule(EnglishCharacterData.UpperCase,1),
//字符規則 至少有一個小寫字母
new CharacterRule(EnglishCharacterData.LowerCase,1),
//字符規則 至少有一個特殊字符
new CharacterRule(EnglishCharacterData.Special,1),
//非法順序規則 不允許有5個連續字母表順序的字母,比如不允許abcde
new IllegalSequenceRule(EnglishSequenceData.Alphabetical,5,false),
//非法順序規則 不允許有5個連續數字順序的數字 比如不允許12345
new IllegalSequenceRule(EnglishSequenceData.Numerical,5,false),
//非法順序規則 不允許有5個連續鍵盤順序的字母 比如不允許asdfg
new IllegalSequenceRule(EnglishSequenceData.USQwerty,5,false),
//空格規則,不能有空格
new WhitespaceRule()
));
return validator.validate(new PasswordData(s)).isValid();
}
}
在入參Dto的字段上面,寫上注解
@NotNull
@ValidPassword
private String password;
@NotNull
@ValidPassword
private String matchingPassword;
啟動,調用注冊接口(這個接口時怎么實現的這里先不關心,主要看一下在傳參的時候,密碼驗證器時怎么起作用的)
這個時候12345678
就不能通過驗證了,因為有5個以上數字順序的數字,還沒有大寫字母,小寫字母,和特殊字符。
沒有通過則會返回HTTP 400的錯誤,這個時候並沒有進入到接口方法當中
只有完全滿足設定的規則,才能進入接口中。