AccessControlFilter(https://www.jianshu.com/p/9bfa22b0e905)

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

 

应用场景

  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); //如果队列里没有此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值,做响应的处理。这里我就先不上代码了,大家可以自己试一试。之后再把代码同步到我的码云上,供大家学习交流。

Shiro 之 HashedCredentialsMatcher 认证匹配

Shiro 提供了用于加密密码和验证密码服务的 CredentialsMatcher 接口,而 HashedCredentialsMatcher 正是 CredentialsMatcher 的一个实现类。写项目的话,总归会用到用户密码的非对称加密,目前主流的非对称加密方式是 MD5 ,以及在 MD5 上的加盐处理,而 HashedCredentialsMatcher 也允许我们指定自己的算法和盐。本文将介绍 HashedCredentialsMatcher 的使用,以及对相关源码的进行解析,MD5 等加密知识请自行查阅。

HashedCredentialsMatcher 的使用
要使用 HashedCredentialsMatcher ,那么首先要进行配置。当然,前提是你已经引入了 Shiro 库。总共有三种配置方式:

XML 格式

<bean id="credentialsMatcher"
class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 加密方式 -->
<property name="hashAlgorithmName" value="MD5" />
<!-- 加密次数 -->
<property name="hashIterations" value="2" />
<!-- 存储散列后的密码是否为16进制 -->
<property name="storedCredentialsHexEncoded" value="true" />
</bean>

ini 等配置文件

首先在 web.xml 中自定义 shiro.ini 位置

<filter>
<filter-name>ShiroFilter</filter-name>
<filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
<init-param>
<param-name>configPath</param-name>
<param-value>/WEB-INF/shiro.ini</param-value>
</init-param>
</filter>

然后除了 shiro 的通常配置之外,需加上:

credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
## 加密方式
credentialsMatcher.hashAlgorithmName=md5
## 加密次数
credentialsMatcher.hashIterations=2
## 存储散列后的密码是否为16进制
credentialsMatcher.storedCredentialsHexEncoded=true

建立 ShiroConfiguration 配置类,除了 shiro 的通常配置之外,需加上:

@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//加密方式
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//加密次数
hashedCredentialsMatcher.setHashIterations(2);
//存储散列后的密码是否为16进制
//hashedCredentialsMatcher.isStoredCredentialsHexEncoded();
return hashedCredentialsMatcher;
}

然后,在登录方法或者自定义的输入中获取登录 token,我选择的方式是在/login中获取:

public Object login(@RequestBody User userParam, HttpSession session) {
String name = userParam.getName();
name = HtmlUtils.htmlEscape(name);
Subject subject = SecurityUtils.getSubject();
// 生成token
UsernamePasswordToken token = new UsernamePasswordToken(name, userParam.getPassword());
try {
// 从自定义Realm获取安全数据进行验证
subject.login(token);
User user = userService.getByName(name);
session.setAttribute("user", user);
return Result.success();
} catch (AuthenticationException e) {
String message ="账号密码错误";
return Result.fail(message);
}
}

接下来,是自定义 Realm,之前我也有博文写过相关知识,所以只贴下代码作参考:

public class JPARealm extends AuthorizingRealm {

@Autowired
private UserService userService;

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 权限分配的相关知识在此不做介绍,重点在验证方面
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
return s;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = token.getPrincipal().toString();
User user = userService.getByName(userName);
String passwordInDB = user.getPassword();
String salt = user.getSalt();
// 认证信息token里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名
// 盐也放进去
// 这样通过配置中的 HashedCredentialsMatcher 进行自动校验
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB, ByteSource.Util.bytes(salt),
getName());
return authenticationInfo;
}
}

HashedCredentialsMatcher 的源码分析
从开发者的角度来看,我们可以自己实现 CredentialsMatcher 的一个类来实现定制化的账户密码验证机制,例如:

public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = getCredentials(token);
Object accountCredentials = getCredentials(info);
return super.equals(tokenCredentials, accountCredentials);
}
}

SimpleCredentialsMatcher 是 CredentialsMatcher 这个类的默认实现,相信我,你基本上不会去自己实现 getCredentials 这种涉及到底层编码的方法,重点在于重写 doCredentialsMatch ,在这里你可以自定义账户密码验证机制。

不过实现 doCredentialsMatch 你还是有可能觉得麻烦,HashedCredentialsMatcher 封装好了 doCredentialsMatch() 方法,你可以完全不用管它。

上一节的使用中,流程就是:使用 token 类将用户输入的信息封装,然后采用 token 进行 login 操作。此时 shiro 将使用 token 中携带的用户信息调用 Realm 中自定义的 doGetAuthenticationInfo 方法进行校验比对,比对成功则登录成功。

原文链接:https://blog.csdn.net/zx48822821/article/details/84325504

shrio总结的更多相关文章

  1. SpringMVC + Spring + MyBatis 整合 + Spring shrio + easyUI + 权限管理框架,带shrio session和shrio cache集群实现方案

    工作之余先来写了一个不算规范的简单架子 基于spring mvc + spring + mybatis + Spring shrio 基于redis的集群方案 系统权限部分,分成多个机构,其中每个机构 ...

  2. SpringMVC集成shrio框架

    使用SHIRO的步骤:1,导入jar2,配置web.xml3,建立dbRelm4,在Spring中配置 添加所需jar包: <!--Apache Shiro所需的jar包--> <d ...

  3. Shrio认证详解+自定义Realm

    Authentication(身份认证)是Shiro权限控制的第一步,用来告诉系统你就是你. 在提交认证的时候,我们需要给系统提交两个信息: Principals:是一个表示用户的唯一属性,可以是用户 ...

  4. Shrio授权验证详解

    所谓授权,就是控制你是否能访问某个资源,比如说,你可以方位page文件夹下的jsp页面,但是不可以访问page文件夹下的admin文件夹下的jsp页面. 在授权中,有三个核心元素:权限,角色,用户. ...

  5. spring boot集成shrio用于权限控制

    下面是一个简单的springBoot集成shrio的项目,技术是:spring boot+idea+gradle+shrio+mybatis 1:首先在build.gradle中导入依赖 builds ...

  6. shrio的简单认识

    博客讲解; shrio的知识储备 shrio的简单认识 笔记整理地址: Shrio.pdf 下载 Shrio理论.doc   下载 shrio知识储备.doc  下载

  7. shrio的知识储备

    博客讲解; shrio的知识储备 shrio的简单认识 笔记整理地址: Shrio.pdf 下载 Shrio理论.doc 下载 Shrio知识储备.doc  下载 Shrio的知识储备 (一)   S ...

  8. shrio的rememberMe不起作用

    在移植项目.每次重启服务器需要登录.比较麻烦.于是研究下shrio的功能. rememberMe大概可以满足我的需求.但是跟着网上配置了.不起作用...... 于是乎查看源代码拉.java的好处... ...

  9. shrio中去掉 login;JSESSIONID

    shrio中去掉 login;JSESSIONID 在session管理器配置页面中新增一条记录

  10. Shrio Demo

    package com.atguigu.shiro.helloworld; import org.apache.shiro.SecurityUtils; import org.apache.shiro ...

随机推荐

  1. Python File flush() 方法

    概述 flush() 方法是用来刷新缓冲区的,即将缓冲区中的数据立刻写入文件,同时清空缓冲区,不需要是被动的等待输出缓冲区写入.高佣联盟 www.cgewang.com 一般情况下,文件关闭后会自动刷 ...

  2. P3270 [JLOI2016]成绩比较 容斥 数论 组合数学 拉格朗日插值

    LINK:成绩比较 大体思路不再赘述 这里只说几个我犯错的地方. 拉格朗日插值的时候 明明是n次多项式 我只带了n个值进去 导致一直GG. 拉格朗日插值的时候 由于是从1开始的 所以分母是\((i-1 ...

  3. bzoj 2839 集合计数 容斥\广义容斥

    LINK:集合计数 容斥简单题 却引出我对广义容斥的深思. 一直以来我都不理解广义容斥是为什么 在什么情况下使用. 给一张图: 这张图想要表达的意思就是这道题目的意思 而求的东西也和题目一致. 特点: ...

  4. jmeter分布式踩得坑汇总

    一.普通的配置文件基本都能网上搜索资料,这里就简单记录: a.jmeter.properties几处修改:1.remote_hosts=master压力机Ip;2.server_port,开启服务器端 ...

  5. OAuth2.0-2jwt令牌

    JWT令牌 解决了之前普通令牌每次都要远程校验令牌带来得网络消耗:(有网友说可以将令牌验证从认证服务器上放到各个资源服务器上,不知是否可行?) JWT令牌的优点: 1.jwt基于json,非常方便解析 ...

  6. “随手记”开发记录day14

    今天继续昨天没有完成的增加“修改”功能.对于已经添加的记账记录,长按可以进行修改和删除的操作. 但是今天并没有完成……

  7. 打破你的认知!Java空指针居然还能这样玩,90%人不知道…

    相信在座的各位都遇到过空指针异常,不甚其烦,本文不是教你避免空指针,而是一些对空指针其他方面的理解. 本文可能有点另类,也可能会打破你对空指针的认知. 1.null.method() 空指针? 我们知 ...

  8. 申请支付宝app支付签约综合评分不足,拒绝不通过快速强开通支付宝App支付强开,强开支付宝App支付产品权限!

    一.如何开通支付宝App支付 正常来说,按照官方的指引要求填写相关资料,即可开通支付宝手机网站支付.但是,更多的时候我们的申请都会碰到一些阻力,常见的阻力就是“系统综合评估签约条件不满足,谢谢您的支持 ...

  9. css 命名规则 BEM!

    随着CSS的发展,使用CSS有语义化的命名约定和CSS层的分离,将有助于它的可扩展性,性能的提高和代码的组织管理.著作权归作者所有. BEM本质应该是一个css命名方案,最流行的命名规则之一就是BEM ...

  10. 【Redis】Redis开篇与如何安装单机版Redis,这次我会了!!

    写在前面 很早之前,就有不少小伙伴微信留言说:冰河,你能不能写一个Redis专栏啊,我最近在学习Redis,看书看不下去,学习视频又觉得视频太长了,还是看你的文章比较给力!哈哈,原来我写的文章能够让小 ...