前篇:spring boot + mybatis + layui + shiro後臺權限管理系統:https://blog.51cto.com/wyait/2082803 javascript
本文是基於spring boot + mybatis + layui + shiro後臺權限管理系統開發的,新增功能:html
後篇: 前端
版本升級及內容優化版本,改動內容:java
項目源碼:(包含數據庫源碼)
github源碼: https://github.com/wyait/manage.git
碼雲:https://gitee.com/wyait/manage.git
github對應項目源碼目錄:wyait-manage-1.2.0
碼雲對應項目源碼目錄:wyait-manage-1.2.0 git
同一個用戶,先在A×××登陸;以後在B×××登陸時,退出A×××的登陸狀態;反之相同。或者限制同一個用戶在不一樣的設備上,同時在線的數量;github
基於shiro和ehcache實現web
spring security就直接提供了相應的功能;
Shiro的話沒有提供默認實現,不過能夠在Shiro中加入這個功能。就是使用shiro強大的自定義訪問控制攔截器:AccessControlFilter,集成這個接口後要實現下面這2個方法。 ajax
/** * Returns <code>true</code> if the request is allowed to proceed through the filter normally, or <code>false</code> * if the request should be handled by the * {@link #onAccessDenied(ServletRequest,ServletResponse,Object) onAccessDenied(request,response,mappedValue)} * method instead. * * @param request the incoming <code>ServletRequest</code> * @param response the outgoing <code>ServletResponse</code> * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings. * @return <code>true</code> if the request should proceed through the filter normally, <code>false</code> if the * request should be processed by this filter's * {@link #onAccessDenied(ServletRequest,ServletResponse,Object)} method instead. * @throws Exception if an error occurs during processing. */ protected abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception; ... ... /** * Processes requests where the subject was denied access as determined by the * {@link #isAccessAllowed(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) isAccessAllowed} * method. * * @param request the incoming <code>ServletRequest</code> * @param response the outgoing <code>ServletResponse</code> * @return <code>true</code> if the request should continue to be processed; false if the subclass will * handle/render the response directly. * @throws Exception if there is an error processing the request. */ protected abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;
查看抽象類AccessControlFilter: redis
isAccessAllowed:表示是否容許訪問;mappedValue就是[urls]配置中攔截器參數部分,若是容許訪問返回true,不然false;spring
onAccessDenied:表示當訪問拒絕時是否已經處理了;若是返回true表示須要繼續處理;若是返回false表示該攔截器實例已經處理了,將直接返回便可。
另外AccessControlFilter還提供了以下方法用於處理如登陸成功後/重定向到上一個請求:
void setLoginUrl(String loginUrl) //身份驗證時使用,默認/login.jsp String getLoginUrl() Subject getSubject(ServletRequest request, ServletResponse response) //獲取Subject實例 boolean isLoginRequest(ServletRequest request, ServletResponse response)//當前請求是不是登陸請求 void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //將當前請求保存起來並重定向到登陸頁面 void saveRequest(ServletRequest request) //將請求保存起來,如登陸成功後再重定向回該請求 void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登陸頁面
要進行用戶訪問控制,能夠繼承AccessControlFilter。
下面就是自定義的訪問控制攔截器:KickoutSessionFilter:
package com.wyait.manage.filter; import java.io.Serializable; import java.util.ArrayDeque; import java.util.Deque; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import com.wyait.manage.pojo.User; 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; import com.lyd.admin.pojo.AdminUser; /** * * @項目名稱:wyait-manager * @類名稱:KickoutSessionFilter * @類描述:自定義過濾器,進行用戶訪問控制 * @建立人:wyait * @建立時間:2018年4月24日 下午5:18:29 * @version: */ public class KickoutSessionFilter extends AccessControlFilter { private static final Logger logger = LoggerFactory .getLogger(KickoutSessionFilter.class); private String kickoutUrl; // 踢出後到的地址 private boolean kickoutAfter = false; // 踢出以前登陸的/以後登陸的用戶 默認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); // 沒有登陸受權 且沒有記住我 if (!subject.isAuthenticated() && !subject.isRemembered()) { // 若是沒有登陸,直接進行以後的流程 return true; } Session session = subject.getSession(); logger.debug("==session時間設置:" + String.valueOf(session.getTimeout()) + "==========="); try { // 當前用戶 User user = (User) subject.getPrincipal(); String username = user.getUsername(); logger.debug("===當前用戶username:==" + username); Serializable sessionId = session.getId(); logger.debug("===當前用戶sessionId:==" + sessionId); // 讀取緩存用戶 沒有就存入 Deque<Serializable> deque = cache.get(username); logger.debug("===當前deque:==" + deque); 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) { logger.debug("===deque隊列長度:==" + deque.size()); 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 } } // ajax請求 // 若是被踢出了,(前者或後者)直接退出,重定向到踢出後的地址 if ((Boolean) session.getAttribute("kickout") != null && (Boolean) session.getAttribute("kickout") == true) { // 會話被踢出了 try { // 退出登陸 subject.logout(); } catch (Exception e) { // ignore } saveRequest(request); logger.debug("==踢出後用戶重定向的路徑kickoutUrl:" + kickoutUrl); // 重定向 WebUtils.issueRedirect(request, response, kickoutUrl); return false; } return true; } catch (Exception e) { // ignore //重定向到登陸界面 WebUtils.issueRedirect(request, response, "/login"); return false; } } }
public interface SessionDAO { /*如DefaultSessionManager在建立完session後會調用該方法; 如保存到關係數據庫/文件系統/NoSQL數據庫;便可以實現會話的持久化; 返回會話ID;主要此處返回的ID.equals(session.getId()); */ Serializable create(Session session); //根據會話ID獲取會話 Session readSession(Serializable sessionId) throws UnknownSessionException; //更新會話;如更新會話最後訪問時間/中止會話/設置超時時間/設置移除屬性等會調用 void update(Session session) throws UnknownSessionException; //刪除會話;當會話過時/會話中止(如用戶退出時)會調用 void delete(Session session); //獲取當前全部活躍用戶,若是用戶量多此方法影響性能 Collection<Session> getActiveSessions(); }
SessionDAO實現類:
a. AbstractSessionDAO提供了SessionDAO的基礎實現,如生成會話ID等; b. CachingSessionDAO提供了對開發者透明的會話緩存的功能,只須要設置相應的CacheManager便可; c. MemorySessionDAO直接在內存中進行會話維護; d. EnterpriseCacheSessionDAO提供了緩存功能的會話維護,默認狀況下使用MapCache實現,內部使用ConcurrentHashMap保存緩存的會話。
/** * EnterpriseCacheSessionDAO shiro sessionDao層的實現; * 提供了緩存功能的會話維護,默認狀況下使用MapCache實現,內部使用ConcurrentHashMap保存緩存的會話。 */ @Bean public EnterpriseCacheSessionDAO enterCacheSessionDAO() { EnterpriseCacheSessionDAO enterCacheSessionDAO = new EnterpriseCacheSessionDAO(); //添加緩存管理器 //enterCacheSessionDAO.setCacheManager(ehCacheManager()); //添加ehcache活躍緩存名稱(必須和ehcache緩存名稱一致) enterCacheSessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache"); return enterCacheSessionDAO; }
/** * * @描述:sessionManager添加session緩存操做DAO * @建立人:wyait * @建立時間:2018年4月24日 下午8:13:52 * @return */ @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); //sessionManager.setCacheManager(ehCacheManager()); sessionManager.setSessionDAO(enterCacheSessionDAO()); return sessionManager; }
/** * * @描述:kickoutSessionFilter同一個用戶多設備登陸限制 * @建立人:wyait * @建立時間:2018年4月24日 下午8:14:28 * @return */ public KickoutSessionFilter kickoutSessionFilter(){ KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter(); //使用cacheManager獲取相應的cache來緩存用戶登陸的會話;用於保存用戶—會話之間的關係的; //這裏咱們仍是用以前shiro使用的ehcache實現的cacheManager()緩存管理 //也能夠從新另寫一個,從新配置緩存時間之類的自定義緩存屬性 kickoutSessionFilter.setCacheManager(ehCacheManager()); //用於根據會話ID,獲取會話進行踢出操做的; kickoutSessionFilter.setSessionManager(sessionManager()); //是否踢出後來登陸的,默認是false;即後者登陸的用戶踢出前者登陸的用戶;踢出順序。 kickoutSessionFilter.setKickoutAfter(false); //同一個用戶最大的會話數,默認1;好比2的意思是同一個用戶容許最多同時兩我的登陸; kickoutSessionFilter.setMaxSession(1); //被踢出後重定向到的地址; kickoutSessionFilter.setKickoutUrl("/toLogin?kickout=1"); return kickoutSessionFilter; }
/** * shiro安全管理器設置realm認證、ehcache緩存管理、session管理器、Cookie記住我管理器 * @return */ @Bean public org.apache.shiro.mgt.SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 設置realm. securityManager.setRealm(shiroRealm()); // //注入ehcache緩存管理器; securityManager.setCacheManager(ehCacheManager()); // //注入session管理器; securityManager.setSessionManager(sessionManager()); //注入Cookie記住我管理器 securityManager.setRememberMeManager(rememberMeManager()); return securityManager; }
... //添加kickout認證 HashMap<String,Filter> hashMap=new HashMap<String,Filter>(); hashMap.put("kickout",kickoutSessionFilter()); shiroFilterFactoryBean.setFilters(hashMap); ... filterChainDefinitionMap.put("/**", "kickout,authc"); ...
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <!--head部分--> <head th:include="layout :: htmlhead" th:with="title='利易達貸款後臺'"> </head> <script type="text/javascript"> var href=window.location.href; if(href.indexOf("kickout")>0){ setTimeout("top.location.href='/login?kickout';", 0); }else{ setTimeout("top.location.href='/login';", 0); } </script> </html>
// 指定要求登陸時的連接 shiroFilterFactoryBean.setLoginUrl("/toLogin"); ... // 配置不會被攔截的連接 從上向下順序判斷 filterChainDefinitionMap.put("/login", "anon");
上面兩個配置,便可解決頁面重定向後,嵌套問題。
若是對用戶在線數量進行限制,踢出了以前登陸的用戶A;這時候用戶A在系統中,發送了一個ajax請求,會出現彈框空白等問題;
package com.wyait.manage.utils; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * * @項目名稱:wyait-manager * @類名稱:ShiroFilterUtils * @類描述:shiro工具類 * @建立人:wyait * @建立時間:2018年4月24日 下午5:12:04 * @version: */ public class ShiroFilterUtils { private static final Logger logger = LoggerFactory .getLogger(ShiroFilterUtils.class); /** * * @描述:判斷請求是不是ajax * @建立人:wyait * @建立時間:2018年4月24日 下午5:00:22 * @param request * @return */ public static boolean isAjax(ServletRequest request){ String header = ((HttpServletRequest) request).getHeader("X-Requested-With"); if("XMLHttpRequest".equalsIgnoreCase(header)){ logger.debug("shiro工具類【wyait-manager-->ShiroFilterUtils.isAjax】當前請求,爲Ajax請求"); return Boolean.TRUE; } logger.debug("shiro工具類【wyait-manager-->ShiroFilterUtils.isAjax】當前請求,非Ajax請求"); return Boolean.FALSE; } }
private final static ObjectMapper objectMapper = new ObjectMapper(); ... // ajax請求 /** * 判斷是否已經踢出 * 1.若是是Ajax 訪問,那麼給予json返回值提示。 * 2.若是是普通請求,直接跳轉到登陸頁 */ //判斷是否是Ajax請求 ResponseResult responseResult = new ResponseResult(); if (ShiroFilterUtils.isAjax(request) ) { logger.debug(getClass().getName()+ "當前用戶已經在其餘地方登陸,而且是Ajax請求!"); responseResult.setCode(IStatusMessage.SystemStatus.MANY_LOGINS.getCode()); responseResult.setMessage("您已在別處登陸,請您修改密碼或從新登陸"); out(response, responseResult); }else{ // 重定向 WebUtils.issueRedirect(request, response, kickoutUrl); } ... /** * * @描述:response輸出json * @建立人:wyait * @建立時間:2018年4月24日 下午5:14:22 * @param response * @param result */ public static void out(ServletResponse response, ResponseResult result){ PrintWriter out = null; try { response.setCharacterEncoding("UTF-8");//設置編碼 response.setContentType("application/json");//設置返回類型 out = response.getWriter(); out.println(objectMapper.writeValueAsString(result));//輸出 logger.error("用戶在線數量限制【wyait-manager-->KickoutSessionFilter.out】響應json信息成功"); } catch (Exception e) { logger.error("用戶在線數量限制【wyait-manager-->KickoutSessionFilter.out】響應json信息出錯", e); }finally{ if(null != out){ out.flush(); out.close(); } } }
/** * 判斷是否登陸,沒登陸刷新當前頁,促使Shiro攔截後跳轉登陸頁 * @param result ajax請求返回的值 * @returns {若是沒登陸,刷新當前頁} */ function isLogin(result){ if(result && result.code && result.code == '1101'){ window.location.reload(true);//刷新當前頁 } return true;//返回true }
$.post("/user/delUser",{"id":id},function(data){ //判斷用戶是否登陸 if(isLogin(data)){ if(data=="ok"){ //回調彈框 layer.alert("刪除成功!",function(){ layer.closeAll(); //加載load方法 load(obj);//自定義 }); }else{ layer.alert(data);//彈出錯誤提示 } } });
只改動了userList.js用戶列表界面,其餘界面//TODO
session默認有效時間:30分鐘(1800s)
# 會話超時(秒)1天 server.session.timeout=86400
使用shiro進行用戶在線數量限制功能;用戶登陸後,2分鐘不操做,以後session失效。
// //注入session管理器; securityManager.setSessionManager(sessionManager());
SessionManager,配置EnterpriseCacheSessionDAO:
sessionManager.setSessionDAO(enterCacheSessionDAO());
EnterpriseCacheSessionDAO類,存取session的時候,是經過ehcache緩存中操做的。
這裏若是配置有緩存的話須要給其配置一個cache的鍵相似於:
shiro默認了一個默認值爲:shiro-activeSessionCache,若是不相同(cache文件中的鍵值) 須要進行替換,最終進行session存取的類爲CachingSessionDAO
緩存管理器使用的是org.apache.shiro.cache.ehcache.EhCacheManager,那麼最終shiro在找session的時候也會調用getCache。
Ehcache.xml配置
<!-- shiro-activeSessionCache活躍用戶session緩存策略 --> <cache name="shiro-activeSessionCache" maxElementsInMemory="10000" timeToIdleSeconds="120" timeToLiveSeconds="120" maxElementsOnDisk="10000000" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </cache>
這裏配置了session緩存時間爲2分鐘,故會出現登陸2分鐘無操做後,session失效問題。
SecurityUtils.getSubject().getSession().setTimeout(30000);//毫秒
】,ehcache中session有效時間120s不變;在無操做30s後,請求後臺,報錯以下:
org.apache.shiro.session.ExpiredSessionException: Session with id [8aac0daf-c432-44b6-86cc-a618095ad2bd] has expired. Last access time: 18-4-24 上午11:32. Current time: 18-4-24 上午11:33. Session timeout is set to 30 seconds (0 minutes) at org.apache.shiro.session.mgt.SimpleSession.validate(SimpleSession.java:292) ~[shiro-core-1.3.1.jar:1.3.1] at org.apache.shiro.session.mgt.AbstractValidatingSessionManager.doValidate(AbstractValidatingSessionManager.java:186) ~[shiro-core-1.3.1.jar:1.3.1] ... ...
故ehcache緩存中session的有效時間和服務器端session有效時間必須配置一致。
//session有效時間1天(毫秒) SecurityUtils.getSubject().getSession().setTimeout(86400000);
SecurityUtils.getSubject().getSession().setTimeout(-1000l);
注意:這裏設置的時間單位是:ms,可是Shiro會把這個時間轉成:s,並且是會舍掉小數部分,這樣設置的是-1ms,轉成s後就是0s,立刻就過時了。全部要是除以1000之後仍是負數,必須設置小於-1000
<!-- shiro-activeSessionCache活躍用戶session緩存策略(秒) --> <cache name="shiro-activeSessionCache" maxElementsInMemory="10000" timeToIdleSeconds="86400" timeToLiveSeconds="86400" maxElementsOnDisk="10000000" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </cache>
經過代碼中查看session有效時間:
logger.debug("session設置的有效時間:"+request.getSession().getMaxInactiveInterval()); logger.debug("shiro中session設置的有效時間:"+SecurityUtils.getSubject().getSession().getTimeout()); //86400(秒) //86400000(毫秒)
具體實現能夠根據具體需求作調整;近期提供redis實現版本。
連接入口--> spring boot + shiro 動態更新用戶信息:https://blog.51cto.com/wyait/2112200
連接入口--> springboot + shiro 權限註解、統一異常處理、請求亂碼解決 :https://blog.51cto.com/wyait/2125708
以上更新,項目wyait-manage、wyait-manage-1.2.0源碼同步更新。
前篇:
spring boot + mybatis + layui + shiro後臺權限管理系統:https://blog.51cto.com/wyait/2082803
後篇:
項目源碼:(包含數據庫源碼)
github源碼: https://github.com/wyait/manage.git
碼雲:https://gitee.com/wyait/manage.git
github對應項目源碼目錄:wyait-manage-1.2.0
碼雲對應項目源碼目錄:wyait-manage-1.2.0
版本升級及內容優化版本,改動內容: