Shiro 学习笔记(6)—— 加密
Shiro 有一个非常强大的功能,就是对加密算法的封装。在我们之前的例子中,我们的密码在 Realm 里返回认证信息的时候,写的都是明文。这样的方式其实是很不安全的。
一般地,密码这样的高度敏感的字段,我们应该是使用一种不可破解的算法加密以后存储到我们的数据库中。当用户登录的时候,在用户输入用户名正确(在库中存在)的前提下,将用户输入的密码使用同样的算法加密以后得到的字符串和数据库中的密文密码进行匹配,匹配成功,则认证通过。
这样处理的好处是,即使是可以接触生产环境的开发人员或者是数据库管理人员,都无法通过数据库信息知道真实用户的密码。即使是密码泄露给外部,别人也无法知道密码的明文是什么,极大地保护了用户信息的安全。
Shiro 提供了 PasswordService 及 CredentialsMatcher 用于提供加密密码及验证密码服务。
我们先来看一个例子:
PasswordService service = new DefaultPasswordService();
String str1 = service.encryptPassword("123456");
String str2 = service.encryptPassword("123456");
System.out.println(str1);
System.out.println(str2);
// 盐值是存放在加密以后的密码中的
boolean boolean1 = service.passwordsMatch("123456",str1);
System.out.println(boolean1);
boolean boolean2 = service.passwordsMatch("123456",str2);
System.out.println(boolean2)
我们发现,同样是“123456”这样的密码,使用 DefaultPasswordService 这个实现类的 encryptPassword() 方法加密以后得到的密文都是不一样的。但是我们可以使用 DefaultPasswordService 这个实现类的 passwordsMatch() 将明文密码与密文密码进行匹配,使用这个方法匹配是成功可靠的,这一点我们须要理解。
那么,我们如何在 Realm 中指定 Shiro 使用 DefaultPasswordService 这个实现类的 passwordsMatch() 方法进行密码的匹配呢?
我们可以使用依赖注入的方式,代码如下:
自定义 Realm 的认证实现部分:
public class MyPasswordRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = authenticationToken.getPrincipal().toString();
// String password = new String((char[]) authenticationToken.getCredentials());
// 此时我们应该从数据库中根据 username 查询出对应的密码
// String passwordFromDB = "$shiro1$SHA-256$500000$tSqKNsDkQ4hLsGwJ9JnNUw==$7UxMi+gx5OjIdu8knQdrWlB0RDfWDEF/LwxjL0JdDRs=";
// String passwordFromDB = "$shiro1$SHA-256$500000$79CtWg6E9u4Bxp0v7BNpxA==$eKAezOeAAQrS2cxTDJZJM8vwovgbCXrqRFSHvbgaMEY=";
String passwordFromDB = "$shiro1$SHA-256$500000$b282jV6ARlycFe0B4OaUQA==$9r6F9OdmO/kvy0ig0FZbEg9etkJ7hd+10CTS1rRjUdo=";
SimpleAuthenticationInfo info= new SimpleAuthenticationInfo(username,passwordFromDB,getName());
return info;
}
}
如果我们不指定 Realm 使用 PasswordMatcher 的实现类的话,默认的匹配方式就是使用字符串是否相同这样的匹配方式,显然不能达到我们的要求。所以我们要在 Realm 中指定 PasswordMatcher 的属性:
# 声明一个 Shiro 已经有的密码匹配的类
passwordMatcher=org.apache.shiro.authc.credential.PasswordMatcher
# 声明自定义的 Realm 类
myPasswordRealm=com.liwei.realm.MyPasswordRealm
# 将 passwordMatcher 注入到自定义的 Realm 类中
myPasswordRealm.credentialsMatcher=$passwordMatcher
# 将自定义的 Realm 注入到 securityManager 中
securityManager.realms=$myPasswordRealm
这样,密文密码匹配的功能就实现了。
实际应用中,我们喜欢自己定义加密的算法,例如 MD5 算法,我们还可以为 MD5 算法指定盐值,让 MD5 加密得到的密文更加安全。
我们首先介绍使用 MD5 算法,通过加盐的方式得到 “123456” 的密文。
String originPassword = "123456";
String salt = "hello";
String md5 = new Md5Hash(originPassword,salt).toString();
System.out.println(md5);
接下来编写自定义 Realm 中的认证实现部分。
public class MyPasswordRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = authenticationToken.getPrincipal().toString();
// String password = new String((char[]) authenticationToken.getCredentials());
// 此时我们应该从数据库中根据 username 查询出对应的密码
String passwordFromDB = "eeb9bad681184779aa6570e402d6ef6c";
String salt = "hello";
SimpleAuthenticationInfo info= new SimpleAuthenticationInfo(username,passwordFromDB,getName());
// 设置加密算法的盐值,使用 ByteSource 这个工具类
info.setCredentialsSalt(ByteSource.Util.bytes(salt.getBytes()));
return info;
}
}
最后,我们要在 shiro.ini 配置文件中配置密码匹配的类(配置加密算法和盐值):
# 声明一个 Shiro 已经有的密码匹配的类
hashMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
hashMatcher.hashAlgorithmName=md5
hashMatcher.hashSalted=hello
# 声明自定义的 Realm 类
myPasswordRealm=com.liwei.realm.MyPasswordRealm
# 将 passwordMatcher 注入到自定义的 Realm 类中
myPasswordRealm.credentialsMatcher=$hashMatcher
# 将自定义的 Realm 注入到 securityManager 中
securityManager.realms=$myPasswordRealm
到这里,加密的部分就简单介绍完成了,我们看到,主要还是通过配置的方式,使得我们的认证更加安全和规范。