前篇: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
版本升级及内容优化版本,改动内容: