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

朱小杰        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,如下:

    @Beanpublic 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);@Overridepublic 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;}@Overrideprotected 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 {@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){return new LifecycleBeanPostProcessor();}/*** 此处注入一个realm* @param realm* @return*/@Beanpublic SecurityManager securityManager(Realm realm){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setSessionManager(new StatelessSessionManager());securityManager.setRealm(realm);return securityManager;}@Beanpublic 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中。

转载于:https://www.cnblogs.com/zhuxiaojie/p/7809767.html

shiro实现无状态的会话,带源码分析相关推荐

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

    CAS的全称为Compare And Swap,直译就是比较交换.是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在i ...

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

    CAS的全称为Compare And Swap,直译就是比较交换.是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,其实现方式是基于硬件平台的汇编指令,在i ...

  3. 系统性详解Redis操作Hash类型数据(带源码分析及测试结果)

    1 缘起 系统讲解Redis的Hash类型CURD, 帮助学习者系统且准确学习Hash数据操作, 逐步养成测试的好习惯, 本文较长,Hash的操作比较多,请耐心看, 既可以集中时间看,亦可以碎片时间学 ...

  4. 02、Log4j(第三方日志框架,带源码分析)

    文章目录 前言 一.认识Log4j 1.1.介绍Log4j 1.2.第三方jar包 1.3.日志等级(6个) 二.Log4j的三大组件 Loggers Appenders Layouts 三.入门Lo ...

  5. scroller类的用法完全解析以及带源码分析

    上一篇:scrollTo与scrollBy用法以及TouchSlop与VelocityTracker解析 通过上一篇内容对scrollTo与scrollBy用法以及TouchSlop与Velocity ...

  6. Netty学习笔记 - 1 (带源码分析部分)

    2021年12月 北京 xxd 一.Netty是什么 Netty 是由 JBOSS 提供的一个 Java 开源框架,现为 Github 上的独立项目. Netty 是一个异步的.基于事件驱动的网络应用 ...

  7. Python2 Python3 爬取赶集网租房信息,带源码分析

    *之前偶然看了某个腾讯公开课的视频,写的爬取赶集网的租房信息,这几天突然想起来,于是自己分析了一下赶集网的信息,然后自己写了一遍,写完又用用Python3重写了一遍.之中也遇见了少许的坑.记一下.算是 ...

  8. idea 线程内存_Java线程池系列之-Java线程池底层源码分析系列(一)

    课程简介: 课程目标:通过本课程学习,深入理解Java线程池,提升自身技术能力与价值. 适用人群:具有Java多线程基础的人群,希望深入理解线程池底层原理的人群. 课程概述:多线程的异步执行方式,虽然 ...

  9. apollo源码分析 感知_Kitty中的动态线程池支持Nacos,Apollo多配置中心了

    目录 回顾昨日 nacos 集成 Spring Cloud Alibaba 方式 Nacos Spring Boot 方式 Apollo 集成 自研配置中心对接 无配置中心对接 实现源码分析 兼容 A ...

最新文章

  1. 03基于python玩转人工智能最火框架之TensorFlow介绍
  2. [转]JDK里的设计模式
  3. 初探云原生应用管理之:聊聊 Tekton 项目
  4. 简约而不简单|值得收藏的Numpy小抄表(含主要语法、代码)
  5. 1093芯片做正弦波逆变器_长途自驾游“缺电”如何选购正确车载逆变器,避开商家套路...
  6. 【Shell】sed实例之第三部分
  7. 企业网络营销意识的重要性
  8. 非满管电磁流量计测量平均流速
  9. tcl网络电视android无法启动,tcl智能电视系统升级和刷机常见问题解答
  10. scanf提取gprmc数据
  11. OpenCV4一部分函数目录
  12. window自带的常见工具
  13. BurpSuite爆破(Intruder)模块四种模式介绍
  14. MATLAB 面向对象编程(十二)抽象类
  15. 51信用卡2018校园招聘编程题学习
  16. PLC(Power Line Carrier)电力线载波介绍
  17. 大数据产业中的新方式:数据交易
  18. 快速学习-Saturn Console部署
  19. 《图解HTTP》思维导图
  20. git的基本使用命令

热门文章

  1. 二阶魔方入门玩法教程
  2. java增大字體_往JRE里增加字体
  3. 2020用户行为分析领域最具商业合作价值企业盘点
  4. 吴军《数学之美》-读书笔记
  5. linux rm 回收站,给rm设置回收站
  6. Zabbix服务器内网监控外网Agent主机问题
  7. 国内外常见DNS汇总 (更新:201904)
  8. python解二元一次方程组
  9. Android环境搭建
  10. Godot着色器基础