文档章节

SpringBoot+Shiro学习之自定义拦截器管理在线用户(踢出用户)

邹海清
 邹海清
发布于 2017/03/05 17:17
字数 2000
阅读 1667
收藏 106

应用场景

  1. 我们经常会有用到,当A 用户在北京登录 ,然后A用户在天津再登录 ,要踢出北京登录的状态。如果用户在北京重新登录,那么又要踢出天津的用户,这样反复。又或是需要限制同一用户的同时在线数量,超出限制后,踢出最先登录的或是踢出最后登录的。

  2. 第一个场景踢出用户是由用户触发的,有时候需要手动将某个在线用户踢出,也就是对当前在线用户的列表进行管理。

·······················································································································································

个人博客:http://z77z.oschina.io/

此项目下载地址:https://git.oschina.net/z77z/springboot_mybatisplus

·······················································································································································

实现思路

spring security就直接提供了相应的功能;Shiro的话没有提供默认实现,不过可以很容易的在Shiro中加入这个功能。那就是使用shiro强大的自定义访问控制拦截器:AccessControlFilter,集成这个接口后要实现下面这三个方法。

abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;  

boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception; 
 
abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;   

isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;

onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。

onPreHandle:会自动调用这两个方法决定是否继续处理;

另外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;如果我们要添加一些通用数据我们可以直接继承PathMatchingFilter。

下面就是我实现的访问控制拦截器:KickoutSessionControlFilter:

/**
 * @author 作者 z77z
 * @date 创建时间:2017年3月5日 下午1:16:38
 * 思路:
 * 1.读取当前登录用户名,获取在缓存中的sessionId队列
 * 2.判断队列的长度,大于最大登录限制的时候,按踢出规则
 *  将之前的sessionId中的session域中存入kickout:true,并更新队列缓存
 * 3.判断当前登录的session域中的kickout如果为true,
 * 想将其做退出登录处理,然后再重定向到踢出登录提示页面
 */
public class KickoutSessionControlFilter extends AccessControlFilter {

    private String kickoutUrl; //踢出后到的地址
    private boolean kickoutAfter = 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) {
        this.cache = cacheManager.getCache("shiro_redis_cache");
    }

    @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();
        SysUser user = (SysUser) subject.getPrincipal();
        String username = user.getNickname();
        Serializable sessionId = session.getId();

        //读取缓存   没有就存入
        Deque<Serializable> deque = cache.get(username);
        
        //如果此用户没有session队列,也就是还没有登录过,缓存中没有
        //就new一个空队列,不然deque对象为空,会报空指针
        if(deque==null){
        	deque = new LinkedList<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;
            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 ((Boolean)session.getAttribute("kickout")!=null&&(Boolean)session.getAttribute("kickout") == true) {
            //会话被踢出了
            try {
            	//退出登录
                subject.logout();
            } catch (Exception e) { //ignore
            }
            saveRequest(request);
            //重定向
            WebUtils.issueRedirect(request, response, kickoutUrl);
            return false;
        }
        return true;
    }
}

将这个自定义的拦截器配置在ShiroConfig.java文件中:

/**
  * 限制同一账号登录同时登录人数控制
  * @return
  */
 public KickoutSessionControlFilter kickoutSessionControlFilter(){
 	KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
 	//使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
 	//这里我们还是用之前shiro使用的redisManager()实现的cacheManager()缓存管理
 	//也可以重新另写一个,重新配置缓存时间之类的自定义缓存属性
 	kickoutSessionControlFilter.setCacheManager(cacheManager());
 	//用于根据会话ID,获取会话进行踢出操作的;
 	kickoutSessionControlFilter.setSessionManager(sessionManager());
 	//是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序。
 	kickoutSessionControlFilter.setKickoutAfter(false);
 	//同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
 	kickoutSessionControlFilter.setMaxSession(1);
 	//被踢出后重定向到的地址;
 	kickoutSessionControlFilter.setKickoutUrl("/kickout");
     return kickoutSessionControlFilter;
  }

将这个kickoutSessionControlFilter()注入到shiroFilterFactoryBean中:

//自定义拦截器
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
//限制同一帐号同时在线的个数。
filtersMap.put("kickout", kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);

由于我们链接权限的控制是动态存在数据库中的,这个可以去看我之前动态权限控制的博文,所以我们还要在数据库中修改链接的权限,将kickout这个自定义的权限配置在对应的链接上。如下图:

权限表

还要编写对应的被踢出的跳转页面:

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%
	String path = request.getContextPath();
	String basePath = request.getScheme() + "://"
			+ request.getServerName() + ":" + request.getServerPort()
			+ path;
%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript"
	src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
<title>被踢出</title>
</head>
<body>
被踢出 或则在另一地方登录,或已经达到此账号登录上限被挤掉。
<input type="button" id="login" value="重新登录" />
</body>
<script type="text/javascript">
$("#login").click(function(){
	window.open("<%=basePath%>/login"); 
});
</script>
</html>

到此,第一个场景就实现了,写到这里实际第二个场景的实现思路已经就很明显了,可以通过sessionDAO获取到全部的shiro会话List,然后显示在前端页面,踢出对应用户就可以使用在对应sessionId的session域中设置key为kickout的值为true,上面的KickoutSessionControlFilter就会判断session域中的kickout值,做响应的处理。这里我就先不上代码了,大家可以自己试一试。之后再把代码同步到我的码云上,供大家学习交流。

处理了这个需求后,我发现一个问题,这里有一个前提,我们知道Ajax不能做页面redirect和forward跳转,所以Ajax请求假如没登录,那么这个请求给用户的感觉就是没有任何反应,而用户又不知道用户已经退出了。这个就要对ajax请求做相应的优化,我已经有解决思路了,大家也可以思考下,我也会在下一博提供代码。

还有我接下来会对之前的前端页面进行完善,比如下面是我更新的登录页面:

登录页面

已经更新到我的码云上面。

© 著作权归作者所有

共有 人打赏支持
邹海清
粉丝 128
博文 17
码字总数 32303
作品 0
成都
程序员
私信 提问
加载中

评论(2)

邹海清
邹海清

引用来自“zheng875”的评论

Deque<Serializable> deque = cache.get(username);
刚登陆查询的时候,肯定是没有的,这时候返回是null。redisCache不会给你返回一个队列出来吧,下面用到的contains肯定就挂了。很奇怪,我每次拉你代码都拉不到最新的
哎 没有清空redis测试一遍 😅 报空指针这个问题今天早上到公司才发现 stable_version是最新的 这个我测试过

master分支方便我在公司和家里提交
zheng875
zheng875
Deque<Serializable> deque = cache.get(username);
刚登陆查询的时候,肯定是没有的,这时候返回是null。redisCache不会给你返回一个队列出来吧,下面用到的contains肯定就挂了。很奇怪,我每次拉你代码都拉不到最新的
shiro拦截器使用的问题(springboot+shiro)

使用shiro开发的时候,配置拦截器有什么作用吗?配置了拦截器之后因该怎么写才能处理拦截到的请求呢? 初始化的时候配置了拦截器。但是是跳到了controller之后调用的Subject.login()方法进入...

HaleyZhang
08/31
635
2
springboot + shiro之登录人数限制、登录判断重定向、session时间设置

springboot + shiro之登录人数控制 项目 前篇:spring boot + mybatis + layui + shiro后台权限管理系统 本文是基于spring boot + mybatis + layui + shiro后台权限管理系统开发的,新增功能...

wyait
04/24
0
0
结合SSM框架讲解Shiro案例--Shiro Demo

本教程结合SSM(SpringMVC + Mybatis)框架讲解Shiro(Shiro是 Java 的一个安全框架。我们经常看到它被拿来和 Spring 的 Security 来对比。),讲解的内容有自定义Shiro拦截器,Shiro Freem...

8446666
2016/06/29
20.9K
8
Apache Shiro权限控制实战,权限控制SpringMVC + Mybatis + Shiro

Demo已经部署到线上,地址是http://shiro.itboy.net, 管理员帐号:admin,密码:sojson.com 如果密码错误,请用sojson。 PS:你可以注册自己的帐号,然后用管理员赋权限给你自己的帐号,但是...

8446666
2016/06/29
8.4K
3
spring boot + shiro 动态更新用户信息

spring boot + shiro 动态更新用户信息 场景 用户A在线,管理员在后台更改了用户A信息(资料或权限)之后;用户A再进行下一步操作时,会被拦截并退出登录状态,再登录才可以执行操作;来确保...

wyait
05/03
0
0

没有更多内容

加载失败,请刷新页面

加载更多

jquery

语法 描述 实例 $("*") 选取所有元素 在线实例 $(this) 选取当前 HTML 元素 在线实例 $("p.intro") 选取 class 为 intro 的 <p> 元素 在线实例 $("p:first") 选取第一个 <p> 元素 在线实例 ...

mskk
13分钟前
0
0
微信红包设计方案

前言 微信红包一经推出,春节期间微信用户红包总发送量达80.8亿,红包峰值40.9w/秒,在如此量级下,系统设计存在各种变数,稍有闪失会功亏一篑。 红包系统 红包系统有三部分组成:信息流,业...

春哥大魔王的博客
23分钟前
0
0
微信开发-正式号的配置

1、设置相关 业务域名的设置(不设置的话,相关页面会显示防欺诈盗号信息提示) JS接口安全域名设置 网页授权域名设置 注意:以上三个设置中都有下载文件的提示,此文件内容一样,下载一个即可...

Code辉
29分钟前
1
0
微服务之Eureka服务发现

当调用API或者发起网络通信的时候,无论如何我们都要知道被调用方的IP和服务端口,大部分情况是通过域名和服务端口,事实上基于DNS的服务发现,因为DNS缓存、无法自治和其他不利因素的存在,...

架构师springboot
29分钟前
0
0
spring boot2 admin login

版本: admin server 配置 admin client 配置 参考资料

showlike
31分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部