Spring Boot 中密码加密的两种姿式!

先说一句:密码是没法解密的。你们也不要再问松哥微人事项目中的密码怎么解密了!前端

密码没法解密,仍是为了确保系统安全。今天松哥就来和你们聊一聊,密码要如何处理,才能在最大程度上确保咱们的系统安全。vue

本文是 Spring Security 系列的第 20 篇,阅读本系列前面的文章有助于更好的理解本文:java

  1. 挖一个大坑,Spring Security 开搞!
  2. 松哥手把手带你入门 Spring Security,别再问密码怎么解密了
  3. 手把手教你定制 Spring Security 中的表单登陆
  4. Spring Security 作先后端分离,咱就别作页面跳转了!通通 JSON 交互
  5. Spring Security 中的受权操做原来这么简单
  6. Spring Security 如何将用户数据存入数据库?
  7. Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!
  8. Spring Boot + Spring Security 实现自动登陆功能
  9. Spring Boot 自动登陆,安全风险要怎么控制?
  10. 在微服务项目中,Spring Security 比 Shiro 强在哪?
  11. SpringSecurity 自定义认证逻辑的两种方式(高级玩法)
  12. Spring Security 中如何快速查看登陆用户 IP 地址等信息?
  13. Spring Security 自动踢掉前一个登陆用户,一个配置搞定!
  14. Spring Boot + Vue 先后端分离项目,如何踢掉已登陆用户?
  15. Spring Security 自带防火墙!你都不知道本身的系统有多安全!
  16. 什么是会话固定攻击?Spring Boot 中要如何防护会话固定攻击?
  17. 集群化部署,Spring Security 要如何处理 session 共享?
  18. 松哥手把手教你在 SpringBoot 中防护 CSRF 攻击!so easy!
  19. 要学就学透彻!Spring Security 中 CSRF 防护源码解析

1.为何要加密

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

此次泄密,也留下了一些有趣的事情,特别是对于广大程序员设置密码这一项。人们从 CSDN 泄密的文件中,发现了一些好玩的密码,例如以下这些:程序员

  • ppnn13%dkstFeb.1st 这段密码的中文解析是:娉娉袅袅十三余,豆蔻梢头二月初。
  • csbt34.ydhl12s 这段密码的中文解析是:池上碧苔三四点,叶底黄鹂一两声
  • ...

等等不一而足,你会发现不少程序员的人文素养仍是很是高的,让人啧啧称奇。github

2.加密方案

密码加密咱们通常会用到散列函数,又称散列算法、哈希函数,这是一种从任何数据中建立数字“指纹”的方法。算法

散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来,而后将数据打乱混合,从新建立一个散列值。散列值一般用一个短的随机字母和数字组成的字符串来表明。好的散列函数在输入域中不多出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。数据库

咱们经常使用的散列函数有 MD5 消息摘要算法、安全散列算法(Secure Hash Algorithm)。后端

可是仅仅使用散列函数还不够,单纯的只使用散列函数,若是两个用户密码明文相同,生成的密文也会相同,这样就增长的密码泄漏的风险。安全

为了增长密码的安全性,通常在密码加密过程当中还须要加盐,所谓的盐能够是一个随机数也能够是用户名,加盐以后,即便密码明文相同的用户生成的密码密文也不相同,这能够极大的提升密码的安全性。

传统的加盐方式须要在数据库中有专门的字段来记录盐值,这个字段多是用户名字段(由于用户名惟一),也多是一个专门记录盐值的字段,这样的配置比较繁琐。

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

不一样于 Shiro 中须要本身处理密码加盐,在 Spring Security 中,BCryptPasswordEncoder 就自带了盐,处理起来很是方便。

3.实践

3.1 codec 加密

commons-codec 是一个 Apache 上的开源项目,用它能够方便的实现密码加密。松哥在 V 部落 项目中就是采用的这种方案(https://github.com/lenve/VBlog)。在 Spring Security 还未推出 BCryptPasswordEncoder 的时候,commons-codec 仍是一个比较常见的解决方案。

因此,这里我先来给你们介绍下 commons-codec 的用法。

首先咱们须要引入 commons-codec 的依赖:

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.11</version>
</dependency>

而后自定义一个 PasswordEncoder:

@Component
public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence rawPassword) {
        return DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes());
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes()));
    }
}

在 Spring Security 中,PasswordEncoder 专门用来处理密码的加密与比对工做,咱们自定义 MyPasswordEncoder 并实现 PasswordEncoder 接口,还须要实现该接口中的两个方法:

  1. encode 方法表示对密码进行加密,参数 rawPassword 就是你传入的明文密码,返回的则是加密以后的密文,这里的加密方案采用了 MD5。
  2. matches 方法表示对密码进行比对,参数 rawPassword 至关因而用户登陆时传入的密码,encodedPassword 则至关因而加密后的密码(从数据库中查询而来)。

最后记得将 MyPasswordEncoder 经过 @Component 注解标记为 Spring 容器中的一个组件。

这样用户在登陆时,就会自动调用 matches 方法进行密码比对。

固然,使用了 MyPasswordEncoder 以后,在用户注册时,就须要将密码加密以后存入数据库中,方式以下:

public int reg(User user) {
    ...
    //插入用户,插入以前先对密码进行加密
    user.setPassword(passwordEncoder.encode(user.getPassword()));
    result = userMapper.reg(user);
    ...
}

其实很简单,就是调用 encode 方法对密码进行加密。完整代码你们能够参考 V 部落(https://github.com/lenve/VBlog),我这里就不赘述了。

3.2 BCryptPasswordEncoder 加密

可是本身定义 PasswordEncoder 仍是有些麻烦,特别是处理密码加盐问题的时候。

因此在 Spring Security 中提供了 BCryptPasswordEncoder,使得密码加密加盐变得很是容易。只须要提供 BCryptPasswordEncoder 这个 Bean 的实例便可,微人事就是采用了这种方案(https://github.com/lenve/vhr),以下:

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(10);
}

建立 BCryptPasswordEncoder 时传入的参数 10 就是 strength,即密钥的迭代次数(也能够不配置,默认为 10)。同时,配置的内存用户的密码也再也不是 123 了,以下:

auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq")
.roles("ADMIN", "USER")
.and()
.withUser("sang")
.password("$2a$10$eUHbAOMq4bpxTvOVz33LIehLe3fu6NwqC9tdOcxJXEhyZ4simqXTC")
.roles("USER");

这里的密码就是使用 BCryptPasswordEncoder 加密后的密码,虽然 admin 和 sang 加密后的密码不同,可是明文都是 123。配置完成后,使用 admin/123 或者 sang/123 就能够实现登陆。

本案例使用了配置在内存中的用户,通常状况下,用户信息是存储在数据库中的,所以须要在用户注册时对密码进行加密处理,以下:

@Service
public class RegService {
    public int reg(String username, String password) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);
        String encodePasswod = encoder.encode(password);
        return saveToDb(username, encodePasswod);
    }
}

用户将密码从前端传来以后,经过调用 BCryptPasswordEncoder 实例中的 encode 方法对密码进行加密处理,加密完成后将密文存入数据库。

4.源码浅析

最后咱们再来稍微看一下 PasswordEncoder。

PasswordEncoder 是一个接口,里边只有三个方法:

public interface PasswordEncoder {
    String encode(CharSequence rawPassword);
    boolean matches(CharSequence rawPassword, String encodedPassword);
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}
  • encode 方法用来对密码进行加密。
  • matches 方法用来对密码进行比对。
  • upgradeEncoding 表示是否须要对密码进行再次加密以使得密码更加安全,默认为 false。

Spring Security 为 PasswordEncoder 提供了不少实现:

可是老实说,自从有了 BCryptPasswordEncoder,咱们不多关注其余实现类了。

PasswordEncoder 中的 encode 方法,是咱们在用户注册的时候手动调用。

matches 方法,则是由系统调用,默认是在 DaoAuthenticationProvider#additionalAuthenticationChecks 方法中调用的。

protected void additionalAuthenticationChecks(UserDetails userDetails,
        UsernamePasswordAuthenticationToken authentication)
        throws AuthenticationException {
    if (authentication.getCredentials() == null) {
        logger.debug("Authentication failed: no credentials provided");
        throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
    }
    String presentedPassword = authentication.getCredentials().toString();
    if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
        logger.debug("Authentication failed: password does not match stored value");
        throw new BadCredentialsException(messages.getMessage(
                "AbstractUserDetailsAuthenticationProvider.badCredentials",
                "Bad credentials"));
    }
}

能够看到,密码比对就是经过 passwordEncoder.matches 方法来进行的。

关于 DaoAuthenticationProvider 的调用流程,你们能够参考 SpringSecurity 自定义认证逻辑的两种方式(高级玩法)一文。

好了,今天就和小伙伴们简单聊一聊 Spring Security 加密问题,小伙伴们要是有收获记得点个在看鼓励下松哥哦~

相关文章
相关标签/搜索