shiro实现无状态的会话,带源码分析
转载请在页首明显处注明作者与出处
朱小杰 http://www.cnblogs.com/zhuxiaojie/p/7809767.html
一:说明
在网上都找不到相关的信息,还是翻了大半天shiro的源码才找到答案。亲试绝对可行,带源码分析
很多时候,开发的项目不仅仅是一个基于浏览器的项目,还可能是基于app的项目,基于小程序的项目,而这些项目都是无状态的。而普通web项目中,一个web项目的会话是由session保持的,而session又是由浏览器携带的cookie来验证身份的,可以这么说,一个会话就是依赖于cookie,但是app与小程序是没有cookie维持的。
一般的作法会在header中带有一个token,或者是在参数中,后台根据这个token来进行校验这个用户的身份,但是这个时候,servlet中的session就无法保存,我们在这个时候,就要实现自己的会话创建,普通的作法就是重写session与request的接口,然后在过滤器在把它替换成自己的request,所以得到的session也是自己的session,然后根据token来创建和维护会话。
但在shiro中会怎么做呢?
二:shiro介绍
shiro是一个权限验证框架,它比spring security的功能要少一些,但是我却更喜欢shiro,因为spring security封装的太死了,如果要重写一些功能,特别的麻烦,而shiro中使用了大量的策略模式,使得开发人员可以很好的替换成自己的策略,灵活性更加强,可以定义自己的过滤器来实现自己需要的一些功能。
shiro中的权限操作是委托给securityManager的,而securityManager管理session又是委托给sessionManager的,在开发web项目中,我们一般会使用
org.apache.shiro.web.mgt.DefaultWebSecurityManager
来创建securityManager,我们看一下这个DefaultWebSecurityManager默认是使用的哪个session管理器,它的构造方法如下
public DefaultWebSecurityManager() {
super();
((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
this.sessionMode = HTTP_SESSION_MODE;
setSubjectFactory(new DefaultWebSubjectFactory());
setRememberMeManager(new CookieRememberMeManager());
setSessionManager(new ServletContainerSessionManager());//这里可以看到是使用的servlet的默认管理器
}
可以看到,如果构造一个DefaultWebSecurityManager,它使用的是
org.apache.shiro.web.session.mgt.ServletContainerSessionManager
它是依赖于浏览器的cookie来维护session的,那肯定不能实现无状态的会话。
不过shiro还提供了另一个基于web的session管理器,它就是
org.apache.shiro.web.session.mgt.DefaultWebSessionManager
如果我们想实现自己的一套session管理器,都会选择去继承它来重写
小提示:笔者1.4.0的版本,当前是最新版本,无法直接在security中设置sessionManager的时候,直接new一个DefaultWebSessionManager,如下:
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setSessionManager(new DefaultWebSessionManager());
securityManager.setRealm(new WebRealm());
return securityManager;
}
如果直接设置为DefaultWebSessionManager,那么在有http请求的时候会报错,提示找不到SecurityManager,解决办法是写一个类来继承它,哪怕继承后什么都不做,都可以解决这个问题
三:重写shiro的sessionManager
上面说到我们要重写DefaultWebSessionManager,那我们要怎么重写呢?
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.UUID; /**
* @author zxj<br>
* 时间 2017/11/8 15:55
* 说明 ...
*/
public class StatelessSessionManager extends DefaultWebSessionManager {
/**
* 这个是服务端要返回给客户端,
*/
public final static String TOKEN_NAME = "TOKEN";
/**
* 这个是客户端请求给服务端带的header
*/
public final static String HEADER_TOKEN_NAME = "token";
public final static Logger LOG = LoggerFactory.getLogger(StatelessSessionManager.class); @Override
public Serializable getSessionId(SessionKey key) {
Serializable sessionId = key.getSessionId();
if(sessionId == null){
HttpServletRequest request = WebUtils.getHttpRequest(key);
HttpServletResponse response = WebUtils.getHttpResponse(key);
sessionId = this.getSessionId(request,response);
}
HttpServletRequest request = WebUtils.getHttpRequest(key);
request.setAttribute(TOKEN_NAME,sessionId.toString());
return sessionId;
} @Override
protected Serializable getSessionId(ServletRequest servletRequest, ServletResponse servletResponse) {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = request.getHeader(HEADER_TOKEN_NAME);
if(token == null){
token = UUID.randomUUID().toString();
} //这段代码还没有去查看其作用,但是这是其父类中所拥有的代码,重写完后我复制了过来...开始
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
//这段代码还没有去查看其作用,但是这是其父类中所拥有的代码,重写完后我复制了过来...结束
return token;
} }
三:源码分析
上面就是完整的重写的代码,我们一个一个方法来看
3.1:第一个方法
public Serializable getSessionId(SessionKey key)
这个方法的覆盖和它的父类其实没有太大的区别,逻辑上面都是通过一个sessionKey来获取一个sessionId,但是重写的部分多了一个把获取到的token设置到request的部分,这是因为app调用登陆接口的时候,是没有token的,登陆成功后,产生了token,我们把它放到request中,返回结果给客户端的时候,把它从request中取出来,并且传递给客户端,客户端每次带着这个token过来,就相当于是浏览器的cookie的作用,也就能维护会话了
这里不得不说一下sessionId和sessionKey的区别了,本人也是因为这个东西坑了好久,从字面上面看,sessionKey是一个对象,而sessionId是一个serializable对象,实际上从我们返回的token可以知道,它就是一个String。
sessionKey是在sessionStore中,对应存储的key值,而sessionId则就是请求带来的token,或者是浏览器请求的cookie中的jsessionid。
我们要想象一个,他们有什么关系呢?我们通过sessionId,应该得到sessionKey,然后通过sessionKey,能在sessionStore中找到session,那我们就把sessionId与sessionKey相等吧,这样就不用找对应关系了,因为sessionId就等于sessionKey的话,那我们也不需要保存他们之间的对应关系了,而其实DefaultWebSessionManager也是这样做的,因数sessionKey这个对象里面就有一个sessionId。
但是有一个值得注意的是,这个方法会被调用多次,用户登陆成功以后,会话保持成功后,怎么调用,传入的sessionKey都是一样的,但是我们把镜头拉到用户登陆的那一次请求中,就会发现一些不同的地方了。
我们可以看到,第一次调用时,sessionKey里面的sessionId是空的,按照我们的逻辑,我们会调用第二个方法,取得header中的token,然后返回sessionId为token。
断点继续,第二次调用的时候,也会传入一个sessionKey,但是这个sessionKey里面的sessionId值却已经有了,它是一个uuid,但是sessionKey里面的sessionId,与第一次返回的sessionId不一致,或者说和我们的token不一致,这是为什么呢?
因为当得到sessionId时,session管理器会尝试到sessionStore中通过这个sessionKey去获取一个session,但是可以肯定的是,这个session肯定是得不到的,因为还没有代码给它创建,所以当检测到获取到的session为null的时候,会调用sessionStore的createSession方法,这个时候,它会生成一个随机的sessionId,然后根据这个新生成的sessionId,创建一个session,然后会把这个sessionId设置到sessionKey里面,替换掉之前的sessionId,所以我们在这个方法后面的几次调用就就会发现第一次不一样,sessionId也和第一次返回的sessionId不一样,因为它创建session的时候生成了一个新的sessionId,这个时候我们要怎么办呢?
我们就修改客户端的token,让它与最新生成的sessionId一致就行了,所以之前说的,这里面有一个把token设置到request中的代码,就是在返回给客户端的时候,通知给客户端最新的token,而不是继续沿用之前的token,因为这个token在sessionStore中是没法取出一个session的。
还有一个要注意的地方,我们从request取出新的token返回给客户端的时候,要在认证完成之后,因为只有当认证完成之后,才会创建session,才会得到最新的token并返回给客户端,不然返回的是老的token。
代码如下:
@RequestMapping("/")
public void login(@RequestParam("code")String code, HttpServletRequest request){
Map<String,Object> data = new HashMap<>();
if(SecurityUtils.getSubject().isAuthenticated()){
//这里代码着已经登陆成功,所以自然不用再次认证,直接从rquest中取出就行了,
data.put(StatelessSessionManager.HEADER_TOKEN_NAME,getServerToken());
data.put(BIND,ShiroKit.getUser().getTel() != null);
response(data);
}
LOG.info("授权码为:" + code);
AuthorizationService authorizationService = authorizationFactory.getAuthorizationService(Constant.clientType);
UserDetail authorization = authorizationService.authorization(code);
Oauth2UserDetail userDetail = (Oauth2UserDetail) authorization;
loginService.login(userDetail);
User user = userService.saveUser(userDetail,Constant.clientType.toString());
ShiroKit.getSession().setAttribute(ShiroKit.USER_DETAIL_KEY,userDetail);
ShiroKit.getSession().setAttribute(ShiroKit.USER_KEY,user);
data.put(BIND,user.getTel() != null);
//这里的代码,必须放到login之执行,因为login后,才会创建session,才会得到最新的token咯
data.put(StatelessSessionManager.HEADER_TOKEN_NAME,getServerToken());
response(data);
}
我们把token返回给客户端,然后客户端每次请求时,带上这个token,我们就维持这个会话了
3.2:第二个方法
方法签名如下
protected Serializable getSessionId(ServletRequest servletRequest, ServletResponse servletResponse)
第二个方法相对简单,因为仅仅是获取token而已,可以从header获取,参数中获取,cookie中获取,当然用户第一次请求的时候,肯定是没有token的,只有登陆成功后才会得到token,所以当token为null的时候,我们生成了一个uuid,但是这个uuid并不会成为后面的token,这个在上面有讲到,因为会被后面生成session时生成的sessionId给替换掉。
而至少那一堆设置数据到request中的代码,我也没去看具体做什么用的,因为它的父类中,执行这个方法的时候,有这些代码的设置,复制过来,怕出什么问题。
四:完整配置代码
完整的配置代码如下:
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap;
import java.util.Map; /**
* @author zxj<br>
* 时间 2017/11/8 15:40
* 说明 ...
*/
@Configuration
public class ShiroConfiguration { @Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
} /**
* 此处注入一个realm
* @param realm
* @return
*/
@Bean
public SecurityManager securityManager(Realm realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setSessionManager(new StatelessSessionManager());
securityManager.setRealm(realm);
return securityManager;
} @Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager); Map<String,String> map = new LinkedHashMap<>();
map.put("/public/**","anon");
map.put("/login/**","anon");
map.put("/**","user");
bean.setFilterChainDefinitionMap(map); return bean;
}
}
其实完整的配置代码都已经不重要了,重要的就是sessionManager,上面红色部分说明了怎么把我们自己写的sessionManager设置到securityManager中。
shiro实现无状态的会话,带源码分析的更多相关文章
- 观察者模式—jdk自带源码分析
一:观察者模式简介 二:jdk实现观察者模式的源码 三:实际例子 四:观察者模式的优点和不足 五:总结 一:观察者模式简介 有时又被称为发布(publish )-订阅(Subscribe)模式.模型- ...
- Java - "JUC线程池" 线程状态与拒绝策略源码分析
Java多线程系列--“JUC线程池”04之 线程池原理(三) 本章介绍线程池的生命周期.在"Java多线程系列--“基础篇”01之 基本概念"中,我们介绍过,线程有5种状态:新建 ...
- Java并发基础:了解无锁CAS就从源码分析
https://segmentfault.com/a/1190000015881923
- Linux进程调度与源码分析(二)——进程生命周期与task_struct进程结构体
1.进程生命周期 Linux操作系统属于多任务操作系统,系统中的每个进程能够分时复用CPU时间片,通过有效的进程调度策略实现多任务并行执行.而进程在被CPU调度运行,等待CPU资源分配以及等待外部事件 ...
- jquery中的$.ajax()的源码分析
针对获取到location.href的兼容代码: try { ajaxLocation = location.href; } catch( e ) { // Use the href attribut ...
- YII框架源码分析(百度PHP大牛创作-原版-无广告无水印)
YII 框架源码分析 百度联盟事业部——黄银锋 目 录 1. 引言 3 1.1.Yii 简介 3 1.2.本文内容与结构 3 2.组件化与模块化 4 2.1.框架加载和运行流程 4 ...
- JDK 自带的观察者模式源码分析以及和自定义实现的取舍
前言 总的结论就是:不推荐使用JDK自带的观察者API,而是自定义实现,但是可以借鉴其好的思想. java.util.Observer 接口源码分析 该接口十分简单,是各个观察者需要实现的接口 pac ...
- Plupload上传实例《模仿微云上传实例》,带源码
Plupload上传实例<模仿微云上传实例>,带源码,作者:鱼塘总裁 如有疑问,加群交流:646104701 一.实例截图 1.上传过程 2.上传成功 3.上传失败 4.最小化 二.所需文 ...
- jQuery 2.0.3 源码分析 Deferred(最细的实现剖析,带图)
Deferred的概念请看第一篇 http://www.cnblogs.com/aaronjs/p/3348569.html ******************构建Deferred对象时候的流程图* ...
随机推荐
- Css Html 大风车
<div style = "height:500px;width:500px;position:static;float:left;"><div class=&q ...
- Java并发(一、概述)
离上次写博客又隔了很久,心中有愧.在我不断使用Java的过程中,几乎都是拿来就用,就Java并发这块我还没有系统的梳理过,趁着国庆有空余时间,把它梳理一遍.以下部分内容参考相关书籍,以作学习之用,特此 ...
- VLAN的三种类型及三种属性
- HDU1085 多重背包
Holding Bin-Laden Captive! Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Ja ...
- 一步一个坑 - WinDbg调试.NET程序
引言 第一次用WinDbg来排查问题,花了很多时间踩坑,记录一下希望对后面的同学有些帮助. 客户现场软件出现偶发性的界面卡死现象一直找不出原因,就想着让客户用任务管理器生成了一个dump文件发给我,我 ...
- Javascript/Jquery操作数组,增删改查以及动态创建HTML元素
<html> <head> <title> New Document </title> <script src="~/Scripts/j ...
- 第九章 MySQL中LIMIT和NOT IN案例
第九章 MySQL中LIMIT和NOT IN案例 一.案例的项目 1.创建数据库语句: #创建数据库 CREATE DATABASE `schoolDB`; USE `schoolDB`; #创建学生 ...
- DevOps之服务器
唠叨话 关于德语噢屁事的知识点,仅提供专业性的精华汇总,具体知识点细节,参考教程网址,如需帮助,请留言. <服务器(Server)> DevOps之服务器划分为三部分:系统.虚拟化.器件. ...
- ABAP POH和POV事件中 获得屏幕字段的值
在Screen显示之前,系统会自动将程序变量值放到屏幕字段中:在PAI事件中,系统会自动将屏幕字段的值更新到相应的程序变量. 在Screen Logic中我们还有POH和POV事件,所以有时需要调用函 ...
- eclipse环境下,java操作MySQL的简单演示
首先先通过power shell 进入MySQL 查看现在数据库的状态(博主是win10系统) 右键开始,选择Windows powershell ,输入MySQL -u用户名 -p密码 选择数据库( ...