转载请在页首明显处注明作者与出处

朱小杰        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实现无状态的会话,带源码分析的更多相关文章

  1. 观察者模式—jdk自带源码分析

    一:观察者模式简介 二:jdk实现观察者模式的源码 三:实际例子 四:观察者模式的优点和不足 五:总结 一:观察者模式简介 有时又被称为发布(publish )-订阅(Subscribe)模式.模型- ...

  2. Java - "JUC线程池" 线程状态与拒绝策略源码分析

    Java多线程系列--“JUC线程池”04之 线程池原理(三) 本章介绍线程池的生命周期.在"Java多线程系列--“基础篇”01之 基本概念"中,我们介绍过,线程有5种状态:新建 ...

  3. Java并发基础:了解无锁CAS就从源码分析

    https://segmentfault.com/a/1190000015881923

  4. Linux进程调度与源码分析(二)——进程生命周期与task_struct进程结构体

    1.进程生命周期 Linux操作系统属于多任务操作系统,系统中的每个进程能够分时复用CPU时间片,通过有效的进程调度策略实现多任务并行执行.而进程在被CPU调度运行,等待CPU资源分配以及等待外部事件 ...

  5. jquery中的$.ajax()的源码分析

    针对获取到location.href的兼容代码: try { ajaxLocation = location.href; } catch( e ) { // Use the href attribut ...

  6. YII框架源码分析(百度PHP大牛创作-原版-无广告无水印)

           YII 框架源码分析    百度联盟事业部——黄银锋 目 录 1. 引言 3 1.1.Yii 简介 3 1.2.本文内容与结构 3 2.组件化与模块化 4 2.1.框架加载和运行流程 4 ...

  7. JDK 自带的观察者模式源码分析以及和自定义实现的取舍

    前言 总的结论就是:不推荐使用JDK自带的观察者API,而是自定义实现,但是可以借鉴其好的思想. java.util.Observer 接口源码分析 该接口十分简单,是各个观察者需要实现的接口 pac ...

  8. Plupload上传实例《模仿微云上传实例》,带源码

    Plupload上传实例<模仿微云上传实例>,带源码,作者:鱼塘总裁 如有疑问,加群交流:646104701 一.实例截图 1.上传过程 2.上传成功 3.上传失败 4.最小化 二.所需文 ...

  9. jQuery 2.0.3 源码分析 Deferred(最细的实现剖析,带图)

    Deferred的概念请看第一篇 http://www.cnblogs.com/aaronjs/p/3348569.html ******************构建Deferred对象时候的流程图* ...

随机推荐

  1. Css Html 大风车

    <div style = "height:500px;width:500px;position:static;float:left;"><div class=&q ...

  2. Java并发(一、概述)

    离上次写博客又隔了很久,心中有愧.在我不断使用Java的过程中,几乎都是拿来就用,就Java并发这块我还没有系统的梳理过,趁着国庆有空余时间,把它梳理一遍.以下部分内容参考相关书籍,以作学习之用,特此 ...

  3. VLAN的三种类型及三种属性

  4. HDU1085 多重背包

    Holding Bin-Laden Captive! Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Ja ...

  5. 一步一个坑 - WinDbg调试.NET程序

    引言 第一次用WinDbg来排查问题,花了很多时间踩坑,记录一下希望对后面的同学有些帮助. 客户现场软件出现偶发性的界面卡死现象一直找不出原因,就想着让客户用任务管理器生成了一个dump文件发给我,我 ...

  6. Javascript/Jquery操作数组,增删改查以及动态创建HTML元素

    <html> <head> <title> New Document </title> <script src="~/Scripts/j ...

  7. 第九章 MySQL中LIMIT和NOT IN案例

    第九章 MySQL中LIMIT和NOT IN案例 一.案例的项目 1.创建数据库语句: #创建数据库 CREATE DATABASE `schoolDB`; USE `schoolDB`; #创建学生 ...

  8. DevOps之服务器

    唠叨话 关于德语噢屁事的知识点,仅提供专业性的精华汇总,具体知识点细节,参考教程网址,如需帮助,请留言. <服务器(Server)> DevOps之服务器划分为三部分:系统.虚拟化.器件. ...

  9. ABAP POH和POV事件中 获得屏幕字段的值

    在Screen显示之前,系统会自动将程序变量值放到屏幕字段中:在PAI事件中,系统会自动将屏幕字段的值更新到相应的程序变量. 在Screen Logic中我们还有POH和POV事件,所以有时需要调用函 ...

  10. eclipse环境下,java操作MySQL的简单演示

    首先先通过power shell 进入MySQL 查看现在数据库的状态(博主是win10系统) 右键开始,选择Windows powershell ,输入MySQL -u用户名 -p密码 选择数据库( ...