• Spring Security加密和匹配


    一. 密码加密简介

    1. 散列加密概述

    我们开发时进行密码加密,可用的加密手段有很多,比如对称加密、非对称加密信息摘要等。在一般的项目里,常用的就是信息摘要算法,也可以被称为散列加密函数,或者称为散列算法、哈希函数。这是一种可以从任何数据中创建数字“指纹”的方法,常用的散列函数有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)等。

    2. 散列加密原理

    散列函数通过把消息或数据压缩成摘要信息,使得数据量变小,将数据的格式固定下来,然后将数据打乱混合,再重新创建成一个散列值,从而达到加密的目的。散列值通常用一个短的随机字母和数字组成的字符串来代表,一个好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理时,如果我们不抑制冲突来区别数据,会使得数据库中的记录很难找到。

    但是仅仅使用散列函数还不够,如果我们只是单纯的使用散列函数而不做特殊处理,其实是有风险的!比如在两个用户密码明文相同时,生成的密文也会相同,这样就增加了密码泄漏的风险。

    所以为了增加密码的安全性,一般在密码加密过程中还需要“加盐”,而所谓的“盐”可以是一个随机数,也可以是用户名。”加盐“之后,即使密码的明文相同,用户生成的密码密文也不相同,这就可以极大的提高密码的安全性。

    传统的加盐方式需要在数据库中利用专门的字段来记录盐值,这个字段可以是用户名字段(因为用户名唯一),也可以是一个专门记录盐值的字段,但这样的配置比较繁琐。

    二、SpringSecurity 中的密码源码分析

    当我们项目只引入springsecurity依赖之后,接下来什么事情都不用做,我们直接来启动项目。

    在项目启动过程中,我们会看到如下一行日志:

    Using generated security password: 10abfb2j-36e1-446a-jh9b-f70024fc89ab

    这就是 Spring Security 为默认用户 user 生成的临时密码,是一个 UUID 字符串。这个密码和用户相关的自动化配置类在 UserDetailsServiceAutoConfiguration 里边,在该类的 getOrDeducePassword 方法中,我们看到如下一行日志: 

    1. if (user.isPasswordGenerated()) {
    2. logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
    3. }

    毫无疑问,我们在控制台看到的日志就是从这里打印出来的。打印的条件是 isPasswordGenerated 方法返回 true,即密码是默认生成的。

    进而我们发现,user.getPassword 出现在 SecurityProperties 中,在 SecurityProperties 中我们看到如下定义:

    1. /**
    2. * Default user name.
    3. */
    4. private String name = "user";
    5. /**
    6. * Password for the default user name.
    7. */
    8. private String password = UUID.randomUUID().toString();
    9. private boolean passwordGenerated = true;

     可以看到,默认的用户名就是 user,默认的密码则是 UUID,而默认情况下,passwordGenerated 也为 true。

    SecurityProperties默认的用户就定义在它里边,是一个静态内部类,我们如果要定义自己的用户名密码,必然是要去覆盖默认配置,我们先来看下 SecurityProperties 的定义:

    1. @ConfigurationProperties(prefix = "spring.security")
    2. publicclass SecurityProperties {}

    这就很清晰了,我们只需要以 spring.security.user 为前缀,去定义用户名密码即可:

    1. spring.security.user.name=admin
    2. spring.security.user.password=123456

    这就是我们新定义的用户名密码。

    在 properties 中定义的用户名密码最终是通过 set 方法注入到属性中去的,这里我们顺便来看下 SecurityProperties.User#setPassword 方法:

    1. public void setPassword(String password) {
    2. if (!StringUtils.hasLength(password)) {
    3. return;
    4. }
    5. this.passwordGenerated = false;
    6. this.password = password;
    7. }

    从这里我们可以看到,application.properties 中定义的密码在注入进来之后,还顺便设置了 passwordGenerated 属性为 false,这个属性设置为 false 之后,控制台就不会打印默认的密码了。

    此时重启项目,就可以使用自己定义的用户名/密码登录了

    除了上面的配置文件这种方式之外,我们也可以在配置类中配置用户名/密码。

    1. @Configuration
    2. publicclass SecurityConfig extends WebSecurityConfigurerAdapter {
    3. @Bean
    4. PasswordEncoder passwordEncoder() {
    5. return NoOpPasswordEncoder.getInstance();
    6. }
    7. @Override
    8. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    9. auth.inMemoryAuthentication()
    10. .withUser("admin")
    11. .password("123456").roles("admin");
    12. }
    13. }

    在配置类中配置,我们就要指定 PasswordEncoder 了,这是一个非常关键的东西

    三、PasswordEncoder

    1、PasswordEncoder

    security中用于加密的接口就是PasswordEncoder,接口用于执行密码的单向转换,以便安全地存储密码,源码如下

    1. public interface PasswordEncoder {
    2. //该方法提供了明文密码的加密处理,加密后密文的格式主要取决于PasswordEncoder接口实现类实例。
    3. String encode(CharSequence rawPassword);
    4. //匹配存储的密码以及登录时传递的密码(登录密码是经过加密处理后的字符串)是否匹配,如果匹配该方法则会返回true,第一个参数表示需要被解析的密码 第二个参数表示存储的密码
    5. boolean matches(CharSequence rawPassword, String encodedPassword);
    6. default boolean upgradeEncoding(String encodedPassword) {
    7. return false;
    8. }
    9. }

    PasswordEncoder 中的 encode 方法是我们在用户注册的时候手动调用,而matches 方法,则是由系统调用,默认是在 DaoAuthenticationProvider#additionalAuthenticationChecks 方法中调用的。

    1. protected void additionalAuthenticationChecks(UserDetails userDetails,
    2. UsernamePasswordAuthenticationToken authentication)
    3. throws AuthenticationException {
    4. if (authentication.getCredentials() == null) {
    5. logger.debug("Authentication failed: no credentials provided");
    6. throw new BadCredentialsException(messages.getMessage(
    7. "AbstractUserDetailsAuthenticationProvider.badCredentials",
    8. "Bad credentials"));
    9. }
    10. String presentedPassword = authentication.getCredentials().toString();
    11. if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
    12. logger.debug("Authentication failed: password does not match stored value");
    13. throw new BadCredentialsException(messages.getMessage(
    14. "AbstractUserDetailsAuthenticationProvider.badCredentials",
    15. "Bad credentials"));
    16. }
    17. }

     可以看到,密码比对就是通过 passwordEncoder.matches 方法来进行的。

    Spring Security 提供了多种密码加密方案,官方推荐使用 BCryptPasswordEncoder,BCryptPasswordEncoder 使用 BCrypt 强哈希函数,开发者在使用时可以选择提供 strength 和 SecureRandom 实例。strength 越大,密钥的迭代次数越多,密钥迭代次数为 2^strength。strength 取值在 4~31 之间,默认为 10。

    不同于 Shiro 中需要自己处理密码加盐,在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来非常方便。而 BCryptPasswordEncoder 就是 PasswordEncoder 接口的实现类。其他实现类列表如下

    举例使用

    三、hutool 工具 BCrypt 进行加密和匹配

    ( cn.hutool.crypto.digest.BCrypt )

    1. import cn.hutool.crypto.digest.BCrypt;
    2. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    3. public class UmsMemberController {
    4. public static void main(String[] args) {
    5. String pas="123456";
    6. String pas1="$2a$10$mMslOUUCblGnotpq5G3j2er8OuqsIU08YF.x50//YOB6vLrGNd7Wq";
    7. BCryptPasswordEncoder n=new BCryptPasswordEncoder();
    8. System.out.println("加密前密码:"+pas);
    9. System.out.println("加密后密码:"+pas1);
    10. System.out.println("重新进行加密后密码:"+n.encode(pas));
    11. if (n.matches(pas,pas1)){
    12. System.out.println("True - 匹配:"+"11111111111111111111111");
    13. }else {
    14. System.out.println("False - 未匹配:"+"2222222222222222222222");
    15. }
    16. System.out.println("======================= 两种方法类似都可进行加密和匹配 =======================");
    17. if (BCrypt.checkpw(pas,pas1)){
    18. System.out.println("True - 匹配:"+"11111111111111111111111");
    19. }else {
    20. System.out.println("False - 未匹配:"+"2222222222222222222222");
    21. }
    22. }
    23. }

  • 相关阅读:
    (七)vulhub专栏:Log4j远程代码执行漏洞复现
    Intel OpenVINO 安装显卡驱动
    Python 从字典构造多叉树
    个人PC安装软件
    设计模式3、工厂方法模式 Factory Method
    【c++】跟webrtc学std array 4: H264PacketBuffer 包缓存
    【node.js 入门篇】三分钟实现简单服务器功能
    TCPIP网络编程第一章踩坑过程 bind() error connect() error
    私域增长 | 私域会员:9大连锁行业15个案例集锦
    非最小相位系统的闭环频域辨识算法
  • 原文地址:https://blog.csdn.net/weixin_39255905/article/details/134023271