Przeglądaj źródła

新增同一个用户最大会话数控制

RuoYi 5 lat temu
rodzic
commit
b46735fd78

+ 4 - 0
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: 

+ 11 - 0
ruoyi-admin/src/main/resources/ehcache/ehcache-shiro.xml

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

+ 31 - 0
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("<font color='red'>您已在别处登录,请您修改密码或重新登录</font>", {
+	        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;
+}

+ 33 - 2
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<String, Filter> filters = new LinkedHashMap<>();
+        Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
         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框架的整合
      */

+ 178 - 0
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<String, Deque<Serializable>> 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<Serializable> deque = cache.get(loginName);
+            if (deque == null)
+            {
+                // 初始化队列
+                deque = new ArrayDeque<Serializable>();
+            }
+
+            // 如果队列里没有此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");
+    }
+}