盐值是什么
首先我们来探讨一下关于密码安全的问题
在一个系统中我们通常会将用户名和密码存在数据库中,如果我们直接将密码存入数据库,会存在很大的安全隐患,比如:数据库被盗,传输过程中被黑客拦截,这都是很常见的问题,数据库所在的服务并不是绝对的安全,传输过程中也有可能被黑客拦截得到你传输的数据获得一些隐私的数据,并篡改后发往服务器。
那么针对这种问题我们如何解决呢,安全在升级,骇客的技术同样也在进步,一个网站竟然是给用户访问,就避免不了一些人钻空子,对于密码我们可以使用不可逆的散列算法来进行加密。虽说不可逆,也并不绝对,比如一些网站有很强大的库,如:CMD5、PMD5、SOMD5等等网站,他们并不能破解,但是他们有大量经过尝试加密存储在数据库的key-value对应解密的值。而我们要做的就是增加他们的破解难度,如何增加,这就是本章的重点。
通常我们使用的散列加密有:MD5、SHA。而盐值加密,举个栗子(伪代码)
用户名:admin
密码:123
盐值:qwe666
用md5加密:md5(admin123qwe666)
方便区分这里我加上颜色,我们可以用md5加密这样的字符串,破解难度就显而易见了,当然你还可以自由搭配这个加密串,如
md5(adminmd5(123)qwe666)
也可以加密多层,盐值就是一串随机的字符串,建议每个用户的盐值不一样,长度自己把控,建议长一些,再迭代加密几次增加破解难度。验证时同样的方式加密后对比就可以了。
shiro实现加密
当我们调用Subject的login方法时(关于Subject可以参考我之前的文章),shiro会调用SecurityManage安全管理器,尝试对比密码并登陆,而我们要做的就是自定义Realm,我们的角色、用户、权限都是从Realm得到的数据,也就是说SecurityManage认证就一定会走Realm取用户的一些信息,可以把它理解为数据源。
MyShiroRealm.java
public class MyShiroRealm extends AuthorizingRealm{
@Autowired
private AppUserService appUserService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
AppUser appUser = (AppUser)principals.getPrimaryPrincipal();
for(AppRole role:appUser.getRoleList()){
//添加角色
authorizationInfo.addRole(role.getRoleKey());
for(AppFn p:role.getAppFnList()){
//添加拥有的权限
authorizationInfo.addStringPermission(p.getFnKey());
}
}
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//UsernamePasswordToken对象用来存放提交的登录信息
UsernamePasswordToken token=(UsernamePasswordToken) authenticationToken;
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
AppUser user = appUserService.findByUsername(token.getUsername());
//用户是否存在
if(user==null){
throw new UnknownAccountException();
}
//是否激活
if(user!=null&&user.getStatus().equals(ConstantsUser.ZERO.getCode())){
throw new DisabledAccountException();
}
//是否锁定
if(user!=null&&user.getStatus().equals(ConstantsUser.THREE.getCode())){
throw new LockedAccountException();
}
//若存在,将此用户存放到登录认证info中,无需自己做密码对比Shiro会为我们进行密码对比校验
if(user!=null&&user.getStatus().equals(ConstantsUser.ONE.getCode())) {
//这里盐值可以自定义
ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUsername+user.getSalt());
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.getUsername(), //用户名
user.getPassword(), //密码
credentialsSalt,//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}
return null;
}
}
SimpleAuthenticationInfo(用户名,密码,盐值,当前的Realm)
我们将用户名基本信息存入这个类中,这里传入了我们的盐值,盐值可以自由定义,建议设置复杂些,当我们调用Subject的login方法,走到AuthenticatingRealm#getAuthenticationInfo方法时
这里会得到我们自己定义的Realm,接下来就是AuthenticatingRealm#assertCredentialsMatch这个方法
这里的doCredentialsMatch就是校验密码的方法,这里的CredentialsMatcher是提供了get/set方法的,点进CredentialsMatcher的实现看一下我,我们竟然要做散列加密就选择
这个类里已经有定义好的方法,包括加密需要指定的参数,而我们要做校验登录次数限制只用的到doCredentialsMatch方法,这时候我选择继承这个类重写doCredentialsMatch方法。
RetryLimitHashedCredentialsMatcher.java
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
private Cache<String, Integer> cache;
@Getter
@Setter
/**
* 自定义密码错误上限
*/
private Integer retryMax;
public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
cache = cacheManager.getCache("passwordRetryCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws ExcessiveAttemptsException {
String username = (String)token.getPrincipal();
Integer retryCount = cache.get(username);
if(retryCount == null) {
retryCount = new Integer(1);
cache.put(username, retryCount);
}
if(retryCount > retryMax) {
throw new ExcessiveAttemptsException("您已连续错误达" + retryMax + "次!请N分钟后再试");
}
if( cache.getClass().getName().contains("RedisCache")){
cache.put(username, ++retryCount);
}
//调用父类的校验方法
boolean matches = super.doCredentialsMatch(token, info);
if(matches) {
cache.remove(username);
}else {
throw new IncorrectCredentialsException("密码错误,已错误" + retryCount + "次,最多错误" + retryMax + "次");
}
return true;
}
}
代码中的有段super.doCredentialsMatch(token,info)又重新调用了父类的方法,这对其原本的代码进行了增强,在设计模式中
我们叫它装饰者模式,当然这还没完,我们只是定义了这个类,并没有将他给注入进去,想要实用起来还需要改一下ShiroConfig.java
/**
* 自定义Realm创建
* @return
*/
@Bean
public MyShiroRealm myShiroRealm(CredentialsMatcher credentialsMatcher){
MyShiroRealm myShiroRealm = new MyShiroRealm();
//将自定义的令牌set到了Realm
myShiroRealm.setCredentialsMatcher(credentialsMatcher);
return myShiroRealm;
}
/**
* 交由SecurityManage管理
* @return
*/
@Bean
@DependsOn("credentialsMatcher")
public SecurityManager securityManager(CredentialsMatcher credentialsMatcher){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm(credentialsMatcher));
return securityManager;
}
/**
* 功能增强
* @param cacheManager
* @return
*/
@Bean(name = "credentialsMatcher")
public CredentialsMatcher credentialsMatcher(CacheManager cacheManager) {
RetryLimitHashedCredentialsMatcher credentialsMatcher = new RetryLimitHashedCredentialsMatcher(cacheManager);
//加密方式
credentialsMatcher.setHashAlgorithmName(properties.getAlgorithmName());
//加密迭代次数
credentialsMatcher.setHashIterations(properties.getIteration());
//true加密用的hex编码,false用的base64编码
credentialsMatcher.setStoredCredentialsHexEncoded(properties.getHexEncoded());
//重新尝试的次数(自己定义的)
credentialsMatcher.setRetryMax(properties.getRetryMax());
return credentialsMatcher;
}
至此我们的整合完毕了,还有一个问题,注册或者添加用户名该怎么办呢?这里我们继续翻阅源码就是刚刚的HashedCredentialsMatcher#doCredentialsMatch方法,我们顺着往里面点
1、
2、
3、
到这里我们就看懂了SimpleHash(加密方式,盐值,加密次数,迭代次数)
String hashAlgorithmName = "MD5";//加密方式
Object crdentials = "123456";//密码原值
Object salt = "qwe";//盐值
int hashIterations = 1024;//加密1024次
String result = new SimpleHash(hashAlgorithmName,crdentials,salt,hashIterations).toBase64();
System.out.println(result);
得到密码后记得调用toBase64()方法,shiro校验时也是会toBase64的,至此关于加密源码的分析以及实现就完了。
大家多多关注我哟,我会经常写一些精彩的博文,与大家一同分享,一起进步。