SpringSecurity(七)【密码加密】

七、密码加密


简介

  • 加密意义

2011年12月21日,有人在网络上公开了一个包含600万个 CSDN 用户资资料的数据库,数据全部为明文储存,包含用户名、密码以及注册邮箱。事件发生后CSDN在微博、官方网站等渠道发出了声明,解释说此数据库系2009年备份所用,因不明原因泄漏 已经向警方报案,后又在官网发出了公开道歉信。在接下来的十多天里,金山、网易 京东、当当、新浪等多家公司被卷入到这次事件中。整个事件中最触目惊心的莫过于 CSDN 把用户密码明文存储,由于很多用户是多个网站共用一个密码,因此一个网站密码泄漏就会造成很大的安全隐患。 由于有了这么多前车之鉴,我们现在做系统时,密码都要加密处理

在前面的案例中,凡是涉及密码的地方,我们都采用明文存储,在实际项目中这肯定是不可取的,因为这会带来极高的安全风险。在企业级应用中,密码不仅需要加密,还需要加盐,最大程度地保证密码安全。

  • 常见方案

    • Hash 算法

    最早我们使用类似 SHA-256、SHA-512、MD5 等这样的单向 Hash 算法。用户注册成功后,保存在数据库中不再是用户的明文密码,而是经过 SHA-256 加密计算的一个字行串,当用户进行登录时,用户输入的明文密码用 SHA-256 进行加密,加密完成之后,再和存储在数据库中的密码进行比对,进而确定用户登录信息是否有效。如果系统遭遇攻击,最多也只是存储在数据库中的密文被泄漏

    这样就绝对安全了吗?由于彩虹表这种攻击方式的存在以及随着计算机硬件的发展,每秒执行数十亿次 HASH 计算己经变得轻轻松松,这意味着即使给密码加密加盐也不再安全

    彩虹表

    • 单向自适应函数

    在Spring Security中,我们现在是用一种自适应单向函数(AdaptiveOne-wayFunctions)来处理密码问题,这种自适应单向函数在进行密码匹配时,会有意占用大量系统资源(例如 CPU、内存等),这样可以增加恶意用户攻击系统的难度,在 Spring Securiy 中,开发者可以通过 bcrypt、PBKDF2、sCrypt 以及 argon2 来体验这种自适应单向函数加密。由于自适应单向函数有意占用大量系统资源,因此每个登录认证请求都会大大降低应用程序的性能,但是 Spring Secuity 不会采取任何措施来提高密码验证速度,因为它正是通过这种方式来增强系统的安全性

  • 密码介绍

参考1:https://byronhe.gitbooks.io/libsodium/content/password_hashing

参考2:https://github.com/xitu/gold-miner/blob/master/TODO1/password-hashing-pbkdf2-scrypt-bcrypt-and-argon2.md

  • BCryptPasswordEncoder

BCryptPasswordEncoder 使用 bcrypt 算法对密码进行加密,为了提高密码的安全性,bcrypt 算法故意降低运行速度,以增强密码破解的难度。同时 BCryptPasswordEncoder “为自己带盐” 开发者不需要额外维护一个 “盐” 字段,使用 BCryptPasswordEncoder 加密后的字符串就已经 “带盐” 了,即使相同的明文每次生成的加密字符串都不相同

  • Argon2PasswordEncoder

Argon2PasswordEncoder 使用 Argon2 算法对密码进行加密,Argon2 曾在 Password Hashing Competition竞赛中获胜。为了解决在定制硬件上密码容易被破解的问题, Argon2 也是故意降低运算速度,同时需要大量内存,以确保系统的安全性

  • Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder 使用 PBKDF2 算法对密码进行加密,和前面几种类似,PBKDF2 算法也是一种故意降低运算速度的算法,当需要 FIPS(Federal Information Processing Standard,美国联邦信息处理标准)认证时,PBKDF2算法是一个很好的选择

  • SCryptPasswordEncoder

SCryptPasswordEncoder 使用 scrypt 算法对密码进行加密,和前面的几种类似,scrypt 也是一种故意降低运算速度的算法,而且需要大量内存

7.1 检验密码源码分析

PasswrodEncoder 与 DelegatingPasswordEncoder 关系

  • 首先,我们来进行一下认证的源码分析,根据前面介绍,对于用户的认证是由 ProviderManager 去进行校验的

在这里插入图片描述

  • 再认证的过程中,对于密码的匹配调用了 DaoAuthenticationProvider 类中的 additionalAuthenticationChecks() 方法。(这里并不是 AbstractUserDetailsAuthenticationProvider 中的 additionalAuthenticationChecks() 方法,而是 DaoAuthenticationProvider 继承了 AbstractUserDetailsAuthenticationProvider)如下图所示

在这里插入图片描述
在这里插入图片描述

可以看到这里的调用了this.passwordEncoder.matches(presentedPassword, userDetails.getPassword()),这个this.passwordEncoder 是 PasswordEncoder 接口

在这里插入图片描述

接口 PasswordEncoder 有诸多实现类,都是不同类型的密码匹配规则

在这里插入图片描述

继续debug,发现这个PasswordEncoder 实现类叫做 DelegatingPasswordEncoded

在这里插入图片描述

进入这个 DelegatingPasswordEncoded 代理类实现的matches()方法,发现这个代理 PasswordEncoder 根据密文的前缀{noop}来获取真正用来加密的实现类(典型的策略设计模式

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

PasswrodEncoder 接口

public interface PasswordEncoder {

    String encode(CharSequence rawPassword);

    boolean matches(CharSequence rawPassword, String encodedPassword);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}
  • encode:用来进行明文加密
  • matches:用来比较密码的方法
  • upgradeEncoding:用来给密码进行升级的方法

在这里插入图片描述

7.2 使用自定义加密方式(一)

bcrypt 方式加密

  1. 在测试类中,对我们输入的明文密码进行加密并输出
@Test
void contextLoads() {
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    // 参数是用户输入的明文
    String encode = bCryptPasswordEncoder.encode("123");
    System.out.println(encode);
    // 这里的"10"代表散列次数,在创建 BCryptPasswordEncoder 时,可以设置参数
    // 第一次加密: $2a$10$oCDDewSNLloZlagnFW0SYexBBBl6QnXwspj2..WZGNwO7tv7AbTPy
    // 第二次加密: $2a$10$EeuYmlPFpuxJHDTe.jXRzuUrDRu0YrChz4NqUVu5n6BqTZgtc/aoS
    // 第三次加密: $2a$10$AiUtLkjsyQ7m3oHVu6hZs.10/lf4ibUDs8ScmSC.XXRNNywsv8CjS
}
  1. 在数据库中修改对应的用户密码(注意:前缀值与加密的方式保持一致

在这里插入图片描述

  1. 在页面中进行登录测试,用例:
    • 用户名:root
    • 密码:123(这里输入明文,数据库存储的是密文)

7.3 使用自定义加密方式(二)

WebSecurityConfigurerAdapter -> AuthenticationManager -> PasswordEncoder -> DelegatingPasswordEncoder -> PasswordEncoderFactories

在这里插入图片描述

在这里插入图片描述

DelegatingPasswordEncoder

根据上面 PasswordEncoder 的介绍,可能会以为 SpringSecurity 中默认的密码加密方案应该是四种自适应单向加密函数中的一种,其实不然,在 SpringSecurity 5.0 之后,默认的密码加密方案其实是 DelegatingPasswordEncoder。从名字上来看, DelegatingPaswordEncoder 是一个代理类,而并非一种全新的密码加密方案,DelegatingPaswordEncoder 主要用来代理上面介绍的不同类型的密码加密方案。为什么采用 DelegatingPasswordEncoder 而不是某一个具体加密方式作为默认的密码加密方案呢?主要考虑了如下两方面的因素:

  • 兼容性:使用 DelegatingPasswrordEncoder 可以帮助许多使用旧密码加密方式的系统顺利迁移到 SpringSecurity 中,它允许在同一个系统中同时存在多种不同的密码加密方案
  • 便捷性:密码存储的最佳方案不可能一直不变,如果使用 DelegatingPasswordEncoder 作为默认的密码加密方案,当需要修改加密方案时,只需要修改很小一部分代码就可以实现

通过源码分析得知,如果再工厂中指定了 PasswordEncoder,就会使用指定 PasswordEncoder,否则就会使用默认 DelegatingPasswordEncoder,在 WebSecurityConfigurerAdapter 配置类中注入指定加密方式即可!

  • 使用固定的加密方案,这时数据库存储的密码无需添加加密方案的前缀
    // 指定单一的加密方式
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // 注意密码不同区别
    @Bean
    public UserDetailsService userDetailsService() {
        // 定义内存用户信息管理者对象,将用户信息存储在内存当中
        InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
        // 创建用户对象
        UserDetails userDetails = User.withUsername("aaa").password("$2a$10$AiUtLkjsyQ7m3oHVu6hZs.10/lf4ibUDs8ScmSC.XXRNNywsv8CjS").roles("admin").build();
        // 将用户对象交由用户管理者去管理
        userDetailsService.createUser(userDetails);
        return userDetailsService;
    }
  • 使用灵活密码加密方案(推荐),这时数据库存储的密码需要添加加密方案的前缀

7.4 密码自动升级

推荐使用 DelegatingPasswordEncoder 的另外一个好处就是自动进行密码加密方案的升级,这个功能在整合一些老的系统时非常有用

  • 对源码分析,我们发现在完成密码认证之后,进行了是否进行密码更新的操作,如下图
  • 在完成认证之后,返回的是createSuccessAuthentication()方法

在这里插入图片描述

  • 而这个 createSuccessAuthentication() 是抽象类 AbstractUserDetailsAuthenticationProvider 中一个受保护(protected)的方法

在这里插入图片描述

  • 真正执行这个方法的是 DaoAuthenticationProvider,它继承了上面这个抽象类,并对 createSuccessAuthentication() 方法进行了重写
  • 根据判断 UserDetailsPasswordService 是否为null PasswordEncoder 对象是否需要进行 upgradeEncoding() 操作,去实现密码的更新

在这里插入图片描述

  • 更新的接口 UserDetailsPasswordService 中的方法

在这里插入图片描述

总结

对原有的密码进行升级(认证之后会随着框架的版本升级进行默认算法的更新),我们只需要同时对 UserDetailsServiceUserDetailsPasswordService 进行实现即可,这需要我们手动去配置才会生效,如果不配置不会生效

  • 重写 DivUserDetailsService 方法
package com.vinjcent.config.security;

import com.vinjcent.pojo.Role;
import com.vinjcent.pojo.User;
import com.vinjcent.service.RoleService;
import com.vinjcent.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.util.List;

@Component
public class DivUserDetailsService implements UserDetailsService, UserDetailsPasswordService {

    // dao ===> springboot + mybatis
    private final UserService userService;

    private final RoleService roleService;

    @Autowired
    public DivUserDetailsService(UserService userService, RoleService roleService) {
        this.userService = userService;
        this.roleService = roleService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1.查询用户
        User user = userService.queryUserByUsername(username);
        if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确!");
        // 2.查询权限信息
        List<Role> roles = roleService.queryRolesByUid(user.getId());
        user.setRoles(roles);
        return user;
    }

    // 默认使用 DelegatingPasswordEncoder,对密码进行更新时,使用的时 Bcrypt 加密
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        Integer result = userService.updatePassword(user.getUsername(), newPassword);
        if (result > 0) {
            ((User) user).setPassword(newPassword);
        }
        return user;
    }
}

开启了自动更新密码的功能之后,认证会将原有的密码进行升级更新(原本是"{noop}123")

在这里插入图片描述

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Naijia_OvO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值