Skip to content

Commit

Permalink
feat: 同一个用户最大会话数控制
Browse files Browse the repository at this point in the history
  • Loading branch information
lerry903 committed Jun 7, 2019
1 parent 0f1a17e commit cba068e
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 1 deletion.
4 changes: 4 additions & 0 deletions ruoyi-admin/src/main/resources/dev/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ shiro:
dbSyncPeriod: 1
# 相隔多久检查一次session的有效性,默认就是10分钟
validationInterval: 10
# 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
maxSession: -1
# 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
kickoutAfter: false

# 防止XSS攻击
xss:
Expand Down
11 changes: 11 additions & 0 deletions ruoyi-admin/src/main/resources/ehcache/ehcache-shiro.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,16 @@
statistics="true">
</cache>

<!-- 系统活跃用户缓存 -->
<cache name="sys-userCache"
maxEntriesLocalHeap="10000"
overflowToDisk="false"
eternal="false"
diskPersistent="false"
timeToLiveSeconds="0"
timeToIdleSeconds="0"
statistics="true">
</cache>

</ehcache>

4 changes: 4 additions & 0 deletions ruoyi-admin/src/main/resources/run/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ shiro:
dbSyncPeriod: 1
# 相隔多久检查一次session的有效性,默认就是10分钟
validationInterval: 10
# 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
maxSession: -1
# 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
kickoutAfter: false

# 防止XSS攻击
xss:
Expand Down
4 changes: 4 additions & 0 deletions ruoyi-admin/src/main/resources/uat/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ shiro:
dbSyncPeriod: 1
# 相隔多久检查一次session的有效性,默认就是10分钟
validationInterval: 10
# 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
maxSession: -1
# 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
kickoutAfter: false

# 防止XSS攻击
xss:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,14 @@ private ShiroConstants(){
* 验证码错误
*/
public static final String CAPTCHA_ERROR = "captchaError" ;

/**
* 登录记录缓存
*/
public static final String LOGINRECORDCACHE = "loginRecordCache";

/**
* 系统活跃用户缓存
*/
public static final String SYS_USERCACHE = "sys-userCache";
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.ruoyi.framework.shiro.session.OnlineSessionFactory;
import com.ruoyi.framework.shiro.web.filter.LogoutFilter;
import com.ruoyi.framework.shiro.web.filter.captcha.CaptchaValidateFilter;
import com.ruoyi.framework.shiro.web.filter.kickout.KickoutSessionFilter;
import com.ruoyi.framework.shiro.web.filter.online.OnlineSessionFilter;
import com.ruoyi.framework.shiro.web.filter.sync.SyncOnlineSessionFilter;
import com.ruoyi.framework.shiro.web.session.OnlineWebSessionManager;
Expand Down Expand Up @@ -54,6 +55,18 @@ public class ShiroConfig {
@Value("${shiro.session.validationInterval}")
private int validationInterval;

/**
* 同一个用户最大会话数
*/
@Value("${shiro.session.maxSession}")
private int maxSession;

/**
* 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
*/
@Value("${shiro.session.kickoutAfter}")
private boolean kickoutAfter;

/**
* 验证码开关
*/
Expand Down Expand Up @@ -276,12 +289,13 @@ public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityMan
filters.put("onlineSession" , onlineSessionFilter());
filters.put("syncOnlineSession" , syncOnlineSessionFilter());
filters.put("captchaValidate" , captchaValidateFilter());
filters.put("kickout", kickoutSessionFilter());
// 注销成功,则跳转到指定页面
filters.put("logout" , logoutFilter());
shiroFilterFactoryBean.setFilters(filters);

// 所有请求需要认证
filterChainDefinitionMap.put("/**" , "user,onlineSession,syncOnlineSession");
filterChainDefinitionMap.put("/**" , "user,kickout,onlineSession,syncOnlineSession");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

return shiroFilterFactoryBean;
Expand Down Expand Up @@ -356,4 +370,20 @@ public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}

/**
* 同一个用户多设备登录限制
*/
private KickoutSessionFilter kickoutSessionFilter() {
KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
kickoutSessionFilter.setCacheManager(getEhCacheManager());
kickoutSessionFilter.setSessionManager(sessionManager());
// 同一个用户最大的会话数,默认-1无限制;比如2的意思是同一个用户允许最多同时两个人登录
kickoutSessionFilter.setMaxSession(maxSession);
// 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序
kickoutSessionFilter.setKickoutAfter(kickoutAfter);
// 被踢出后重定向到的地址;
kickoutSessionFilter.setKickoutUrl("/login?kickout=1");
return kickoutSessionFilter;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.ruoyi.framework.shiro.web.filter.kickout;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.ruoyi.common.base.AjaxResult;
import com.ruoyi.common.constant.ShiroConstants;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.framework.util.ShiroUtils;
import com.ruoyi.system.domain.SysUser;
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;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.Deque;

/**
* 登录帐号控制过滤器
*
* @author ruoyi
*/
public class KickoutSessionFilter extends AccessControlFilter {

/**
* 同一个用户最大会话数
**/
private int maxSession = -1;

/**
* 踢出之前登录的/之后登录的用户 默认false踢出之前登录的用户
**/
private Boolean kickoutAfter = false;

/**
* 踢出后到的地址
**/
private String kickoutUrl;

private SessionManager sessionManager;
private Cache<String, Deque<Serializable>> cache;

@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
return false;
}

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
boolean flag = !subject.isAuthenticated() && !subject.isRemembered() || maxSession == -1;
if (flag) {
// 如果没有登录或用户最大会话数为-1,直接进行之后的流程
return true;
}
try {
Session session = subject.getSession();
// 当前登录用户
SysUser user = ShiroUtils.getSysUser();
String loginName = user.getLoginName();
Serializable sessionId = session.getId();

// 读取缓存用户 没有就存入
Deque<Serializable> deque = cache.get(loginName);
if (deque == null) {
// 初始化队列
deque = new ArrayDeque<>();
}

// 如果队列里没有此sessionId,且用户没有被踢出;放入队列
if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
// 将sessionId存入队列
deque.push(sessionId);
// 将用户的sessionId队列缓存
cache.put(loginName, deque);
}

// 如果队列里的sessionId数超出最大会话数,开始踢人
while (deque.size() > maxSession) {
Serializable kickoutSessionId = null;
// 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
if (kickoutAfter) {
// 踢出后者
kickoutSessionId = deque.removeFirst();
} else {
// 踢出前者
kickoutSessionId = deque.removeLast();
}
// 踢出后再更新下缓存队列
cache.put(loginName, deque);

try {
// 获取被踢出的sessionId的session对象
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if (null != kickoutSession) {
// 设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {
// 面对异常,我们选择忽略
}
}

// 如果被踢出了,(前者或后者)直接退出,重定向到踢出后的地址
if (ObjectUtil.isNotEmpty(session.getAttribute("kickout")) && Convert.toBool(session.getAttribute("kickout"))) {
// 退出登录
subject.logout();
saveRequest(request);
return isAjaxResponse(request, response);
}
return true;
} catch (Exception e) {
return isAjaxResponse(request, response);
}
}

private boolean isAjaxResponse(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
if (ServletUtils.isAjaxRequest(req)) {
AjaxResult ajaxResult = AjaxResult.error("您已在别处登录,请您修改密码或重新登录");
ServletUtils.renderString(res, JSONUtil.toJsonStr(ajaxResult));
} else {
WebUtils.issueRedirect(request, response, kickoutUrl);
}
return false;
}

public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}

public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}

public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}

public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}

/**
* 设置Cache的key的前缀
*/
public void setCacheManager(CacheManager cacheManager) {
// 必须和ehcache缓存配置中的缓存name一致
this.cache = cacheManager.getCache(ShiroConstants.SYS_USERCACHE);
}
}

0 comments on commit cba068e

Please sign in to comment.