一、背景
为了密码的安全性,密码要进行加密操作。通常我们使用的散列加密算法,其加密算法是不可逆的,即不能由密文推算出明文,所以即使数据库管理员或者入侵者知道了你存储在数据库里面的密码,也不会知道你真实的密码,这样就保证了你账户的安全性。
然而,单纯的只使用散列加密算法加密,就安全吗?
第一、虽然散列加密算法不能解密,但是却可以碰撞,从而得出原来的明文密码,如果你的密码是 1234 或者 abc 这样简单的密码,即使通过散列加密算法进行加密,也同样是可以被破解出来的。
第二、同一个程序,所使用的加密算法是一致的,假设注册者所拥有的初始密码是一致的,那么,当两个人都没有改变密码的时候,两者存储在数据库里面的密码是一致的。任何人都可以使用 A、B 的账号 + 初始密码登录进去。所以使用散列加密算法也不是很安全的,我们希望能有一个标识,在使用散列加密算法加密的时候可以用到这个标识,这个标识应该是唯一的,如用户的 id 或者用户的唯一编号等。这个标识,用专业的术语描述叫做盐(salt)。
通常我们使用的散列加密有:MD5、SHA等。
二、盐值加密
举个盐值加密的例子:用户名:admin 密码:123456 盐值:yanzhi1234 用 md5 加密:md5(admin123456yanzhi1234)
方便区分这里我加上颜色,我们可以使用 md5 加密这样的字符串,破解难度就显而易见了,当然你可以自由搭配这个加密串,如: md5(adminmd5(123456)yanzhi1234)。也可以多层加密,盐值就是一串随机的字符串,建议每个用户的盐值都不一样,长度自己把控,建议长一些,再迭代加密几次增加破解难度。等到验证的时候以同样的方式加密后对比就可以了。
三、用户注册
以我们第二篇的文章为例,如果想要使用 shiro 的盐值加密,则用户存储在数据库表中的密码必须是加密后的数据。先看下面的代码:
@Component
public class InsertUser implements InitializingBean{
@Autowired
UserService userService;
@Override
public void afterPropertiesSet() throws Exception {
userService.deleteAll();
User user = new User();
// 这里我们假设username+"salt"的值为盐值。
String salt1 = "zhangsan";
String password1 = "123456";
String encryp_password1 = MD5Pwd(salt1,password1);
user.setUserName(salt1);
user.setPassword(encryp_password1);
user.setRoleIds("1,2");
user.setStatus("1");
userService.insert(user);
User user2 = new User();
String salt2 = "lisi";
String password2 = "123456";
String encryp_password2 = MD5Pwd(salt2,password2);
user2.setUserName(salt2);
user2.setPassword(encryp_password2);
user2.setRoleIds("2");
user2.setStatus("1");
userService.insert(user2);
User user3 = new User();
String salt3 = "wangwu";
String password3 = "123456";
String encryp_password3 = MD5Pwd(salt3,password3);
user3.setUserName(salt3);
user3.setPassword(encryp_password3);
user3.setRoleIds("1");
user3.setStatus("1");
userService.insert(user3);
}
public static String MD5Pwd(String username, String pwd) {
// 获取盐值
ByteSource salt = ByteSource.Util.bytes(username + "salt");
/*
* SimpleHash()方法是shiro为我们提供的api,对原始密码进行加密。
* 第一个参数表示使用MD5方式进行加密
* 第二个参数表示原始密码
* 第三个参数表示盐值
* 第四个参数表示加密的次数
* 最后用toHex()方法将加密后的密码转换成String类型
* */
String md5Pwd = new SimpleHash("MD5",pwd,salt,2).toHex();
return md5Pwd;
}
}
我们初始化模拟用户注册的场景,将用户的账号和密码存储到数据库表里面,其中用户的密码需要进行加密存储,说白了就是使用 MD5 加密算法然后再加上盐值就可以了,我以前老是不明白这个盐值是不是该存储到数据库表里面,后来翻阅了大量的资料,了解到,这个盐值的组成可以使用用户的 username 或者是其他的字段,然后再拼接个字符串啥的,基本上也就够了。如果你生成了一个随机的字符串作为盐值,或者作为盐值的一部分,则需要将这个随机的字符串存储到数据库表中,要不你校验的时候盐值就拿不到了。
在上面的代码中,我们的创建的盐值由 username+"salt" 构成,并迭代了两次,通过 MD5 加密算法最终得到了加密后的字符串。然后将该字符串存储到数据库的表中。
四、密码校验
首先需要在我们的 ShiroConfig 类中配置支持盐值加密的 bean,如下所示:
// 将自己的验证方式加入容器
@Bean
public CustomRealm myShiroRealm() {
CustomRealm customRealm = new CustomRealm();
// 告诉realm,使用credentialsMatcher加密算法类来验证密文
customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return customRealm;
}
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数,比如散列两次,相当于 md5(md5(""));
hashedCredentialsMatcher.setHashIterations(2);
// storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
看一下我们自定义实现的 CustomRealm 类的 doGetAuthenticationInfo 方法的实现,如下:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if(StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
// 获取用户信息
String userName = authenticationToken.getPrincipal().toString();
User user = userService.selectByUserName(userName);
// 用户是否存在
if(user == null) {
throw new UnknownAccountException();
}
// 是否激活
if(user !=null && user.getStatus().equals("0")){
throw new DisabledAccountException();
}
// 是否锁定
if(user!=null && user.getStatus().equals("3")){
throw new LockedAccountException();
}
// 若存在将此用户存放到登录认证info中,无需做密码比对shiro会为我们进行密码比对校验
if(user !=null && user.getStatus().equals("1")){
ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUserName()+ "salt");
/** 这里验证authenticationToken和simpleAuthenticationInfo的信息,构造方法支持三个或者四个参数,
* 第一个参数传入userName或者是user对象都可以。
* 第二个参数传入数据库中该用户的密码(记得是加密后的密码)
* 第三个参数传入加密的盐值,若没有则可以不加
* 第四个参数传入当前Relam的名字
**/
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, user.getPassword().toString(),credentialsSalt, getName());
return simpleAuthenticationInfo;
}
return null;
}