文档章节

springmvc集成shiro后,session、request姓汪还是姓蒋?

细肉云吞
 细肉云吞
发布于 2017/06/18 00:18
字数 2401
阅读 5260
收藏 347
点赞 24
评论 22

1. 疑问

我们在项目中使用了spring mvc作为MVC框架,shiro作为权限控制框架,在使用过程中慢慢地产生了下面几个疑惑,本篇文章将会带着疑问慢慢地解析shiro源码,从而解开心里面的那点小纠纠。

(1)在spring controller中,request有何不同呢

于是,在controller中打印了request的类对象,发现request对象是org.apache.shiro.web.servlet.ShiroHttpServletRequest ,很明显,此时的 request 已经被shiro包装过了。

(2)众所周知,spring mvc整合shiro后,可以通过两种方式获取到session:

通过Spring mvc中controller的request获取session

 Session session = request.getSession();

通过shiro获取session

 Subject currentUser = SecurityUtils.getSubject();
 Session session = currentUser.getSession();

那么,问题来了,两种方式获取的session是否相同呢

这里需要看一下项目中的shiro的securityManager配置,因为配置影响了shiro session的来源。这里没有配置session管理器。

    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    
        <!--<property name="sessionManager" ref="sessionManager"/>-->

        <property name="realm" ref="shiroRealm"/>
    </bean>

在controller中再次打印了session,发现前者的session类型是 org.apache.catalina.session.StandardSessionFacade ,后者的session类型是org.apache.shiro.subject.support.DelegatingSubject$StoppingAwareProxiedSession。

很明显,前者的session是属于httpServletRequest中的HttpSession,那么后者呢?仔细看StoppingAwareProxiedSession,它是属于shiro自定义的session的子类。通过这个代理对象的源码,我们发现所有与session相关的方法都是通过它内部委托类delegate进行的,通过断点,可以看到delegate的类型其实也是 org.apache.catalina.session.StandardSessionFacade ,也就是说,两者在操作session时,都是用同一个类型的session。那么它什么时候包装了httpSession呢?

输入图片说明

2. 一起一层一层剥开它的芯

2.1 怎么获取过滤器filter

spring mvc 整合shiro,需要在web.xml中配置该filter

    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <!-- 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean-->
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>shiroFilter</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>

DelegatingFilterProxy 是一个过滤器,准确来说是目的过滤器的代理,由它在doFilter方法中,获取spring 容器中的过滤器,并调用目标过滤器的doFilter方法,这样的好处是,原来过滤器的配置放在web.xml中,现在可以把filter的配置放在spring中,并由spring管理它的生命周期。另外,DelegatingFilterProxy中的targetBeanName指定需要从spring容器中获取的过滤器的名字,如果没有,它会以filterName过滤器名从spring容器中获取。

输入图片说明

2.2 request的来源

前面说 DelegatingFilterProxy 会从spring容器中获取名为 targetBeanName 的过滤器。接下来看下spring配置文件,在这里定义了一个shiro Filter的工厂 org.apache.shiro.spring.web.ShiroFilterFactoryBean。

 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login"/>
        <property name="successUrl" value="/main"/>
        <property name="unauthorizedUrl" value="/unauthorized"/>
        <property name="filters">
            <map>
                <!--表单认证器-->
                <entry key="authc" value-ref="formAuthenticationFilter"/>
            </map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                <!-- 请求 logout地址,shiro去清除session-->
                /logout = logout
                /static/** = anon
                /** = authc
            </value>
        </property>
    </bean>

熟悉spring 的应该知道,bean的工厂是用来生产相关的bean,并把bean注册到spring容器中的。通过查看工厂bean的getObject方法,可知,委托类调用的filter类型是SpringShiroFilter。接下来我们看一下类图,了解一下它们之间的关系。

输入图片说明

既然SpringShiroFilter属于过滤器,那么它肯定有一个doFilter方法,doFilter由它的父类 OncePerRequestFilter 实现。OncePerRequestFilter 在doFilter方法中,判断是否在request中有"already filtered"这个属性设置为true,如果有,则交给下一个过滤器,如果没有就执行 doFilterInternal( ) 抽象方法。

doFilterInternal由AbstractShiroFilter类实现,即SpringShiroFilter的直属父类实现。doFilterInternal 一些关键流程如下:

protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
            throws ServletException, IOException {

            //包装request/response
            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

            //创建subject,其实创建的是Subject的代理类DelegatingSubject
            final Subject subject = createSubject(request, response);
            
             // 继续执行过滤器链,此时的request/response是前面包装好的request/response
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });
    }

在doFilterInternal中,可以看到对ServletRequest和ServletReponse进行了包装。除此之外,还把包装后的request/response作为参数,创建Subject,这个subject其实是代理类DelegatingSubject。

那么,这个包装后的request是什么呢?我们继续解析prepareServletRequest。

    protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) {
        ServletRequest toUse = request;
        if (request instanceof HttpServletRequest) {
            HttpServletRequest http = (HttpServletRequest) request;
            toUse = wrapServletRequest(http);  //真正去包装request的方法
        }
        return toUse;
    }

继续包装request,看下wrapServletRequest方法。无比兴奋啊,文章前面的ShiroHttpServletRequest终于出来了,我们在controller中获取到的request就是它,是它,它。它是servlet的HttpServletRequestWrapper的子类。

    protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
        //看看看,ShiroHttpServletRequest
        return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());  
    }

ShiroHttpServletRequest构造方法的第三个参数是个关键参数,我们先不管它怎么来的,进ShiroHttpServletRequest里面看看它有什么用。它主要在两个地方用到,一个是getRequestedSessionId(),这个是获取sessionid的方法;另一个是getSession(),它是获取session会话对象的。

先来看一下getRequestedSessionId()。isHttpSessions决定sessionid是否来自servlet。

public String getRequestedSessionId() {
        String requestedSessionId = null;
        if (isHttpSessions()) {
            requestedSessionId = super.getRequestedSessionId();   //从servlet中获取sessionid
        } else {
            Object sessionId = getAttribute(REFERENCED_SESSION_ID);   //从request中获取REFERENCED_SESSION_ID这个属性
            if (sessionId != null) {
                requestedSessionId = sessionId.toString();
            }
        }

        return requestedSessionId;
    }

再看一下getSession()。isHttpSessions决定了session是否来自servlet。

    public HttpSession getSession(boolean create) {

        HttpSession httpSession;

        if (isHttpSessions()) {
            httpSession = super.getSession(false);  //从servletRequest获取session
            if (httpSession == null && create) {
                if (WebUtils._isSessionCreationEnabled(this)) {
                    httpSession = super.getSession(create);  //从servletRequest获取session
                } else {
                    throw newNoSessionCreationException();
                }
            }
        } else {
            if (this.session == null) {

                boolean existing = getSubject().getSession(false) != null; //从subject中获取session

                Session shiroSession = getSubject().getSession(create); //从subject中获取session
                if (shiroSession != null) {
                    this.session = new ShiroHttpSession(shiroSession, this, this.servletContext);
                    if (!existing) {
                        setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
                    }
                }
            }
            httpSession = this.session;
        }

        return httpSession;
    }

既然isHttpSessions()那么重要,我们还是要看一下在什么情况下,它返回true。

   protected boolean isHttpSessions() {
        return getSecurityManager().isHttpSessionMode();
    }

isHttpSessions是否返回true是由使用的shiro安全管理器的 isHttpSessionMode() 决定的。回到前面,我们使用的安全管理器是 DefaultWebSecurityManager ,我们看一下 DefaultWebSecurityManager 的源码,找到 isHttpSessionMode 方法。可以看到,SessionManager 的类型和 isServletContainerSessions() 起到了决定性的作用。

    public boolean isHttpSessionMode() {
        SessionManager sessionManager = getSessionManager();
        return sessionManager instanceof WebSessionManager && ((WebSessionManager)sessionManager).isServletContainerSessions();
    }

在配置文件中,我们并没有配置SessionManager ,安全管理器会使用默认的会话管理器 ServletContainerSessionManager,在 ServletContainerSessionManager 中,isServletContainerSessions 返回 true 。

因此,在前面的shiro配置的情况下,request中获取的session将会是servlet context下的session。

2.3 subject的session来源

前面 doFilterInternal 的分析中,还落下了subject的创建过程,接下来我们解析该过程,从而揭开通过subject获取session,这个session是从哪来的。

回忆下,在controller中怎么通过subject获取session。

 Subject currentUser = SecurityUtils.getSubject();
 Session session = currentUser.getSession(); // session的类型?

我们看一下shiro定义的session类图,它们具有一些与 HttpSession 相同的方法,例如 setAttribute 和 getAttribute。

输入图片说明

还记得在 doFilterInternal 中,shiro把包装后的request/response作为参数,创建subject吗

final Subject subject = createSubject(request, response);

subject的创建时序图

输入图片说明

最终,由 DefaultWebSubjectFactory 创建subject,并把 principals, session, request, response, securityManager这些参数封装到subject。由于第一次创建session,此时session没有实例。

那么,当我们调用 subject .getSession() 尝试获取会话session时,发生了什么呢。从前面的代码可以知道,我们获取到的subject是 WebDelegatingSubject 类型的,它的父类 DelegatingSubject 实现了getSession 方法,下面的代码是getSession方法中的关键步骤。

 public Session getSession(boolean create) {
        if (this.session == null && create) {
            // 创建session上下文,上下文里面封装有request/response/host
            SessionContext sessionContext = createSessionContext();
            // 根据上下文,由securityManager创建session
            Session session = this.securityManager.start(sessionContext);
            // 包装session
            this.session = decorate(session);
        }
        return this.session;
    }

接下来解析一下,安全管理器根据会话上下文创建session这个流程,追踪代码后,可以知道它其实是交由 sessionManager 会话管理器进行会话创建,由前面的代码可以知道,这里的sessionManager 其实是 ServletContainerSessionManager类,找到它的 createSession 方法。

 protected Session createSession(SessionContext sessionContext) throws AuthorizationException {
        HttpServletRequest request = WebUtils.getHttpRequest(sessionContext);

        // 从request中获取HttpSession
        HttpSession httpSession = request.getSession();

        String host = getHost(sessionContext);

        // 包装成 HttpServletSession 
        return createSession(httpSession, host);
    }

这里就可以知道,其实session是来源于 request 的 HttpSession,也就是说,来源于上一个过滤器中request的HttpSession。HttpSession 以成员变量的形式存在 HttpServletSession 中。回忆前面从安全管理器获取 HttpServletSession 后,还调用 decorate() 装饰该session,装饰后的session类型是 StoppingAwareProxiedSession,HttpServletSession 是它的成员 。

在文章一开始的时候,通过debug就已经知道,当我们通过 subject.getSession() 获取的就是 StoppingAwareProxiedSession,可见,这与前面分析的是一致的 。

那么,当我们通过session.getAttribute和session.addAttribute时,StoppingAwareProxiedSession 做了什么?它是由父类 ProxiedSession 实现 session.getAttribute和session.addAttribute 方法。我们看一下 ProxiedSession 相关源码。

  public Object getAttribute(Object key) throws InvalidSessionException {
        return delegate.getAttribute(key);
    }
    public void setAttribute(Object key, Object value) throws InvalidSessionException {
        delegate.setAttribute(key, value);
    }

可见,getAttribute 和 addAttribute 由委托类delegate完成,这里的delegate就是HttpServletSession 。接下来看 HttpServletSession 的相关方法。

    public Object getAttribute(Object key) throws InvalidSessionException {
        try {
            return httpSession.getAttribute(assertString(key));
        } catch (Exception e) {
            throw new InvalidSessionException(e);
        }
    }

    public void setAttribute(Object key, Object value) throws InvalidSessionException {
        try {
            httpSession.setAttribute(assertString(key), value);
        } catch (Exception e) {
            throw new InvalidSessionException(e);
        }
    }

此处的httpSession就是之前从HttpServletRequest获取的,也就是说,通过request.getSeesion()与subject.getSeesion()获取session后,对session的操作是相同的。

结论

(1)controller中的request,在shiro过滤器中的doFilterInternal方法,将被包装为ShiroHttpServletRequest 。

(2)在controller中,通过 request.getSession(_) 获取会话 session ,该session到底来源servletRequest 还是由shiro管理并管理创建的会话,主要由 安全管理器 SecurityManager 和 SessionManager 会话管理器决定。

(3)不管是通过 request.getSession或者subject.getSession获取到session,操作session,两者都是等价的。在使用默认session管理器的情况下,操作session都是等价于操作HttpSession。

© 著作权归作者所有

共有 人打赏支持
细肉云吞
粉丝 104
博文 90
码字总数 120137
作品 0
其它
高级程序员
加载中

评论(22)

O
OrangeCo
谢谢分享~正需要..
菠罗
菠罗
正需要这个
来自九江的小矮人
来自九江的小矮人

引用来自“DevidGe”的评论

为什么不用spring security

引用来自“吓两跳丸子”的评论

你剔牙的时候怎么不用避雷针
这个比喻very形象
七仔谁用了
七仔谁用了

引用来自“DevidGe”的评论

为什么不用spring security

引用来自“吓两跳丸子”的评论

你剔牙的时候怎么不用避雷针

引用来自“百世经纶之傲笑红尘”的评论

:joy:
#此处输入软件名#:snowman:
七仔谁用了
七仔谁用了

引用来自“香港otari奥达力颈腰舒展器”的评论

mark
#此处输入软件名#:convenience_store:
leejingyu
leejingyu
這裡需要看一下項目中的shiro的securityManager配置,因為配置影響了shiro session的來源。這裡沒有配置session管理器。

所以如果有配置的话,那就会不一样了是嘛?
谢谢
Ge白
Ge白
学习了,分析的真不错,很到位。赞~ :+1:
办公室扫地菜鸡
办公室扫地菜鸡
mark good
君千殇520
君千殇520

引用来自“DevidGe”的评论

为什么不用spring security

引用来自“吓两跳丸子”的评论

你剔牙的时候怎么不用避雷针
个人感觉比shiro要轻多了
云颖
云颖
mark
springboot + shiro 权限注解、请求乱码解决、统一异常处理

springboot + shiro 权限注解、请求乱码解决、统一异常处理 前篇 后台权限管理系统 相关: spring boot + mybatis + layui + shiro后台权限管理系统 springboot + shiro之登录人数限制、登录...

wyait ⋅ 06/06 ⋅ 0

Spring集成shiro做登陆认证

一、背景   其实很早的时候,就在项目中有使用到shiro做登陆认证,直到今天才又想起来这茬,自己抽空搭了一个spring+springmvc+mybatis和shiro进行集成的种子项目,当然里面还有很简单的测...

hafiz.zhang ⋅ 2017/07/23 ⋅ 0

Apache Shiro权限控制实战,权限控制SpringMVC + Mybatis + Shiro

Demo已经部署到线上,地址是http://shiro.itboy.net, 管理员帐号:admin,密码:sojson.com 如果密码错误,请用sojson。 PS:你可以注册自己的帐号,然后用管理员赋权限给你自己的帐号,但是...

8446666 ⋅ 2016/06/29 ⋅ 3

一步一步教你用shiro——1引入shiro框架

一步一步教你用shiro——1引入shiro框架 一步一步教你用shiro——2配置并自定义realm 一步一步教你用shiro——3配置并自定义sessionManager 一步一步教你用shiro——4配置并自定义sessionDa...

肥肥小浣熊 ⋅ 04/30 ⋅ 0

springboot + shiro之登录人数限制、登录判断重定向、session时间设置

springboot + shiro之登录人数控制 项目 前篇:spring boot + mybatis + layui + shiro后台权限管理系统 本文是基于spring boot + mybatis + layui + shiro后台权限管理系统开发的,新增功能...

wyait ⋅ 04/24 ⋅ 0

Shiro和Spring Security对比

Shiro简介 Apache Shiro是Java的一个安全框架。目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security,可能没有Spring Security做的功能强大,但是在实际工作时可能并不...

有余力则学文 ⋅ 04/27 ⋅ 0

spring-springmvc-shiro整合(转载)

这里用的是SpringMVC-3.2.4和Shiro-1.2.2,示例代码如下 首先是web.xml <?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns......

李永china ⋅ 2016/06/06 ⋅ 0

springmvc返回的页面404 在线等答案

说明:项目是Spring Springmvc Hibernate Shiro整合的测试项目 首先贴出我的配置文件的关键配置 web.xml 中DispatchServlet配置的是拦截全部路径,如下图 springmvcorg.springframework.web...

梁阳波 ⋅ 2014/07/31 ⋅ 3

30分钟学会如何使用Shiro

一、架构 要学习如何使用Shiro必须先从它的架构谈起,作为一款安全框架Shiro的设计相当精妙。Shiro的应用不依赖任何容器,它也可以在JavaSE下使用。但是最常用的环境还是JavaEE。下面以用户登...

qq5923dd411b8fa ⋅ 2017/05/23 ⋅ 0

30分钟学会如何使用Shiro

一、架构 要学习如何使用Shiro必须先从它的架构谈起,作为一款安全框架Shiro的设计相当精妙。Shiro的应用不依赖任何容器,它也可以在JavaSE下使用。但是最常用的环境还是JavaEE。下面以用户登...

qq58edeba279279 ⋅ 2017/05/09 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

那些证书相关的玩意儿(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)

之前没接触过证书加密的话,对证书相关的这些概念真是感觉挺棘手的,因为一下子来了一大堆新名词,看起来像是另一个领域的东西,而不是我们所熟悉的编程领域的那些东西,起码我个人感觉如此,且很长...

颖辉小居 ⋅ 18分钟前 ⋅ 0

利用有限制通配符提升API灵活性(28)

1、参数化类型是不可变的 List<String> 不是List<Object>的子类,但是二者是有联系的 利用有限制的通配符类型处理类似情况 List<? extends Object>(生产者) Collection<? super E>(消费者......

职业搬砖20年 ⋅ 24分钟前 ⋅ 0

ssm框架 +bootstrap分页

这里有两种方式 方式一:自己写分页 方式二:使用插件PageHelper 1.自己写分页 1.1 效果 1.2 实现过程 1.2.1 创建分页公共类 //---------------------------1.属性-------------------------...

Lucky_Me ⋅ 31分钟前 ⋅ 0

Istio

helm template install/kubernetes/helm/istio --name istio --namespace istio-system > $HOME/istio.yaml after $ kubectl create namespace istio-system$ kubectl create -f $HOME/ist......

openthings ⋅ 32分钟前 ⋅ 0

内核线程、轻量级进程、用户线程

线程与进程概念 在现代操作系统中,进程支持多线程。 进程是资源管理的最小单元; 线程是程序执行的最小单元。 即线程作为调度和分配的基本单位,进程作为资源分配的基本单位 一个进程的组成...

117 ⋅ 37分钟前 ⋅ 0

elasticsearch2.4.6升级为elasticsearch-5.5.0的经历

将elasticsearch-5.5.0 中的配置 path.data 指向原来的数据路径 即 path.data: /usr/local/src/elasticsearch-2.4.6/data 注意: elasticsearch-5.5.0 需要将jdk版本升级到1.8...

晨猫 ⋅ 37分钟前 ⋅ 1

lvm讲解 磁盘故障小案例

1

oschina130111 ⋅ 41分钟前 ⋅ 0

那些提升开发人员工作效率的在线工具

本文转载自公众号 Hollis 作为一个Java开发人员,经常要和各种各样的工具打交道,除了我们常用的IDE工具以外,其实还有很多工具是我们在日常开发及学习过程中要经常使用到的。 Hollis偏爱使用...

时刻在奔跑 ⋅ 54分钟前 ⋅ 0

restful风格 实现DELETE PUT请求 的web.xml的配置

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframe......

泉天下 ⋅ 59分钟前 ⋅ 0

Shell数组

Shell数组 Shell在编程方面比Windows批处理强大很多,无论是在循环、运算。 bash支持一维数组(不支持多维数组),并且没有限定数组的大小。类似与C语言,数组元素的下标由0开始编号。获取数...

蜗牛奔跑 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部