目前市面上流行密码安全管理有如下几种:
-
spring-security
提供的工具类BCryptPasswordEncoder
-
Shiro
提供的工具类SimpleHash
, 借助commons-lang3
提供的类RandomStringUtils
来加盐
注意:本文仅仅比较两种对数据进行加密的方式,不涉及集成到springboot中
当我们对两个字符串进行比较时,通常是: if(“123456”==“654321”),但是这两个库不一样,都提供了各自的专用类,使用专门的方法去比较
spring-security
提供的 BCryptPasswordEncoder
引入坐标:
<!-- BCryptPasswordEncoder 加密-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.7.3</version>
</dependency>
在这里我们做了这些操作先对数据 123456
加密 encode
一次,得到加密之后的字符串 hexString
。
接下来再一次使用数据 123456
,与刚才得到的字符串 hexString
执行匹配算法,
得到结果: 123456 与字符串 hexStrin 匹配。
@Test
public void testSpringSecurityEncryptPassword(){
String password = "123456";
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 第一次
String hexString = bCryptPasswordEncoder.encode(password);
System.out.println(hexString);
// 比较
boolean isMatch = bCryptPasswordEncoder.matches(password, hexString);
System.out.println(isMatch);
// 第二次
String hexString2 = bCryptPasswordEncoder.encode(password);
System.out.println(hexString2);
// 比较
boolean isMatch2 = bCryptPasswordEncoder.matches(password, hexString);
System.out.println(isMatch2);
}
这里我们为什么要这样演示呢?密码不能明文存在数据库中,只能存加密之后的数据。
试想一下,当用户登录时,用户会数据明文数据密码 123456
,我们先对此数据数据加密一次,然后将加密之后的字符串 hexString
, 存入数据库。
当用户再一次登录时,用户依然会输入明文密码 123456
,此时数据库中已经有了加密之后的hexString
,
再次执行对比,用户输入的 123456
执行加密,与存入数据库中的hexString
比较。如果匹配上,那么就说明输入的是同一个密码。
这样就实现了数据库存入加密后数据,即使是数据库拥有者也不知道用户的密码是什么。
在演示中,一共执行了2次,可以发现每一次生成的hexString都是不一样的,这样就保证了密码数据的随机安全性。
同时,在两次匹配match中,得到的结果都是true,又保证了输入同样的密码,可以与存入数据库中的字符串相等。
Shiro
提供的 SimpleHash
在maven 仓库中搜索Shiro
,会发现有很多跟 shiro 相关的包:
- shiro-core
- shiro-web
- shiro-spring
- shiro-ehcache 缓存
- shiro-cas 单点登录
我们的目的仅仅是对比 shiro 中的数据密码这一小部分 ,因此只需要引入这个坐标:
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.0</version>
</dependency>
- 加密:Shiro 提供的类
SimpleHash
可以对密码进行加密。类SimpleHash
主要有 3 个参数
- String algorithmName 算法
- Object source 被加密的数据
- Object salt 加密用的盐
- 匹配:Shiro 提供的类
SimpleCredentialsMatcher
专门用来对密码进行比较是否相等。类SimpleCredentialsMatcher
主要有2参数
- AuthenticationToken 用户token
- AuthenticationInfo 认证信息
shiro 比较密码的过程
与上面spring-security比较密码的过程
部分类似。第一个参数:shiro通过类
UsernamePasswordToken
允许我们创建第一个参数。
第二个参数: shiro通过接口AuthenticationInfo
允许我们创建第二个参数,并且直接提供了它的一个实现类SimpleAuthenticationInfo
,便于我们直接使用…
// 对密码 123456 按照 MD5 哈希算法进行了一次加盐(salt)操作
@SpringBootTest
class UserControllerTest {
@Test
public void testShiroCore(){
String username = "ifredom";
String password = "123456";
// 加盐(密)
String salt = RandomStringUtils.randomAlphanumeric(20);// 生成了一个20位长度的随机字符串
SimpleHash hash = new SimpleHash("md5", password, salt);
System.out.println(hash.toHex());
// shiro 提供 SimpleCredentialsMatcher 来比较密码是否相同,该方法需要2参数
SimpleCredentialsMatcher matcher = new SimpleCredentialsMatcher();
// 第一个参数 AuthenticationToken。这个类又必须提供用户名和密码。
AuthenticationToken token = new UsernamePasswordToken(username, password);
// 第二个参数 SimpleAuthenticationInfo。这个类又需要4参数:用户名,密码,字节类型salt,以及一个 realm 名称字符串 .
/// 注意:这个 realm 由自己实现,shiro 会从javaBean中查找,如果没有找到,那么不会使用 realm内部实现得验证规则。可以称之为更高优先级规则
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username, //用户名
password, //密码
ByteSource.Util.bytes(salt),// salt= username + salt
"realmName" //realm name
);
// 比较
boolean match = matcher.doCredentialsMatch(token, authenticationInfo);
if (match) {
System.out.println("密码相同");
}else{
System.out.println("密码不同");
}
}
}
hash: 534e3f1e836820220ed938c4337206a8
密码相同
shiro 相比较 spring-security 的比较过程真的麻烦了很多.
commons-lang3 提供的 RandomStringUtils
RandomStringUtils 中提供了很多静态方法:
@Test
public void testCommonLangRandomStringUtils() {
/*
* 从所有字符集中选择字符,产生5位长度的随机字符串,中文环境下是乱码
* 打印结果:𨱼𠋤တ
*/
String random1 = RandomStringUtils.random(5);
/*
* count 长度
* letters 生成的字符串是否包括字母字符
* numbers 生成的字符串是否包含数字字符
* 打印结果:rPjlaGrKco
*/
String random2 = RandomStringUtils.random(10, true, false);
/*
* 使用指定的字符生成5位长度的随机字符串,第二个参数如果NULL,则使用所有字符集
* 打印结果:eacdb
*/
String random3 = RandomStringUtils.random(5, new char[]{'a', 'b', 'c', 'd', 'e', 'f', '1', '2', '3'});
/*
* 生成指定长度的字母和数字组成的随机组合字符串
* 打印结果:RQ7WX
*/
String random4 = RandomStringUtils.randomAlphanumeric(5);
/*
* 创建一个随机字符串,其长度介于包含最小值和最大最大值之间,字符将从拉丁字母(a-z、A-Z)和数字0-9中选择。
* minLengthInclusive :要生成的字符串的包含最小长度
* maxLengthExclusive :要生成的字符串的包含最大长度
* 打印结果:F5VFlEVLSIgtSo4dEBXcXTja
*/
String random5 = RandomStringUtils.randomAlphanumeric(5, 68);
/*
* 生成随机数字字符串
* 打印结果:25735
*/
String random6 = RandomStringUtils.randomNumeric(5);
/*
* 创建一个随机字符串,其长度介于包含最小值和最大最大值之间,将从数字字符集中选择字符.
* minLengthInclusive, 要生成的字符串的包含最小长度
* maxLengthExclusive 要生成的字符串的包含最大长度
* 打印结果:710920665944410618
*/
String random7 = RandomStringUtils.randomNumeric(15, 20);
/*
* 生成随机[a-z,A-Z]字符串,包含大小写
* 打印结果:HzVus
*/
String random8 = RandomStringUtils.randomAlphabetic(5);
/*
* 创建一个随机字符串,其长度介于包含最小值和最大最大值之间,,字符将从拉丁字母(a-z、A-Z的选择)。
* minLengthInclusive :要生成的字符串的最小长度
* maxLengthExclusive :要生成的字符串的最大长度
* 打印结果:MIwdfReXxEoi
*/
String random9 = RandomStringUtils.randomAlphabetic(2, 15);
/*
* 生成ASCII值从32到126组成的随机字符串
* 打印结果:}t;r
*/
String random10 = RandomStringUtils.randomAscii(4);
/*
* 创建一个随机字符串,其长度介于包含最小值和最大最大值之间,字符将从ASCII值介于32到126之间的字符集中选择(包括)
* minLengthInclusive :要生成的字符串的包含最小长度
* maxLengthExclusive :要生成的字符串的包含最大长度
* 打印结果:ye{.jAaF`p#mjf>Apo
*/
String random11 = RandomStringUtils.randomAscii(15, 30);
}