From 1054bad2362555755904a03563da8b0e97f2148d Mon Sep 17 00:00:00 2001 From: RuoYi Date: Tue, 4 Jun 2019 16:34:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=8C=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=9C=80=E5=A4=A7=E4=BC=9A=E8=AF=9D=E6=95=B0?= =?UTF-8?q?=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 4 + .../main/resources/ehcache/ehcache-shiro.xml | 11 ++ .../src/main/resources/static/ruoyi/login.js | 31 +++ .../ruoyi/framework/config/ShiroConfig.java | 35 +++- .../filter/kickout/KickoutSessionFilter.java | 178 ++++++++++++++++++ 5 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 ruoyi-framework/src/main/java/com/ruoyi/framework/shiro/web/filter/kickout/KickoutSessionFilter.java 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"); + } +}