Spring Boot 中密码加密的两种方法
作者:江南一点雨 发布时间:2021-09-12 15:08:38
先说一句:密码是无法解密的。大家也不要再问松哥微人事项目中的密码怎么解密了!
密码无法解密,还是为了确保系统安全。今天松哥就来和大家聊一聊,密码要如何处理,才能在最大程度上确保我们的系统安全。
1.为什么要加密
2011 年 12 月 21 日,有人在网络上公开了一个包含 600 万个 CSDN 用户资料的数据库,数据全部为明文储存,包含用户名、密码以及注册邮箱。事件发生后 CSDN 在微博、官方网站等渠道发出了声明,解释说此数据库系 2009 年备份所用,因不明原因泄露,已经向警方报案,后又在官网发出了公开道歉信。在接下来的十多天里,金山、网易、京东、当当、新浪等多家公司被卷入到这次事件中。整个事件中最触目惊心的莫过于 CSDN 把用户密码明文存储,由于很多用户是多个网站共用一个密码,因此一个网站密码泄露就会造成很大的安全隐患。由于有了这么多前车之鉴,我们现在做系统时,密码都要加密处理。
这次泄密,也留下了一些有趣的事情,特别是对于广大程序员设置密码这一项。人们从 CSDN 泄密的文件中,发现了一些好玩的密码,例如如下这些:
ppnn13%dkstFeb.1st 这段密码的中文解析是:娉娉袅袅十三余,豆蔻梢头二月初。
csbt34.ydhl12s 这段密码的中文解析是:池上碧苔三四点,叶底黄鹂一两声
...
等等不一而足,你会发现很多程序员的人文素养还是非常高的,让人啧啧称奇。
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 接口,还需要实现该接口中的两个方法:
encode 方法表示对密码进行加密,参数 rawPassword 就是你传入的明文密码,返回的则是加密之后的密文,这里的加密方案采用了 MD5。
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 加密问题,小伙伴们要是有收获记得点个在看鼓励下松哥哦~
来源:https://mp.weixin.qq.com/s/jBoU5j4YChDVwyX22SbtgA
猜你喜欢
- 1. 查找1) 顺序查找 SeqSearch.java2) 二分查找【二分法,放在算法讲解】2. 顺序查找有一个数列:白眉鹰王、金毛狮王、紫
- 1. MyBatis 中 #{}和 ${}的区别是什么?#{}是预编译处理,${}是字符替换。 在使用 #{}时,MyBatis 会将 SQ
- 一个中大型的 Java 项目往往包含若干 JAR 包,这些 JAR 包有着不同的版本号。如果这些 JAR 包单独发布,然后直接通过版本号引用
- Java类成员变量的默认值基本数据类型作为类的成员变量时可以不赋予初值,在方法中输出也不会报错。下面我们看看我们的八种数据类型的默认值是什么
- springboot上传文件大小的配置我这里记录两种,一种是设置在配置文件里只有两行代码,一种是加个Bean首先第一种:applicatio
- 在早期开发的时候,我们完成的都是静态页面也就是html页面,随着时间轴的发展,慢慢的引入了jsp页面,当在后端服务查询到数据之后可以转发到j
- 前提最近发现各个频道推荐了很多ULID相关文章,这里对ULID的规范文件进行解读,并且基于Java语言自行实现ULID,通过此实现过程展示U
- 概述透视表是依据已有数据源来创建的交互式表格,我们可在excel中创建透视表,也可编辑已有透视表。所需工具:Free Spire.XLS f
- 1.简述描述:1、对输入的字符串进行加解密,并输出。2、加密方法为:当内容是英文字母时则用该英文字母的后一个字母替换,同时字母变换大小写,如
- 太多的if-else不太直观,难以维护。 以下面代码为例,展示几种替代if else的方法。String input = &quo
- 之前学习 Java 的时候,感觉最难做的一件事情就是配置 jdk 的环境。那叫一个困难啊,Path, JAVA_HOME, CLASSPAT
- 本文研究的主要是Java编程Nashorn的相关内容,具体如下。Nashorn是什么Nashorn,发音“nass-horn”,是德国二战时
- 1. 字段取别名,和属性名保持一致映射文件<mapper namespace="com.atguigu.mybatis.ma
- 1. 将对象转换为JSON字符串,返回值为一个JSON字符串public static String toJson(Object value
- 1.WinMergeWinMerge是一款运行于Windows系统下的文件比较和合并工具https://winmerge.org/downl
- 一、介绍Properties文件在Java中主要为配置文件,文件类型为:.properties,格式为文本文件,内容格式为"键=值
- 一、关键字关键字:被Java语言赋予特定含义的单词。组成关键字的字母全部小写。注:goto和const作为保留字存在,目前并不使用。main
- 今天突然想起来,java产生随机数的问题,上机试了一下,找到了一点区别,在这里总结一下;直接调用Math.random()是产生一个[0,1
- 本文实例讲述了Java实现克隆的三种方式。分享给大家供大家参考,具体如下:1、浅复制(浅克隆)这种浅复制,其实也就是把被复制的这个对象的一些
- 1.前置准备默认服务器上的hadoop服务已经启动本地如果是windows环境,需要本地配置下hadoop的环境变量本地配置hadoop的环