前篇:spring boot + mybatis + layui + shiro后台权限管理系统:https://blog.51cto.com/wyait/2082803
本文是基于spring boot + mybatis + layui + shiro后台权限管理系统开发的,新增功能:
后篇:
项目源码:(包含数据库源码)
github源码: https://github.com/wyait/manage.git
码云:https://gitee.com/wyait/manage.git
github对应项目源码目录:wyait-manage-1.2.0
码云对应项目源码目录:wyait-manage-1.2.0
同一个用户,先在A×××登录;之后在B×××登录时,退出A×××的登录状态;反之相同。或者限制同一个用户在不同的设备上,同时在线的数量;
基于shiro和ehcache实现
spring security就直接提供了相应的功能;
Shiro的话没有提供默认实现,不过可以在Shiro中加入这个功能。就是使用shiro强大的自定义访问控制拦截器:AccessControlFilter,集成这个接口后要实现下面这2个方法。
/**
* Returns true
if the request is allowed to proceed through the filter normally, or false
* if the request should be handled by the
* {@link #onAccessDenied(ServletRequest,ServletResponse,Object) onAccessDenied(request,response,mappedValue)}
* method instead.
*
* @param request the incoming ServletRequest
* @param response the outgoing ServletResponse
* @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings.
* @return true
if the request should proceed through the filter normally, false
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 ServletRequest
* @param response the outgoing ServletResponse
* @return true
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:
isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;
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> 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 deque = cache.get(username);
logger.debug("===当前deque:==" + deque);
if (deque == null) {
// 初始化队列
deque = new ArrayDeque();
}
// 如果队列里没有此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 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 hashMap=new HashMap();
hashMap.put("kickout",kickoutSessionFilter());
shiroFilterFactoryBean.setFilters(hashMap);
...
filterChainDefinitionMap.put("/**", "kickout,authc");
...
// 指定要求登录时的链接
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配置
这里配置了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
通过代码中查看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
转载于:https://blog.51cto.com/wyait/2107423