您的位置:首页 > 其它

shiro并发登录人数控制

2017-10-10 09:43 537 查看
在某些项目中可能会遇到如每个账户同时只能有一个人登录或几个人同时登录,如果同时有多人登录:要么不让后者登录;要么踢出前者登录(强制退出)。比如spring security就直接提供了相应的功能;Shiro的话没有提供默认实现,不过可以很容易的在Shiro中加入这个功能。

首先来看看如何配置使用(spring-shiro.xml)

<!-- session 校验单个用户是否多次登录 -->
<bean id="kickoutSessionFilter"  class="cn.easted.edm.core.security.KickoutSessionFilter">
<property name="sessionManager" ref="sessionManager"/>
<property name="cacheManager" ref="shiroEhcacheManager"/>
<property name="kickoutAfter" value="false"/>
<property name="maxSession" value="1"/>
<property name="kickoutUrl" value="/error/500"/>
</bean>


cacheManager:使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;

sessionManager:用于根据会话ID,获取会话进行踢出操作的;

kickoutAfter:是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;

maxSession:同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;

kickoutUrl:被踢出后重定向到的地址;

shiroFilter配置 :

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/login"/>
<property name="successUrl" value="/dashboard/list"/>
<property name="unauthorizedUrl" value="/401"/>
<property name="filters">
<map>
<entry key="authc">
<bean
class="cn.easted.edm.core.security.RoleAuthorizationFilter" />
</entry>
<entry key="roleOrFilter" value-ref="roleOrFilter"/>
<entry key="kickout" value-ref="kickoutSessionFilter"></entry>
</map>

</property>

<property name="filterChainDefinitions">
<value>
<!--角色认证-->
/desktop/edit=authc,kickout,roleOrFilter["operator,admin"]
</value>
</property>
</bean>


KickoutSessionControlFilter核心代码:

package cn.easted.edm.core.security;

import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;

/**
* 踢出用户过滤器
* @ClassName:KickoutSessionFilter
* @author:Wanghao
* @date: 2017年9月29日 下午3:24:15
*/
public class KickoutSessionFilter extends AccessControlFilter{

private String kickoutUrl; //踢出后到的地址
private boolean kickoutAfter; //踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
private int maxSession; //同一个帐号最大会话数 默认1
private SessionManager sessionManager;
private Cache<String, Deque<Serializable>> cache;

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
return 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();
String username = (String) subject.getPrincipal();
Serializable sessionId = session.getId();
// 初始化用户的队列放到缓存里
Deque<Serializable> deque = cache.get(username);
if (deque == null) {
deque = new LinkedList<Serializable>();
cache.put(username, 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.getFirst();
kickoutSessionId = deque.removeFirst();
} else { //否则踢出前者
kickoutSessionId = deque.removeLast();
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {
e.printStackTrace();
}
}
//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null) {
//会话被踢出了
try {
subject.logout();
} catch (Exception e) {
}
//          throw new CommonException("101", "您已被迫下线,若不是您本人操作,请立即修改密码!");
HttpServletResponse res = (HttpServletResponse) response;
res.setStatus(101);
res.sendRedirect(kickoutUrl);
//          RoleAuthorizationFilter.sendFastJson(response, "您已被迫下线,若不是您本人操作,请立即修改密码!");
//          WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
}

public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache("shiro-activeSessionCache");
}
/**
* @param kickoutUrl the kickoutUrl to set
*/
public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}

/**
* @param kickoutAfter the kickoutAfter to set
*/
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}

/**
* @param maxSession the maxSession to set
*/
public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}

/**
* @param sessionManager the sessionManager to set
*/
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}

/**
* @Title: getKickoutUrl <BR>
* @return:String <BR>
*/
public String getKickoutUrl() {
return kickoutUrl;
}

/**
* @Title: isKickoutAfter <BR>
* @return:boolean <BR>
*/
public boolean isKickoutAfter() {
return kickoutAfter;
}

/**
* @Title: getMaxSession <BR>
* @return:int <BR>
*/
public int getMaxSession() {
return maxSession;
}

/**
* @Title: getSessionManager <BR>
* @return:SessionManager <BR>
*/
public SessionManager getSessionManager() {
return sessionManager;
}

/**
* @Title: getCache <BR>
* @return:Cache<String,Deque<Serializable>> <BR>
*/
public Cache<String, Deque<Serializable>> getCache() {
return cache;
}

/**
* @param cache the cache to set
*/
public void setCache(Cache<String, Deque<Serializable>> cache) {
this.cache = cache;
}
}


此处使用了Cache缓存用户名—会话id之间的关系;如果量比较大可以考虑如持久化到数据库/其他带持久化的Cache中;另外此处没有并发控制的同步实现,可以考虑根据用户名获取锁来控制,减少锁的粒度。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  shiro