shiro实现用户踢出功能
KickoutSessionControlFilte
import java.io.IOException; import java.io.PrintWriter; 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 com.fasterxml.jackson.databind.ObjectMapper; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * * @类名称:KickoutSessionFilter * @类描述:自定义过滤器,进行用户访问控制 * */ public class KickoutSessionControlFilter extends AccessControlFilter { private static final Logger logger = LoggerFactory .getLogger(KickoutSessionControlFilter.class); private final static ObjectMapper objectMapper = new ObjectMapper(); private String kickoutUrl; // 踢出后到的地址 private boolean kickoutAfter = true; // 踢出之前登录的/之后登录的用户 默认false踢出之前登录的用户 private int maxSession = 1; // 同一个帐号最大会话数 默认1 private SessionManager sessionManager; private Cache<String, Deque<Serializable>> cache; public void setKickoutUrl(String kickoutUrl) { this.kickoutUrl = kickoutUrl; } public void setKickoutAfter(boolean kickoutAfter) { this.kickoutAfter = kickoutAfter; } public void setMaxSession(int maxSession) { this.maxSession = maxSession; } public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } // 设置Cache的key的前缀 public void setCacheManager(CacheManager cacheManager) { //必须和ehcache缓存配置中的缓存name一致 this.cache = cacheManager.getCache("shiro-activeSessionCache"); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { Subject subject = getSubject(request, response); Session session = subject.getSession(); // 没有登录授权 且没有记住我 if (!subject.isAuthenticated() && !subject.isRemembered()) { // 如果没有登录,直接进行之后的流程 //判断是不是Ajax请求,异步请求,直接响应返回未登录 if (ShiroFilterUtils.isAjax(request) ) { out(response,"超时"); return false; }else{ request.setAttribute("forceLogout", "1"); session.setAttribute("forceLogout", "forceLogout"); return true; } } // 获得用户请求的URI HttpServletRequest req=(HttpServletRequest) request; String path = req.getRequestURI(); //放行登录 if(path.equals("/login")){ return true; } try { // 当前用户 String username = (String) subject.getPrincipal(); Serializable sessionId = session.getId(); // 读取缓存用户 没有就存入 Deque<Serializable> deque = cache.get(username); if (deque == null) { // 初始化队列 deque = new ArrayDeque<Serializable>(); } // 如果队列里没有此sessionId,且用户没有被踢出;放入队列 if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) { // 将sessionId存入队列 deque.push(sessionId); // 将用户的sessionId队列缓存 cache.put(username, deque); } // 如果队列里的sessionId数超出最大会话数,开始踢人 while (deque.size() > maxSession) { Serializable kickoutSessionId = null; // 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户; if (kickoutAfter) { // 如果踢出后者 kickoutSessionId = deque.removeFirst(); } else { // 否则踢出前者 kickoutSessionId = deque.removeLast(); } // 踢出后再更新下缓存队列 cache.put(username, deque); try { // 获取被踢出的sessionId的session对象 Session kickoutSession = sessionManager .getSession(new DefaultSessionKey(kickoutSessionId)); if (kickoutSession != null) { // 设置会话的kickout属性表示踢出了 kickoutSession.setAttribute("kickout", true); } } catch (Exception e) {// ignore exception } } // 如果被踢出了,(前者或后者)直接退出,重定向到踢出后的地址 if (session.getAttribute("kickout") != null) { // 会话被踢出了 subject.logout(); try { // 退出登录("退出登录" + kickoutUrl); } catch (Exception e) { // ignore logger.debug("==踢出后用户重定向的路径kickoutUrl:" + kickoutUrl); } saveRequest(request); // 重定向 return isAjaxResponse(request,response); } return true; } catch (Exception e) { // ignore logger.debug("控制用户在线数量【lyd-admin-->KickoutSessionFilter.onAccessDenied】异常!"); } return isAjaxResponse(request,response); } public boolean out(ServletResponse response,String status ){ PrintWriter out = null; try { response.setCharacterEncoding("UTF-8");//设置编码 //response.setContentType("application/json");//设置返回类型 out = response.getWriter(); out.println("<html>"); out.println("<script>"); if("重复".equals(status)) { out.println("alert('当前用户已经在其他地方登录,请您修改密码或重新登录!');"); }else if("超时".equals(status)) { out.println("alert('当前用户操作超时或被踢出登录,请您修改密码或重新登录!');"); } //out.println("history.back();"); out.println("</script>"); out.println("</html>"); return true; } catch (Exception e) { logger.debug("用户在线数量限制【wyait-manager-->KickoutSessionFilter.out1】响应json信息出错"); }finally{ if(null != out){ out.flush(); out.close(); } } return true; } private boolean isAjaxResponse(ServletRequest request, ServletResponse response) throws IOException { // ajax请求 /** * 判断是否已经踢出 * 1.如果是Ajax 访问,那么给予json返回值提示。 * 2.如果是普通请求,直接跳转到登录页 */ //判断是不是Ajax请求 if (ShiroFilterUtils.isAjax(request) ) { out(response,"重复"); }else{ // 重定向 WebUtils.issueRedirect(request, response, kickoutUrl); } return false; } }
jsp页面
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <!--jQuery js--> <script src="<%=request.getContextPath()%>/resources/scripts2/jquery.min.js" type="text/javascript"></script> <!--MiniUI--> <link href="<%=request.getContextPath()%>/resources/scripts2/miniui/themes/default/miniui.css" rel="stylesheet" type="text/css" /> <!-- <link href="<%=request.getContextPath()%>/resources/scripts/miniui/themes/blue/skin.css" rel="stylesheet" type="text/css" /> --> <link href="<%=request.getContextPath()%>/resources/scripts2/miniui/themes/icons.css" rel="stylesheet" type="text/css" /> <script src="<%=request.getContextPath()%>/resources/scripts2/miniui/miniui.js" type="text/javascript"></script> <!-- <script src="../../scripts/boot.js" type="text/javascript"></script> --> <style type="text/css"> html, body{ margin:0;padding:0;border:0;width:100%;height:100%;overflow:hidden; } </style> </head> <body> <div style="padding:10px; background: #EDF0F3"> <form id="queryForm"> <table style="table-layout:fixed;"> <tr> <td style="width:100px;">当前在线人数:</td> <td style="width:200px;"> <input id="sesessionCount" name="sesessionCount" property="editor" class="mini-textbox" allowInput="true" autoPopup="true" enabled="flase" style="width:80%" valueFromSelect="true" /> </td> </tr> </table> </form> </div> <!-- Start Tool bar --> <div id="toolbar" style="width:100%; "> <div class="mini-toolbar" style="border-bottom:0;padding:0px;"> <table style="width:100%;"> <tr> <td style="width:100%;"> <a class="mini-button" iconCls="icon-reload" onclick="refreshForm()" plain="true">刷新</a> <!-- <a class="mini-button" iconCls="icon-print" onclick="output()" plain="true" tooltip="输出Excel">输出Excel</a> --> </td> </tr> </table> </div> </div> <!-- End Tool bar --> <div class="mini-fit"> <div id="datagrid" class="mini-datagrid" ondrawcell="drawCellStatus" style="width:100%;height:100%;" allowResize="true" fitColumns="false" showPager="false" showFilterRow="false" multiSelect="true" virtualScroll="true" > <div property="columns"> <div type="indexcolumn" width="30"></div> <!-- <div type="checkcolumn" width="15"></div>--> <div name="id" id="id" field="id" align="left" headerAlign="center" width="100">会话id <input id="id" name="id" property="editor" class="mini-spinner" format="n3" vtype="float" style="width:100%;" required="true" enabled="true" /> </div> <div name="operatorId" field="operatorId" align="left" headerAlign="center" width="100">用户id <input id="id" name="id" property="editor" class="mini-spinner" format="n3" vtype="float" style="width:100%;" required="true" enabled="true" /> </div> <div name="username" field="username" align="left" headerAlign="center" width="150">用户名</div> <div name="lastAccessTime" field="lastAccessTime" width="180" headerAlign="center" dateFormat="yyyy-MM-dd HH:mm:ss">最后访问时间</div> <div name="host" field="host" headerAlign="center" width="100" >主机地址</div> <div name="tichu" field="tichu" headerAlign="center" width="100" >是否踢出</div> </div> </div> </div> </body> <script src="<%=request.getContextPath()%>/resources/js/common.js" type="text/javascript"></script> <script type="text/javascript"> mini.parse(); var grid = mini.get("datagrid"); loadData(); function loadData() { var form = new mini.Form("queryForm"); var o = form.getData(true); var json = mini.encode(o); //grid.load({json: json}); $.ajax({ url: 'ajax/getSessions', type: 'get', success: function(text) { console.log(text); var o = mini.decode(text); var sesessionCount = o.sesessionCount; mini.get("sesessionCount").setValue(sesessionCount); grid.setData(o.sessions); var data = mini.get('datagrid').getData(); for (i = 0; i < sesessionCount; i++) { var operatorId = o.sessions[i].attributes.USER_ID; var sessionId = o.sessions[i].id; var username = o.sessions[i].attributes.USER_NAME; var row = mini.get('datagrid').getRow(i); var tichu = "<a class='mini-button' iconCls='icon-search' onclick='tichu(\""+sessionId+"\")' tooltip='踢出...'>踢出</a>" mini.get('datagrid').updateRow(row, {'tichu': tichu}); mini.get('datagrid').updateRow(row, {'operatorId': operatorId}); mini.get('datagrid').updateRow(row, {'username': username}); } }, error: function(err) { mini.alert("读取数据失败!") } }); } function refreshForm() { clearForm(); loadData(); } function query() { loadData(); } function tichu(sessionId) { $.ajax({ url: 'ajax/forceLogout', type: 'post', data: {sessionId: sessionId}, cache: false, success: function(text) { removeRow(); mini.alert("成功踢出用户xx!"); }, error: function(err) { mini.alert("读取数据失败") } }); } function clearForm() { var form = new mini.Form("queryForm"); form.clear(); } function onQuyuNodeSelect(e) { //mini.alert(e.isLeaf); if(e.isLeaf == false) { e.cancel=true; } } function removeRow() { var datagrid = mini.get("datagrid"); var rows = datagrid.getSelecteds(); if (rows.length > 0) { datagrid.removeRows(rows, true); } } function drawCellStatus(e) { var column = e.column; var field = e.field; var value = 1; var grid = mini.get("datagrid"); if (field === 'operatorId') { if (value == 1) { console.log("行数为"+e.row); grid.removeRows(e.row, true); } } } </script> </html>
applicationContext-security.xml配置页面
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="cacheManager" class="com.infotech.security.filter.SpringCacheManagerWrapper"> <property name="cacheManager" ref="springCacheManager"/> </bean> <bean id="credentialsMatcher" class="com.infotech.security.filter.RetryLimitHashedCredentialsMatcher"> <constructor-arg ref="cacheManager"/> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="2"/> <property name="storedCredentialsHexEncoded" value="true"/> </bean> <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg name="name" value="JSESSID"/> <property name="path" value="/"/> </bean> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="2592000"/><!-- 30天 --> </bean> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/> <property name="sessionIdGenerator" ref="sessionIdGenerator"/> </bean> <bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler"> <property name="sessionValidationInterval" value="1800000"/> <property name="sessionManager" ref="sessionManager"/> </bean> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="globalSessionTimeout" value="1800000"/> <property name="deleteInvalidSessions" value="true"/> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/> <property name="sessionDAO" ref="sessionDAO"/> <property name="sessionIdCookieEnabled" value="true"/> <property name="sessionIdCookie" ref="sessionIdCookie"/> <property name="sessionIdUrlRewritingEnabled" value="false" /> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list><ref bean="permissionAuthenticationRealm"></ref></list> </property> <property name="sessionManager" ref="sessionManager"/> </bean> <bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"> <property name="usernameParam" value="username"/> <property name="passwordParam" value="password"/> <property name="rememberMeParam" value="rememberMe"/> <property name="loginUrl" value="/index"/> </bean> <bean id="kickoutSessionControlFilter" class="com.infotech.security.filter.KickoutSessionControlFilter"> <property name="cacheManager" ref="cacheManager"/> <property name="sessionManager" ref="sessionManager"/> <property name="kickoutAfter" value="false"/> <property name="maxSession" value="1"/> <property name="kickoutUrl" value="/index?kickout=1"/> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/index" /> <property name="successUrl" value="/main" /> <property name="unauthorizedUrl" value="/404" /> <property name="filters"> <util:map> <entry key="authc" value-ref="formAuthenticationFilter"/> <entry key="sysUser" value-ref="sysUserFilter"/> <entry key="kickout" value-ref="kickoutSessionControlFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /resources/** = anon<!--设置静态资源文件为游客可访问--> /mp/** = anon <!-- 微信接口设为可访问 --> /wechat/** = anon <!-- 微信验证接口 --> /css/** = anon /data/** = anon /images2/** = anon /images/** = anon /img/** = anon /scripts2/** = anon /js/** = anon /login/** = anon /login = authc /logout = logout /authenticated = authc /** = kickout,user,sysUser </value> </property> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> </beans>
AjaxSysController 控制类
@Autowired private SessionDAO sessionDAO; /** * ############################# SYS04 在线用户统计 * ###################################### */ /** * 根据条件分页查询日志信息 * * @param goods * @param page * @param rows * @return * @throws Exception */ @RequestMapping("/ajax/getSessions") public @ResponseBody Map<String, Object> getSessions() { Map<String, Object> result = new HashMap<String, Object>(); try { Collection<Session> sessions = sessionDAO.getActiveSessions(); result.put("sessions", sessions); result.put("sesessionCount", sessions.size()); } catch (Exception e) { result.put(MsgConstants.ErrCode, MsgConstants.Err1); result.put(MsgConstants.ErrMsg, MsgConstants.ErrText99); LOGGER.error(e.getMessage(), e); } return result; } @RequestMapping("/ajax/forceLogout") public @ResponseBody String forceLogout(@RequestParam("sessionId") String sessionId) { try { Subject subject = SecurityUtils.getSubject(); Collection<Session> sessions = sessionDAO.getActiveSessions(); for(Session session:sessions){ if(session.getId().equals(sessionId)) { session.setTimeout(0);//设置session立即失效,即将其踢出系统 } } } catch (Exception e) {/*ignore*/} return "error2"; }
分类:
Shiro
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构