Loading

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的错误,这个时候并没有进入到接口方法当中

只有完全满足设定的规则,才能进入接口中。

posted @ 2021-09-27 16:34  硝酸铜  阅读(2376)  评论(0编辑  收藏  举报