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. PHP mb_substr() 函数

    实例 从字符串中返回 "菜鸟": <?php echo mb_substr("菜鸟教程", 0, 2); // 输出:菜鸟 ?> 定义和用法 mb_ ...

  2. luogu P2525 Uim的情人节礼物 其之壱

    LINK:Uim的情人节礼物·其之壱 壱 古代通壹 常在日文中出现. 完全可以使用STL -->prev_permutation来解决. 不过我简单了解了一下康托展开. 这是一个一个排列对应一个 ...

  3. OpenCL Kernel设计优化

    使用Intel® FPGA SDK for OpenCL™ 离线编译器,不需要调整kernel代码便可以将其最佳的适应于固定的硬件设备,而是离线编译器会根据kernel的要求自适应调整硬件的结构. 通 ...

  4. FastAPI框架入门 基本使用, 模版渲染, form表单数据交互, 上传文件, 静态文件配置

    安装 pip install fastapi[all] pip install unicorn 基本使用(不能同时支持,get, post方法等要分开写) from fastapi import Fa ...

  5. Android Studio--家庭记账本(一)

    今天通过观看视频,根据老师所讲内容,编译代码.实现了Android Studio记账本里面的增加功能 源代码如下: CostBean.java: package com.example.family; ...

  6. 【av68676164(p23-p24)】临界区和锁

    4.4.1 临界资源和临界区 临界资源(Critical Resource) 一次只允许一个进程独占访问(使用)的资源 例:例子中的共享变量i 临界区(Critical Section) 进程中访问临 ...

  7. PyTorch上路

    PyTorch torch.autograd模块 深度学习的算法本质上是通过反向传播求导数, PyTorch的autograd模块实现了此功能, 在Tensor上的所有操作, autograd都会为它 ...

  8. django python manage.py runserver 流程

    python manage.py runserver 流程分析 版本 python27 django 1.0 搭建可运行的环境 创建python27 虚拟环境 github 下载 django-1.0 ...

  9. C#LeetCode刷题-树状数组

    树状数组篇 # 题名 刷题 通过率 难度 218 天际线问题   32.7% 困难 307 区域和检索 - 数组可修改   42.3% 中等 315 计算右侧小于当前元素的个数   31.9% 困难 ...

  10. oracle正则表达式语法介绍及实现手机号码匹配方法

    Oracle10g提供了在查询中使用正则表达的功能,它是通过各种支持正则表达式的函数在where子句中实现的.本文将简单的介绍oracle正则表达式常用语法,并通过一个手机特号匹配的例子演示正则表达式 ...