shiro_实现分布式会话SessionManager、限制密码重试次数和并发登录控制
要解决的问题
解决方案
所有服务器的Session都存储到redis服务器中,通过redis实现Session共享。
QL(B6.pngQL(B6.png))
首先,继承AbstractSessionDAO并实现
java
package com.itheima.shiro.core.impl;
import com.itheima.shiro.constant.CacheConstant;
import com.itheima.shiro.utils.ShiroRedissionSerialize;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @Description:自定义统一sessiondao实现
*/
public class RedisSessionDao extends AbstractSessionDAO {
@Resource(name = "redissonClientForShiro")
RedissonClient redissonClient;
private long globalSessionTimeout;
/**
* @Description 创建session
* @param session 会话对象
* @return
*/
@Override
protected Serializable doCreate(Session session) {
//创建唯一标识的sessionId
Serializable sessionId = generateSessionId(session);
//为session会话指定唯一的sessionId
assignSessionId(session, sessionId);
//放入缓存中
String key = CacheConstant.GROUP_CAS+sessionId.toString();
RBucket<String> bucket = redissonClient.getBucket(key);
bucket.trySet(ShiroRedissionSerialize.serialize(session), globalSessionTimeout/1000, TimeUnit.SECONDS);
return sessionId;
}
/**
* @Description 读取sessio
* @param sessionId 唯一标识
* @return
*/
@Override
protected Session doReadSession(Serializable sessionId) {
String key = CacheConstant.GROUP_CAS+sessionId.toString();
RBucket<String> bucket = redissonClient.getBucket(key);
return (Session) ShiroRedissionSerialize.deserialize(bucket.get());
}
/**
* @Description 更新session
* @param session 对象
* @return
*/
@Override
public void update(Session session) throws UnknownSessionException {
String key = CacheConstant.GROUP_CAS+session.getId().toString();
RBucket<String> bucket = redissonClient.getBucket(key);
bucket.set(ShiroRedissionSerialize.serialize(session), globalSessionTimeout/1000, TimeUnit.SECONDS);
}
/**
* @Description 删除session
* @param
* @return
*/
@Override
public void delete(Session session) {
String key = CacheConstant.GROUP_CAS+session.getId().toString();
RBucket<String> bucket = redissonClient.getBucket(key);
bucket.delete();
}
/**
* @Description 统计当前活跃用户数(暂不实现)
* @param
* @return
*/
@Override
public Collection<Session> getActiveSessions() {
return Collections.emptySortedSet();
}
public void setGlobalSessionTimeout(long globalSessionTimeout) {
this.globalSessionTimeout = globalSessionTimeout;
}
}
在shiroConfig中配置会话管理器
java
/**
* @Description 会话管理器
*/
@Bean(name="sessionManager")
public DefaultWebSessionManager shiroSessionManager(){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 指定自定义的SessionDao
sessionManager.setSessionDAO(redisSessionDao());
sessionManager.setSessionValidationSchedulerEnabled(false);
sessionManager.setSessionIdCookieEnabled(true);
sessionManager.setSessionIdCookie(simpleCookie());
sessionManager.setGlobalSessionTimeout(shiroRedisProperties.getGlobalSessionTimeout());
return sessionManager;
}
/**
* 配置Session的超时时间
* @return
*/
@Bean("redisSessionDao")
public RedisSessionDao redisSessionDao(){
RedisSessionDao redisSessionDao = new RedisSessionDao();
redisSessionDao.setGlobalSessionTimeout(shiroRedisProperties.getGlobalSessionTimeout());
return redisSessionDao;
}
限制密码重试次数
- 获取系统中是否已有登录次数缓存,缓存对象结构预期为:用户名---登录次数
- 如果没有登录缓存,则创建一个登录次数缓存
- 如果缓存次数已超过限制,则驳回本次登录请求
- 将缓存记录的登录次数加1,设定指定时间内有效
- 验证用户本次输入的账号密码,如果登录成功,则清除登录次数缓存
自定义密码比较器
java
package com.itheima.shiro.core.impl;
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.redisson.api.RAtomicLong;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
/**
* @Description:自定义密码比较器
*/
public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher {
private RedissonClient redissonClient;
// 限制登录5次
private static Long RETRY_LIMIT_NUM = 4L;
public RetryLimitCredentialsMatcher(String hashAlgorithmName,RedissonClient redissonClient) {
super(hashAlgorithmName);
this.redissonClient = redissonClient;
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String loginName = (String) token.getPrincipal();
// 1、获取系统中是否已有登录次数缓存,缓存对象结构预期为:"用户名--登录次数"。
RAtomicLong atomicLong = redissonClient.getAtomicLong(loginName);
//2、获取登录次数,如果之前没有登录缓存,默认为0。
long retryFlat = atomicLong.get();
//判断是否超过次数
if (retryFlat>RETRY_LIMIT_NUM){
//3、如果缓存次数已经超过限制,则驳回本次登录请求。
// 10分钟后过期
atomicLong.expire(10, TimeUnit.MINUTES);
// 这里抛出异常需要前端进行处理
throw new ExcessiveAttemptsException("密码次数错误5次,请10分钟后重试");
}
//4、将缓存记录的登录次数加1,设置指定时间内有效
atomicLong.incrementAndGet();
atomicLong.expire(10, TimeUnit.MINUTES);
//5、验证用户本次输入的帐号密码,如果登录登录成功,则清除掉登录次数的缓存
boolean flag = super.doCredentialsMatch(token, info);
if (flag){
atomicLong.delete();
}
return flag;
}
}
使用自定义密码比较器
修改ShiroDbRealmImpl类中的initCredentialsMatcher方法
java
@Override
public void initCredentialsMatcher() {
//指定密码算法
RetryLimitCredentialsMatcher hashedCredentialsMatcher = new RetryLimitCredentialsMatcher(SuperConstant.HASH_ALGORITHM,redissonClient);
//指定迭代次数
hashedCredentialsMatcher.setHashIterations(SuperConstant.HASH_INTERATIONS);
//生效密码比较器
setCredentialsMatcher(hashedCredentialsMatcher);
}
在LoginAction中处理异常
java
/**
* @Description 登录操作
* @param loginVo 登录对象
* @return
*/
@RequestMapping(value="/usersLongin",method=RequestMethod.POST)
public ModelAndView usersLongin(LoginVo loginVo){
ModelAndView modelAndView = new ModelAndView("/account/login");
String shiroLoginFailure=null;
Map<String, String> map = new HashMap<String, String>();
try {
loginVo.setSystemCode(ShiroConstant.PLATFORM_MGT);
loginService.route(loginVo);
} catch (UnknownAccountException ex) {
log.error("登陆异常:{}",ex);
shiroLoginFailure="登录失败 - 账号不存在!";
map.put("loginName", loginVo.getLoginName());
map.put("shiroLoginFailure", shiroLoginFailure);
modelAndView.addAllObjects(map);
} catch (IncorrectCredentialsException ex) {
log.error("登陆异常:{}",ex);
shiroLoginFailure="登录失败 - 密码不正确!";
map.put("shiroLoginFailure", shiroLoginFailure);
map.put("loginName", loginVo.getLoginName());
modelAndView.addAllObjects(map);
} catch (ExcessiveAttemptsException ex) {
// 登录次数异常
log.error("登陆异常:{}",ex);
shiroLoginFailure=ex.getMessage();
map.put("shiroLoginFailure", shiroLoginFailure);
map.put("loginName", loginVo.getLoginName());
modelAndView.addAllObjects(map);
}catch (Exception ex) {
log.error("登陆异常:{}",ex);
shiroLoginFailure="登录失败 - 请联系平台管理员!";
map.put("shiroLoginFailure", shiroLoginFailure);
map.put("loginName", loginVo.getLoginName());
modelAndView.addAllObjects(map);
}
if (shiroLoginFailure!=null) {
return modelAndView;
}
modelAndView.setViewName("redirect:/menus/system");
return modelAndView;
}
在线账户控制
一个账号只允许同时一个在线,当账号在其他地方登录时,会踢出前面登录的账号。
实现思路:
- 先判断用户是否登录
- 使用RedissionClient创建队列
- 判断当前SessionID是否在此队列
- 不存在则放入队列尾端
- 存在则判断队列大小是否超过同时在线人数
- 如果超过:
- 从队列头部拿到用户SessionID
- 从SessionManager根据SessionID获得Session
- 从SessionDao中移除Session
- 未超过不执行操作
自定义过滤器
java
package com.itheima.shiro.core.filter;
import com.itheima.shiro.constant.CacheConstant;
import com.itheima.shiro.core.impl.RedisSessionDao;
import com.itheima.shiro.utils.EmptyUtil;
import com.itheima.shiro.utils.ShiroUserUtil;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.session.ExpiredSessionException;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.redisson.api.RDeque;
import org.redisson.api.RedissonClient;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
* @Description:自定义踢出过滤器
*/
@Log4j2
public class KickedOutAuthorizationFilter extends AccessControlFilter {
private RedissonClient redissonClient;
private RedisSessionDao redisSessionDao;
private DefaultWebSessionManager defaultWebSessionManager;
public KickedOutAuthorizationFilter(RedissonClient redissonClient, RedisSessionDao redisSessionDao, DefaultWebSessionManager defaultWebSessionManager) {
this.redissonClient = redissonClient;
this.redisSessionDao = redisSessionDao;
this.defaultWebSessionManager = defaultWebSessionManager;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//1、只针对登录用户处理,首先判断是否登录
Subject subject = getSubject(request, response);
if (!subject.isAuthenticated()){
return true;
}
//2、使用RedissionClien创建队列
String sessionId = ShiroUserUtil.getShiroSessionId();
String logiName = ShiroUserUtil.getShiroUser().getLoginName();
//2.1当前用户的队列
RDeque<String> deque = redissonClient.getDeque(CacheConstant.GROUP_CAS + "kickedOutAuthorizationFilter:" + logiName);
//3、判断当前sessionId是否存在于此用户的队列=key:登录名 value:多个sessionId
boolean flag = deque.contains(sessionId);
//4、不存在则放入队列尾端==>存入sessionId
if (!flag){
deque.addLast(sessionId);
}
//5、判断当前队列大小是否超过限定此账号的可在线人数
if (deque.size()>1){
//6、超过:
//*从队列头部拿到用户sessionId
//*从sessionManger根据sessionId拿到session
//*从sessionDao中移除session会话
sessionId = deque.getFirst();
deque.removeFirst();
Session session = null;
try {
session = defaultWebSessionManager.getSession(new DefaultSessionKey(sessionId));
}catch (UnknownSessionException ex){
log.info("session已经失效");
}catch (ExpiredSessionException expiredSessionException){
log.info("session已经过期");
}
if (!EmptyUtil.isNullOrEmpty(session)){
// 删掉Session
redisSessionDao.delete(session);
}
}
//7、未超过:放过操作
return true;
}
}
在ShiroConfig配置过滤器
java
/**
* @Description 自定义拦截器定义
*/
private Map<String, Filter> filters() {
Map<String, Filter> map = new HashMap<String, Filter>();
map.put("role-or", new RolesOrAuthorizationFilter());
// 装载过滤器
map.put("kicked-out", new KickedOutAuthorizationFilter(redissonClient(), redisSessionDao(), shiroSessionManager()));
return map;
}
在authentication.properties中配置
properties
/static/**=anon
#登录链接不拦截
/login/**=anon
#访问/resource/**需要有admin的角色
/resource/**=role-or[MangerRole,SuperAdmin]
#其他链接是需要登录的
/**=kicked-out,authc