360SDN.COM

首页/Java/列表

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

来源:开源中国  2017-09-11 12:14:02    评论:0点击:

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。

推荐阅读

33 款主宰 2017 iOS 开发的开源库

Nginx+Keepalived(双机热备)搭建高可用负载均衡环境(HA)

十大 Node.js 端到端测试框架,快速提升工作效率

从“某公司专利”事件,谈谈开源协议与知识产权

“放码过来”邀您亮“项”,一不小心就火了!

点击“阅读原文”查看更多精彩内容

阅读原文

为您推荐

友情链接 |九搜汽车网 |手机ok生活信息网|ok生活信息网|ok微生活
 Powered by www.360SDN.COM   京ICP备11022651号-4 © 2012-2016 版权