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

朱小杰        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. 【JVM命令系列】jstat

    命令基本概述 Jstat是JDK自带的一个轻量级小工具.全称"Java Virtual Machine statistics monitoring tool",它位于java的bi ...

  2. Java的类加载器

    一.类加载器的概念 类加载器(class loader)用来加载 Java 类到 Java 虚拟机中.一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 ...

  3. 我的第一篇blog—— 一起来赛马呀

      作为一名大三的学生现在开始学习acm,或许太晚.感叹时光蹉跎....我的blog将以讲解的形式的发布,以专题的形式的形式介绍一些基本的知识和经典的题目.虽然感觉自己所剩时间无多,但也希望起到前人种 ...

  4. MyBatis 关系映射XML配置

    关系映射 在我看来这些实体类就没啥太大关联关系,不就是一个sql语句解决的问题,直接多表查询就完事,程序将它设置关联就好 xml里面配置也是配置了sql语句,下面给出几个关系的小毛驴(xml) 一对多 ...

  5. 在linux上安装rz、sz包

    在SecureCRT这样的ssh登录软件里, 通过在Linux界面里输入rz/sz命令来上传/下载文件. 对于RHEL5, rz/sz默认没有安装所以需要手工安装.sz: 将选定的文件发送(send) ...

  6. JS双击div编辑文本内容

    HTML代码: <div class="album"> <div class="image"><a href="java ...

  7. Python自学笔记-sorted()函数(来自廖雪峰的官网Python3)

    感觉廖雪峰的官网http://www.liaoxuefeng.com/里面的教程不错,所以学习一下,把需要复习的摘抄一下. 以下内容主要为了自己复习用,详细内容请登录廖雪峰的官网查看. 排序算法 排序 ...

  8. Access-Control-Allow-Origin与Ajax跨域

    问题 在某域名下使用Ajax向另一个域名下的页面请求数据,会遇到跨域问题.另一个域名必须在response中添加 Access-Control-Allow-Origin 的header,才能让前者成功 ...

  9. 大数据算法设计模式(2) - 左外链接(leftOuterJoin) spark实现

    左外链接(leftOuterJoin) spark实现 package com.kangaroo.studio.algorithms.join; import org.apache.spark.api ...

  10. 学习PID

    最近在想自己的文章有些是不是写的太难以理解了呢.........竟然好多人看了还是会直接问我很多问题....... 其实PID哈靠自己想像就能自己写出来自己的代码,也许是网上的讲的太过的高深什么积分微 ...