此篇博客根据之前写的shiro快速配置延续的,建议不了解的可以先看看之前的博客。
1.为了使用密码加密,我们新建一个对用户信息操作的工具类
package com.bf.planner.util;
import org.apache.shiro.crypto.SecureRandomNumberGenerator;
import org.apache.shiro.crypto.hash.SimpleHash;
import com.bf.planner.model.User;
public class EncryptUtil {
private static String algorithmName = "MD5"; // 加密方式
private static int hashIterations = 1024; // 加密次数
public static User EncryptUser(User user) {
// 随机盐对象
SecureRandomNumberGenerator secureRandomNumberGenerator = new SecureRandomNumberGenerator();
// 根据用户名以及随机生成的盐来拼接成密码盐
String salt = user.getUser_name() + secureRandomNumberGenerator.nextBytes().toHex();
// 获取加密后的密码
String passWord = new SimpleHash(algorithmName, user.getPassword(), salt, hashIterations).toHex();
user.setSalt(salt); //设置密码盐
user.setPassword(passWord); //设置加密过后的密码
return user;
}
}
tips:加密方式可以修改,推荐使用MD5等散列式加密方式,因为此种加密方式不可逆;密码盐我是使用用户名拼接了生成的随机盐,此处可根据需求添加私盐,或定义固定盐,推荐使用私盐加随机盐;密码盐一定要存放起来,并且不要使用ByteSource.Util.bytes()方法进行转译,因为后面我们登录时shiro需要对其进行此方法的转译。
2.用户注册或修改信息时,调用上面的工具类进行盐的设置以及密码的加密:
EncryptUtil.EncryptUser(user);
tips:此处我们传递过去的其实是user对象的引用,所以不需要接收返回值。
3.密码加密已经完成,接下来我们需要对其解密,首先在springmvc配置中配置密码解析
<!-- 数据库保存的密码是使用MD5算法加密的,所以这里需要配置一个密码匹配对象 -->
<bean id="credentialsMatcher" class="com.bf.planner.realm.MyMatcher">
<constructor-arg ref="cacheManager"/> <!-- 缓存 -->
<property name="hashAlgorithmName" value="MD5"></property> <!-- 加密算法的名称 -->
<property name="hashIterations" value="1024"></property> <!-- 配置加密的次数 -->
<property name="storedCredentialsHexEncoded" value="true"></property> <!-- 是否存储为16进制 -->
</bean>
<!-- 自定义Realm -->
<bean id="myRealm" class="com.bf.planner.realm.MyRealm">
<property name="credentialsMatcher" ref="credentialsMatcher" /> <!-- 加密配置 -->
</bean>
tips:上面自定义了credentialsMatcher的实现类是为了做账户锁定,如果不需要的话,只要将class引向
org.apache.shiro.authc.credential.HashedCredentialsMatcher
类,去掉缓存的配置即可。这里的配置是为了在用户登录时对用户输入的密码的加密配置,因为上面我使用的是MD5加密,加密了1024次,所以此处配置这样。
4.配置基本完成,要想使用我们还需要在MyRealm中修改:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String user_name = (String) token.getPrincipal();// 获取登录者的用户名
User user = loginService.getUser(user_name); // 根据用户名查出数据库中所对应的用户信息
if (user == null) {
throw new UnknownAccountException(); // 抛出账号不存在异常
}
AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getUser_name(), // 用户名
user.getPassword(), // 密码
ByteSource.Util.bytes(user.getSalt()), // 盐
getName()); // realm name
// 设置此用户的令牌,即设置正确的用户名与密码
return authcInfo;
}
tips:方法与原来的不同是在登录的验证处,我们多传递了一个盐,这是为了shiro对密码进行加密使用。
5.有的时候我们希望在用户多次输入错误密码时对其进行锁定操作,首先我们可以定义一个控制锁定密码时间的关于缓存的配置文件:
springmvc配置文件中引向我们的缓存配置文件:
<!-- 缓存管理 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml" />
</bean>
tips:此处指向了项目下的文件。
在ehcache.xml中我们配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="shirocache" updateCheck="false"> <!-- updateCheck关闭网络获取缓存 -->
<diskStore path="java.io.tmpdir" />
<!-- 登录记录缓存 锁定10分钟 -->
<cache name="passwordRetryCache" eternal="false"
timeToIdleSeconds="0" timeToLiveSeconds="600" overflowToDisk="false"
maxBytesLocalHeap="10M" statistics="true">
</cache>
<!-- timeToIdleSeconds 此属性设置后,限制时间以上次访问开始
eg:设置时间为10分钟的话,我们在1分的时候账号被锁定,预定为10分时解锁.而我们在5分时再次输入密码(此时密码已被锁定,无论怎样都不会通过认证),那么解锁时间变为15分.
timeToLiveSeconds 此属性设置后,限制时间以缓存创建开始
eg:设置时间为10分钟的话,我们在1分的时候账号被锁定,预定为10分时解锁.而我们在5分时再次输入密码(此时密码已被锁定,无论怎样都不会通过认证),那么解锁时间还是10分.
maxBytesLocalHeap用来限制缓存所能使用的堆内存的最大字节数的,如果不设置则需设置另外一个属性,否则项目会编译出错,无法允许,此处不再详写。
-->
</ehcache>
tips:updateCheck属性默认为true,有时会抛出异常Update check failed,建议设置为false。
6.配置完之后我们需要写我们自己的MyMatcher类:
package com.bf.planner.realm;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
/**
* @ClassName: MyMatcher
* @Description: TODO(shiro密码配置)
* @author HYK
* @date 2017年3月9日 下午1:18:02
*/
public class MyMatcher extends HashedCredentialsMatcher {
private Cache<String, AtomicInteger> passwordRetryCache; //创建缓存的对象
public MyMatcher(CacheManager cacheManager) {
//赋予缓存对象,此处获取的是我们在ehcache.xml文件中配置,注意getCache("")获取的是xml中的name
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal(); //获取用户名
AtomicInteger retryCount = passwordRetryCache.get(username); //获取用户登录的次数
if (retryCount == null) { //如果用户未登陆过
retryCount = new AtomicInteger(0); //新建一个登录次数
passwordRetryCache.put(username, retryCount); //放入缓存中
}
if (retryCount.incrementAndGet() > 3) { //如果用户登录次数超过三次(此处可根据需要自定义)
throw new ExcessiveAttemptsException(); //抛出用户锁定异常类
}
boolean matches = super.doCredentialsMatch(token, info); //判断用户是否可用,即是否为正确的账号密码
if(matches){
passwordRetryCache.remove(username); //移除缓存中用户的登录次数
}
return matches;
}
}
tips:引入类为org.apache.shiro下的;用户登录时的验证方法为doCredentialsMatch(token,info),此方法中将用户输入的令牌token和正确的令牌info进行验证。有兴趣的可以查看一下它的源码。在方法中它将info中的salt(盐)获取出来,然后根据我们在配置文件中的配置对用户输入的密码进行加密处理,然后对两者进行比较,比较一致则返回true,表示登录成功,否则返回false,表示登录失败。
7.登陆时我们需要捕获用户登录的异常:
@RequestMapping("/login")
@ResponseBody
public ResultInfo login(User user) {
ResultInfo resultInfo = new ResultInfo();
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUser_name(), user.getPassword());
try {
subject.login(token);
User userInfo = loginService.getUser(user.getUser_name());
subject.getSession().setAttribute("user_id", userInfo.getUser_id());
resultInfo.setCode(true); // 登录成功
} catch (UnknownAccountException e) {
resultInfo.setCode(false);
resultInfo.setMsg("用户名/密码错误");
} catch (IncorrectCredentialsException e) {
resultInfo.setCode(false);
resultInfo.setMsg("用户名/密码错误");
} catch (ExcessiveAttemptsException e) {
resultInfo.setCode(false);
resultInfo.setMsg("登录失败多次,账户锁定10分钟");
} catch (Exception e) {
e.printStackTrace();
resultInfo.setCode(false);
resultInfo.setMsg("其他错误");
}
return resultInfo;
}