diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index 7d0ffa23..f39fb29f 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -115,6 +115,10 @@ shiro:
dbSyncPeriod: 1
# 相隔多久检查一次session的有效性,默认就是10分钟
validationInterval: 10
+ # 同一个用户最大会话数,比如2的意思是同一个账号允许最多同时两个人登录(默认-1不限制)
+ maxSession: -1
+ # 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
+ kickoutAfter: false
# 防止XSS攻击
xss:
diff --git a/ruoyi-admin/src/main/resources/ehcache/ehcache-shiro.xml b/ruoyi-admin/src/main/resources/ehcache/ehcache-shiro.xml
index aa641df6..66c01c30 100644
--- a/ruoyi-admin/src/main/resources/ehcache/ehcache-shiro.xml
+++ b/ruoyi-admin/src/main/resources/ehcache/ehcache-shiro.xml
@@ -22,6 +22,17 @@
overflowToDisk="false"
statistics="true">
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-admin/src/main/resources/static/ruoyi/login.js b/ruoyi-admin/src/main/resources/static/ruoyi/login.js
index 6bdd9860..f633f51d 100644
--- a/ruoyi-admin/src/main/resources/static/ruoyi/login.js
+++ b/ruoyi-admin/src/main/resources/static/ruoyi/login.js
@@ -1,5 +1,6 @@
$(function() {
+ validateKickout();
validateRule();
$('.imgcode').click(function() {
var url = ctx + "captcha/captchaImage?type=" + captchaType + "&s=" + Math.random();
@@ -62,3 +63,33 @@ function validateRule() {
}
})
}
+
+function validateKickout() {
+ if (getParam("kickout") == 1) {
+ layer.alert("您已在别处登录,请您修改密码或重新登录", {
+ icon: 0,
+ title: "系统提示"
+ },
+ function(index) {
+ //关闭弹窗
+ layer.close(index);
+ if (top != self) {
+ top.location = self.location;
+ } else {
+ var url = location.search;
+ if (url) {
+ var oldUrl = window.location.href;
+ var newUrl = oldUrl.substring(0, oldUrl.indexOf('?'));
+ self.location = newUrl;
+ }
+ }
+ });
+ }
+}
+
+function getParam(paramName) {
+ var reg = new RegExp("(^|&)" + paramName + "=([^&]*)(&|$)");
+ var r = window.location.search.substr(1).match(reg);
+ if (r != null) return decodeURI(r[2]);
+ return null;
+}
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java
index 7d26b5ff..a3dd77d3 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ShiroConfig.java
@@ -28,6 +28,7 @@ import com.ruoyi.framework.shiro.session.OnlineSessionDAO;
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;
@@ -48,6 +49,18 @@ public class ShiroConfig
@Value("${shiro.session.expireTime}")
private int expireTime;
+ // 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟
+ @Value("${shiro.session.validationInterval}")
+ private int validationInterval;
+
+ // 同一个用户最大会话数
+ @Value("${shiro.session.maxSession}")
+ private int maxSession;
+
+ // 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
+ @Value("${shiro.session.kickoutAfter}")
+ private boolean kickoutAfter;
+
// 验证码开关
@Value("${shiro.user.captchaEnabled}")
private boolean captchaEnabled;
@@ -244,16 +257,17 @@ public class ShiroConfig
// 系统权限列表
// filterChainDefinitionMap.putAll(SpringUtils.getBean(IMenuService.class).selectPermsAll());
- Map filters = new LinkedHashMap<>();
+ Map filters = new LinkedHashMap();
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;
@@ -316,6 +330,23 @@ public class ShiroConfig
return cookieRememberMeManager;
}
+ /**
+ * 同一个用户多设备登录限制
+ */
+ public 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;
+ }
+
/**
* thymeleaf模板引擎和shiro框架的整合
*/
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/kickout/KickoutSessionFilter.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/kickout/KickoutSessionFilter.java
new file mode 100644
index 00000000..87a69f9c
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/kickout/KickoutSessionFilter.java
@@ -0,0 +1,178 @@
+package com.ruoyi.framework.shiro.web.filter.kickout;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+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;
+import org.apache.shiro.web.util.WebUtils;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.framework.util.ShiroUtils;
+import com.ruoyi.system.domain.SysUser;
+
+/**
+ * 登录帐号控制过滤器
+ *
+ * @author ruoyi
+ */
+public class KickoutSessionFilter extends AccessControlFilter
+{
+ private final static ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * 同一个用户最大会话数
+ **/
+ private int maxSession = -1;
+
+ /**
+ * 踢出之前登录的/之后登录的用户 默认false踢出之前登录的用户
+ **/
+ private Boolean kickoutAfter = false;
+
+ /**
+ * 踢出后到的地址
+ **/
+ private String kickoutUrl;
+
+ private SessionManager sessionManager;
+ private Cache> cache;
+
+ @Override
+ protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o)
+ throws Exception
+ {
+ return false;
+ }
+
+ @Override
+ protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception
+ {
+ Subject subject = getSubject(request, response);
+ if (!subject.isAuthenticated() && !subject.isRemembered() || maxSession == -1)
+ {
+ // 如果没有登录或用户最大会话数为-1,直接进行之后的流程
+ return true;
+ }
+ try
+ {
+ Session session = subject.getSession();
+ // 当前登录用户
+ SysUser user = ShiroUtils.getSysUser();
+ String loginName = user.getLoginName();
+ Serializable sessionId = session.getId();
+
+ // 读取缓存用户 没有就存入
+ Deque 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);
+
+ // 获取被踢出的sessionId的session对象
+ Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
+ if (kickoutSession != null)
+ {
+ // 设置会话的kickout属性表示踢出了
+ kickoutSession.setAttribute("kickout", true);
+ }
+ }
+
+ // 如果被踢出了,(前者或后者)直接退出,重定向到踢出后的地址
+ if ((Boolean) session.getAttribute("kickout") != null && (Boolean) session.getAttribute("kickout") == true)
+ {
+ // 退出登录
+ 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, objectMapper.writeValueAsString(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("sys-userCache");
+ }
+}