SpringSecurity系列學習(二):密碼驗證


系列導航

SpringSecurity系列

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的錯誤,這個時候並沒有進入到接口方法當中

只有完全滿足設定的規則,才能進入接口中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM