Skip to content

Commit cba068e

Browse files
committed
feat: 同一个用户最大会话数控制
1 parent 0f1a17e commit cba068e

7 files changed

Lines changed: 227 additions & 1 deletion

File tree

ruoyi-admin/src/main/resources/dev/application.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ shiro:
158158
dbSyncPeriod: 1
159159
# 相隔多久检查一次session的有效性,默认就是10分钟
160160
validationInterval: 10
161+
# 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
162+
maxSession: -1
163+
# 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
164+
kickoutAfter: false
161165

162166
# 防止XSS攻击
163167
xss:

ruoyi-admin/src/main/resources/ehcache/ehcache-shiro.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,16 @@
2323
statistics="true">
2424
</cache>
2525

26+
<!-- 系统活跃用户缓存 -->
27+
<cache name="sys-userCache"
28+
maxEntriesLocalHeap="10000"
29+
overflowToDisk="false"
30+
eternal="false"
31+
diskPersistent="false"
32+
timeToLiveSeconds="0"
33+
timeToIdleSeconds="0"
34+
statistics="true">
35+
</cache>
36+
2637
</ehcache>
2738

ruoyi-admin/src/main/resources/run/application.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ shiro:
158158
dbSyncPeriod: 1
159159
# 相隔多久检查一次session的有效性,默认就是10分钟
160160
validationInterval: 10
161+
# 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
162+
maxSession: -1
163+
# 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
164+
kickoutAfter: false
161165

162166
# 防止XSS攻击
163167
xss:

ruoyi-admin/src/main/resources/uat/application.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ shiro:
158158
dbSyncPeriod: 1
159159
# 相隔多久检查一次session的有效性,默认就是10分钟
160160
validationInterval: 10
161+
# 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
162+
maxSession: -1
163+
# 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
164+
kickoutAfter: false
161165

162166
# 防止XSS攻击
163167
xss:

ruoyi-common/src/main/java/com/ruoyi/common/constant/ShiroConstants.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,14 @@ private ShiroConstants(){
4545
* 验证码错误
4646
*/
4747
public static final String CAPTCHA_ERROR = "captchaError" ;
48+
49+
/**
50+
* 登录记录缓存
51+
*/
52+
public static final String LOGINRECORDCACHE = "loginRecordCache";
53+
54+
/**
55+
* 系统活跃用户缓存
56+
*/
57+
public static final String SYS_USERCACHE = "sys-userCache";
4858
}

ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.ruoyi.framework.shiro.session.OnlineSessionFactory;
88
import com.ruoyi.framework.shiro.web.filter.LogoutFilter;
99
import com.ruoyi.framework.shiro.web.filter.captcha.CaptchaValidateFilter;
10+
import com.ruoyi.framework.shiro.web.filter.kickout.KickoutSessionFilter;
1011
import com.ruoyi.framework.shiro.web.filter.online.OnlineSessionFilter;
1112
import com.ruoyi.framework.shiro.web.filter.sync.SyncOnlineSessionFilter;
1213
import com.ruoyi.framework.shiro.web.session.OnlineWebSessionManager;
@@ -54,6 +55,18 @@ public class ShiroConfig {
5455
@Value("${shiro.session.validationInterval}")
5556
private int validationInterval;
5657

58+
/**
59+
* 同一个用户最大会话数
60+
*/
61+
@Value("${shiro.session.maxSession}")
62+
private int maxSession;
63+
64+
/**
65+
* 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
66+
*/
67+
@Value("${shiro.session.kickoutAfter}")
68+
private boolean kickoutAfter;
69+
5770
/**
5871
* 验证码开关
5972
*/
@@ -276,12 +289,13 @@ public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityMan
276289
filters.put("onlineSession" , onlineSessionFilter());
277290
filters.put("syncOnlineSession" , syncOnlineSessionFilter());
278291
filters.put("captchaValidate" , captchaValidateFilter());
292+
filters.put("kickout", kickoutSessionFilter());
279293
// 注销成功,则跳转到指定页面
280294
filters.put("logout" , logoutFilter());
281295
shiroFilterFactoryBean.setFilters(filters);
282296

283297
// 所有请求需要认证
284-
filterChainDefinitionMap.put("/**" , "user,onlineSession,syncOnlineSession");
298+
filterChainDefinitionMap.put("/**" , "user,kickout,onlineSession,syncOnlineSession");
285299
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
286300

287301
return shiroFilterFactoryBean;
@@ -356,4 +370,20 @@ public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
356370
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
357371
return authorizationAttributeSourceAdvisor;
358372
}
373+
374+
/**
375+
* 同一个用户多设备登录限制
376+
*/
377+
private KickoutSessionFilter kickoutSessionFilter() {
378+
KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
379+
kickoutSessionFilter.setCacheManager(getEhCacheManager());
380+
kickoutSessionFilter.setSessionManager(sessionManager());
381+
// 同一个用户最大的会话数,默认-1无限制;比如2的意思是同一个用户允许最多同时两个人登录
382+
kickoutSessionFilter.setMaxSession(maxSession);
383+
// 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序
384+
kickoutSessionFilter.setKickoutAfter(kickoutAfter);
385+
// 被踢出后重定向到的地址;
386+
kickoutSessionFilter.setKickoutUrl("/login?kickout=1");
387+
return kickoutSessionFilter;
388+
}
359389
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package com.ruoyi.framework.shiro.web.filter.kickout;
2+
3+
import cn.hutool.core.convert.Convert;
4+
import cn.hutool.core.util.ObjectUtil;
5+
import cn.hutool.json.JSONUtil;
6+
import com.ruoyi.common.base.AjaxResult;
7+
import com.ruoyi.common.constant.ShiroConstants;
8+
import com.ruoyi.common.utils.ServletUtils;
9+
import com.ruoyi.framework.util.ShiroUtils;
10+
import com.ruoyi.system.domain.SysUser;
11+
import org.apache.shiro.cache.Cache;
12+
import org.apache.shiro.cache.CacheManager;
13+
import org.apache.shiro.session.Session;
14+
import org.apache.shiro.session.mgt.DefaultSessionKey;
15+
import org.apache.shiro.session.mgt.SessionManager;
16+
import org.apache.shiro.subject.Subject;
17+
import org.apache.shiro.web.filter.AccessControlFilter;
18+
import org.apache.shiro.web.util.WebUtils;
19+
20+
import javax.servlet.ServletRequest;
21+
import javax.servlet.ServletResponse;
22+
import javax.servlet.http.HttpServletRequest;
23+
import javax.servlet.http.HttpServletResponse;
24+
import java.io.IOException;
25+
import java.io.Serializable;
26+
import java.util.ArrayDeque;
27+
import java.util.Deque;
28+
29+
/**
30+
* 登录帐号控制过滤器
31+
*
32+
* @author ruoyi
33+
*/
34+
public class KickoutSessionFilter extends AccessControlFilter {
35+
36+
/**
37+
* 同一个用户最大会话数
38+
**/
39+
private int maxSession = -1;
40+
41+
/**
42+
* 踢出之前登录的/之后登录的用户 默认false踢出之前登录的用户
43+
**/
44+
private Boolean kickoutAfter = false;
45+
46+
/**
47+
* 踢出后到的地址
48+
**/
49+
private String kickoutUrl;
50+
51+
private SessionManager sessionManager;
52+
private Cache<String, Deque<Serializable>> cache;
53+
54+
@Override
55+
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
56+
return false;
57+
}
58+
59+
@Override
60+
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
61+
Subject subject = getSubject(request, response);
62+
boolean flag = !subject.isAuthenticated() && !subject.isRemembered() || maxSession == -1;
63+
if (flag) {
64+
// 如果没有登录或用户最大会话数为-1,直接进行之后的流程
65+
return true;
66+
}
67+
try {
68+
Session session = subject.getSession();
69+
// 当前登录用户
70+
SysUser user = ShiroUtils.getSysUser();
71+
String loginName = user.getLoginName();
72+
Serializable sessionId = session.getId();
73+
74+
// 读取缓存用户 没有就存入
75+
Deque<Serializable> deque = cache.get(loginName);
76+
if (deque == null) {
77+
// 初始化队列
78+
deque = new ArrayDeque<>();
79+
}
80+
81+
// 如果队列里没有此sessionId,且用户没有被踢出;放入队列
82+
if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
83+
// 将sessionId存入队列
84+
deque.push(sessionId);
85+
// 将用户的sessionId队列缓存
86+
cache.put(loginName, deque);
87+
}
88+
89+
// 如果队列里的sessionId数超出最大会话数,开始踢人
90+
while (deque.size() > maxSession) {
91+
Serializable kickoutSessionId = null;
92+
// 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;
93+
if (kickoutAfter) {
94+
// 踢出后者
95+
kickoutSessionId = deque.removeFirst();
96+
} else {
97+
// 踢出前者
98+
kickoutSessionId = deque.removeLast();
99+
}
100+
// 踢出后再更新下缓存队列
101+
cache.put(loginName, deque);
102+
103+
try {
104+
// 获取被踢出的sessionId的session对象
105+
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
106+
if (null != kickoutSession) {
107+
// 设置会话的kickout属性表示踢出了
108+
kickoutSession.setAttribute("kickout", true);
109+
}
110+
} catch (Exception e) {
111+
// 面对异常,我们选择忽略
112+
}
113+
}
114+
115+
// 如果被踢出了,(前者或后者)直接退出,重定向到踢出后的地址
116+
if (ObjectUtil.isNotEmpty(session.getAttribute("kickout")) && Convert.toBool(session.getAttribute("kickout"))) {
117+
// 退出登录
118+
subject.logout();
119+
saveRequest(request);
120+
return isAjaxResponse(request, response);
121+
}
122+
return true;
123+
} catch (Exception e) {
124+
return isAjaxResponse(request, response);
125+
}
126+
}
127+
128+
private boolean isAjaxResponse(ServletRequest request, ServletResponse response) throws IOException {
129+
HttpServletRequest req = (HttpServletRequest) request;
130+
HttpServletResponse res = (HttpServletResponse) response;
131+
if (ServletUtils.isAjaxRequest(req)) {
132+
AjaxResult ajaxResult = AjaxResult.error("您已在别处登录,请您修改密码或重新登录");
133+
ServletUtils.renderString(res, JSONUtil.toJsonStr(ajaxResult));
134+
} else {
135+
WebUtils.issueRedirect(request, response, kickoutUrl);
136+
}
137+
return false;
138+
}
139+
140+
public void setMaxSession(int maxSession) {
141+
this.maxSession = maxSession;
142+
}
143+
144+
public void setKickoutAfter(boolean kickoutAfter) {
145+
this.kickoutAfter = kickoutAfter;
146+
}
147+
148+
public void setKickoutUrl(String kickoutUrl) {
149+
this.kickoutUrl = kickoutUrl;
150+
}
151+
152+
public void setSessionManager(SessionManager sessionManager) {
153+
this.sessionManager = sessionManager;
154+
}
155+
156+
/**
157+
* 设置Cache的key的前缀
158+
*/
159+
public void setCacheManager(CacheManager cacheManager) {
160+
// 必须和ehcache缓存配置中的缓存name一致
161+
this.cache = cacheManager.getCache(ShiroConstants.SYS_USERCACHE);
162+
}
163+
}

0 commit comments

Comments
 (0)