编码 / 解码
Shiro 提供了 base64 和 16 进制字符串编码 / 解码的 API 支持,方便一些编码解码操作。Shiro 内部的一些数据的存储 / 表示都使用了 base64 和 16 进制字符串。
Base64:
16进制HEX
散列算法
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。
一般进行散列时最好提供一个 salt(盐),比如加密密码 “admin”,产生的散列值是 “21232f297a57a5a743894a0e4a801fc3”,可以到一些 md5 解密网站很容易的通过散列值得到密码 “admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如用户名和 ID(即盐);这样散列的对象是 “密码 + 用户名 +ID”,这样生成的散列值相对来说更难破解。
还可以指定散列次数:
加密在Realm中应该怎么用
首先,毋庸置疑的是,在你的真实项目中,插入用户密码的时候,需要先进行加密处理,再插入数据库的表。在验证用户密码的时候,再使用相同的加密算法计算用户输入的密码。
开始:
先计算出加密后的密码:就是存在数据库中的加密密码(123+盐+3次散列)
配置文件:shiro-decode.ini
[main] myrealm=com.lc.demo.EncodeRealm securityManager.realms=$myrealm
自定义的Realm:
public class EncodeRealm extends AuthorizingRealm { @Override public String getName() { return "myrealm"; } public EncodeRealm(){ //密码123在本类初始化时已经被MD5加密3次 //采用md5算法 HashedCredentialsMatcher passwordMatcher = new HashedCredentialsMatcher("md5"); //循环加密3次 passwordMatcher.setHashIterations(3); //再将这个加密组件注入到我们的Realm中 this.setCredentialsMatcher(passwordMatcher); } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String username =(String) authenticationToken.getPrincipal(); SimpleAuthenticationInfo simpleAuthenticationInfo= new SimpleAuthenticationInfo( username, "9d7281eeaebded0b091340cfa658a7e8", //模拟从数据库中拿到加密的密码(123+salt+3次散列) ByteSource.Util.bytes(username), //计算盐值 getName()); //就是上面的方法。获取realm的名字 return simpleAuthenticationInfo; //返回计算盐值加密后的密码的值.与红色部分对比, } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } }
测试代码:
@Test public void t3(){ //1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-encode.ini"); //2、得到SecurityManager实例 并绑定给SecurityUtils org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); //3、得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证) Subject subject = SecurityUtils.getSubject(); //验证密码123456是否能够登录成功 UsernamePasswordToken token = new UsernamePasswordToken("admin", "123"); try { //4、登录,即身份验证 subject.login(token); } catch (AuthenticationException e) { //5、身份验证失败 e.printStackTrace(); } Assert.assertEquals(true, subject.isAuthenticated()); //断言用户已经登录 //6、退出 subject.logout(); }
总结:
- 1. 为什么使用 MD5 盐值加密:
- 希望即使两个原始密码相同,但是加密得到的两个字符串也不同(数据库中存储)。
- 2. 如何做到:
- 1). 在 doGetAuthenticationInfo 方法返回值创建 SimpleAuthenticationInfo 对象的时候, 需要使用SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName) 构造器
- 2). 使用 ByteSource.Util.bytes() 来计算盐值.
- 3). 盐值需要唯一: 一般使用随机字符串或 user id
- 4). 使用 new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); 来计算盐值加密后的密码的值.
密码重试次数限制
如在 1 个小时内密码最多重试 5 次,如果尝试次数超过 5 次就锁定 1 小时,1 小时后可再次重试,如果还是重试失败,可以锁定如 1 天,以此类推,防止密码被暴力破解。我们通过继承 HashedCredentialsMatcher,且使用 Ehcache 记录重试次数和超时时间。
RetryLimitHashedCredentialsMatcher: 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 次那么抛出异常表示超出重试次数了。