在某些项目中可能会遇到如每个账户同时只能有一个人登录或几个人同时登录,如果同时 有多人登录:要么不让后者登录;要么踢出前者登录(强制退出)。比如 spring security 就 直接提供了相应的功能;Shiro 的话没有提供默认实现,不过可以很容易的在 Shiro 中加入 这个功能。
一.思路
利用shiro拦截器,自定义一个拦截器,这次选择继承AccessControlFilter,因为它提供了getSubject(ServletRequest request, ServletResponse response) //获取 Subject 实例,saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) 等方法,使用起来灵活。
如果是指定url处理可以选择继承PathMatchingFilter。
在redis中,每个账号对应一个linkedlist,里面是用此账号登录的sessionid。拦截器中每个账号请求时被拦截,看redis中linkedlist的大小是否大于1,如果大于1说明有第二个人B用此账号登录了,就把前一个人A的session中放一个变量标记要被踢出,当A再次请求进入拦截器时,发现被标记踢出,就执行退出登录方法,并重定向到登录页面。
也可以采用锁,保证并发效果,防止两个人同时进入拦截器比对linkedlist 通过。
二.实现
我采用了redisson,因为它提供了RMAP 有分段锁
isAccessAllowed方法 表示是否允许访问,返回true表示允许,我返回false,都不允许,就会进入到onAccessDenied方法,不允许访问怎么办。
在redis放入 <用户id,<linkedlist>>这样的Rmap,比对用此账号的登录人数时,得到此userid对应数据的分段锁,防止两人同时登录一个账号,而其他账号不影响
public class KickoutSessionControlFilter extends AccessControlFilter {
private RedissonClient redissonClient;
@Value("${kickoutUrl}")
private String kickoutUrl; //踢出后到的地址
private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
private int maxSession = 1; //同一个帐号最大会话数 默认1
private SessionManager sessionManager;
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
//全不允许访问,直接跳转到 onAccessDenied 访问失败怎么办
return false;
}
/**
* 返回true表示继续进行拦截器链,返回false就结束了,不能访问了
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
if(!subject.isAuthenticated() && !subject.isRemembered()) {
//如果没有登录,直接进行之后的流程
return true;
}
Session session = subject.getSession();
UserInfo user = (UserInfo) subject.getPrincipal();
String userCode = user.getCode();
Serializable sessionId = session.getId();
RMap<String,Deque<Serializable>> kickMap = redissonClient.getMap(Constants.redisKeys.KICKOUT.getValue());
//利用RMap 的分段锁,锁住usercode对应的队列,只有一个线程能对 对应队列操作比对,防止多人同时获取队列的大小为空,从而同时登录
RLock lock = kickMap.getLock(userCode);
lock.lock();
Deque<Serializable> deque = kickMap.get(userCode);
if(deque == null) {
deque = new LinkedList<Serializable>();
kickMap.put(userCode, deque);
}
//如果队列里没有此sessionId,且用户没有被踢出;压入栈
if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
deque.push(sessionId);
}
//如果队列里的sessionId数超出最大会话数,开始踢人
while(deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if(kickoutAfter) { //如果踢出后者
kickoutSessionId = deque.removeFirst();
} else { //否则踢出前者
kickoutSessionId = deque.removeLast();
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
//将队列中的其他的session标记为踢出状态,当下次这个session发起请求时,就会被踢出
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {//ignore exception
}
}
//将踢完后。大小为1的队列保存。此队列大小最多是2,因为有锁,且一旦是2就会踢人
kickMap.put(userCode, deque);
lock.unlock();
//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null) {
//会话被踢出了
try {
subject.logout();
} catch (Exception e) { //ignore
}
//saveRequest(request);
//WebUtils.issueRedirect(request, response, kickoutUrl);
//重定向到登录页面
saveRequestAndRedirectToLogin(request, response);
return false;
}
return true;
}
shiro配置,除了登录都要被我们定义的拦截器拦截。效果是用户A登录后,用户B可以登录成功,用户B访问处登录以外的接口时,会在缓存中对应账号对应linkedlist中加入自己的sessionid,并把用户A的sessio标记踢出,当用户A访问其他接口时,就会被踢出并重定向到登录页面
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//访问的是后端url地址为 /login的接口
shiroFilterFactoryBean.setLoginUrl("/login");
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("kick", kickoutSessionControlFilter());
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/ajaxLogin", "anon");
filterChainDefinitionMap.put("/dl/msglogin", "anon");
filterChainDefinitionMap.put("/dl/weixinlogin", "anon");
filterChainDefinitionMap.put("/dl/studentlogin", "anon");
filterChainDefinitionMap.put("/dl/**", "authc");
filterChainDefinitionMap.put("/dl/**", "kick");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}