第五章:shiro密码加密

在涉及到密码存储问题上,应该加密/生成密码摘要存储,而不是存储明文密码。比如之前的600w csdn账号泄露对用户可能造成很大损失,因此应加密/生成不可逆的摘要方式存储。

5.1 编码/解码 

Shiro提供了base64和16进制字符串编码/解码的API支持,方便一些编码解码操作。Shiro内部的一些数据的存储/表示都使用了base64和16进制字符串。

String str = "hello";  
String base64Encoded = Base64.encodeToString(str.getBytes());  
String str2 = Base64.decodeToString(base64Encoded);  
Assert.assertEquals(str, str2);  //Junit4中的方法

通过如上方式可以进行base64编码/解码操作,更多API请参考其Javadoc。

String str = "hello";  
String hexEncoded = Hex.encodeToString(str.getBytes());  
String str2 = new String(Hex.decode(hexEncoded.getBytes()));  
Assert.assertEquals(str, str2);   

通过如上方式可以进行16进制字符串编码/解码操作,更多API请参考其Javadoc。

还有一个可能经常用到的类CodecSupport,提供了toBytes(str, "utf-8") / toString(bytes, "utf-8")用于在byte数组/String之间转换。

5.2 散列算法

散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和ID(即盐);这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难破解。

Shiro还提供了通用的散列支持:

String str = "hello";  
String salt = "123";  
//内部使用MessageDigest  
String simpleHash = new SimpleHash("MD5", str, salt,1024).toString();   

通过调用SimpleHash时指定散列算法,其内部使用了Java的MessageDigest实现。

HashedCredentialsMatcher实现密码验证服务

Shiro提供了CredentialsMatcher的散列实现HashedCredentialsMatcher,它只用于密码验证,且可以提供自己的盐,而不是随机生成盐,且生成密码散列值的算法需要自己写,因为能提供自己的盐。

1.ini配置(shiro-hashedCredentialsMatcher.ini)

[main]  
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher  
credentialsMatcher.hashAlgorithmName=md5  
credentialsMatcher.hashIterations=2  
credentialsMatcher.storedCredentialsHexEncoded=true  
myRealm=com.github.zhangkaitao.shiro.chapter5.hash.realm.MyRealm2  
myRealm.credentialsMatcher=$credentialsMatcher  
securityManager.realms=$myRealm   

1、通过credentialsMatcher.hashAlgorithmName=md5指定散列算法为md5,需要和生成密码时的一样;

2、credentialsMatcher.hashIterations=2,散列迭代次数,需要和生成密码时的意义;

3、credentialsMatcher.storedCredentialsHexEncoded=true表示是否存储散列后的密码为16进制,需要和生成密码时的一样,默认是base64;

此处最需要注意的就是HashedCredentialsMatcher的算法需要和生成密码时的算法一样。另外HashedCredentialsMatcher会自动根据AuthenticationInfo的类型是否是SaltedAuthenticationInfo来获取credentialsSalt盐。

2、自定义Realm继承AuthorizingRealm实现doGetAuthenticationInfo方法

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
    String username = "liu"; //用户名及salt1  
    String password = "202cb962ac59075b964b07152d234b70"; //加密后的密码  
    String salt2 = username ;  
   SimpleAuthenticationInfo ai =   new SimpleAuthenticationInfo(username, password, getName());  
    ai.setCredentialsSalt(ByteSource.Util.bytes(username+salt2)); //盐是用户名+随机数  
    return ai;  
}   

密码重试次数限制

如在1个小时内密码最多重试5次,如果尝试次数超过5次就锁定1小时,1小时后可再次重试,如果还是重试失败,可以锁定如1天,以此类推,防止密码被暴力破解。我们通过继承HashedCredentialsMatcher,且使用Ehcache记录重试次数和超时时间。

public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {  
       String username = (String)token.getPrincipal();  
        //retry count + 1  
        Element element = passwordRetryCache.get(username);  
        if(element == null) {  
            element = new Element(username , new AtomicInteger(0));  
            passwordRetryCache.put(element);  
        }  
        AtomicInteger retryCount = (AtomicInteger)element.getObjectValue();  
        if(retryCount.incrementAndGet() > 5) {  
            //if retry count > 5 throw  
            throw new ExcessiveAttemptsException();  
        }  
  
        boolean matches = super.doCredentialsMatch(token, info);  
        if(matches) {  
            //clear retry count  
            passwordRetryCache.remove(username);  
        }  
        return matches;  
}   

如上代码逻辑比较简单,即如果密码输入正确清除cache中的记录;否则cache中的重试次数+1,如果超出5次那么抛出异常表示超出重试次数了。

猜你喜欢

转载自www.cnblogs.com/deityjian/p/10780299.html