shiro中session实现的简单分析

前阵子对shiro进行分布式环境下的改造时跟了一遍源码,当时只是使用了思维带图简要的记录了一下方法的调用过程。最近有空了决定用博客详细的记录分析一下这个流程,以帮助自己更好的理解。

配置

首先看看shiro在web.xml文件中的配置

 <!-- shiro过滤器 -->
 <filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
 </filter>

 <filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
 </filter-mapping>

可以看到使用的<filter-class>标签是Spring的代理过滤器,那么它是如何代理shiro的过滤器的呢?看看DelegatingFilterProxy的源码

    @Override
    protected void initFilterBean() throws ServletException {
        synchronized (this.delegateMonitor) {
            if (this.delegate == null) {
                // If no target bean name specified, use filter name.
                //如果没有delegate则根据<filter-name>去Spring容器中寻找对应的bean
                if (this.targetBeanName == null) {
                    this.targetBeanName = getFilterName();
                }
                // Fetch Spring root application context and initialize the delegate early,
                // if possible. If the root application context will be started after this
                // filter proxy, we'll have to resort to lazy initialization.
                WebApplicationContext wac = findWebApplicationContext();
                if (wac != null) {
                    this.delegate = initDelegate(wac);
                }
            }
        }
    }

于是Spring中应该配置了name为shiroFilter的bean,下面看看Spring中与shiro相关的配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
     http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
     http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

     <!-- shiroFilter对象 -->
     <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/user/loginpage"/>
        <property name="unauthorizedUrl" value="/403.html"/>
        <property name="filterChainDefinitions">
             <value>
                 /user/loginpage = anon
                 /user/login = anon
                 /* = authc
                 /user/perms1 = perms["user:delete"]
                 /user/perms2 = perms["user:select"]
                 /user/admin = roles["admin"]
                 #自定义的过滤器,只要多个权限中有一个满足即可
                 /user/users = rolesOr["admin","user"]
             </value>
         </property>

         <property name="filters">
             <map>
               <entry key="rolesOr" value-ref="rolesOrFilter" />
             </map>
         </property>
     </bean>

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

     <!-- 创建realm -->
     <bean id="realm" class="com.cfh.studyshiro.common.CustomeRealm">
     </bean>

     <!-- 自定义过滤器 -->
     <bean id="rolesOrFilter" class="com.cfh.studyshiro.filter.RolesOrFilter" />

     <!-- 注入自定义的sessionDao -->
     <bean id="redisSessionDao" class="com.cfh.studyshiro.common.RedisSessionDao" />

     <!-- 在sessionManager中引入自定义的sessionDao -->
     <bean id="sessionManager" class="com.cfh.studyshiro.common.CustomSessionManager">
        <property name="sessionDAO" ref="redisSessionDao" />
        <!-- 关闭cookie -->
        <!-- <property name="sessionIdCookieEnabled" value="false" /> -->
     </bean>
</beans>

ShiroFilterFactoryBean的源码这里不进行讨论,先看看ShiroFilterFactoryBean.class中生成ShiroFilter的createInstance()方法

protected AbstractShiroFilter createInstance() throws Exception {

        log.debug("Creating Shiro Filter instance.");

        SecurityManager securityManager = getSecurityManager();
        if (securityManager == null) {
            String msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        }

        if (!(securityManager instanceof WebSecurityManager)) {
            String msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        }

        FilterChainManager manager = createFilterChainManager();

        //Expose the constructed FilterChainManager by first wrapping it in a
        // FilterChainResolver implementation. The AbstractShiroFilter implementations
        // do not know about FilterChainManagers - only resolvers:
        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);

        //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built
        //FilterChainResolver.  It doesn't matter that the instance is an anonymous inner class
        //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts
        //injection of the SecurityManager and FilterChainResolver:
        return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
    }

spring的DelegatingFilterProxy由此获得了对AbstractShiroFilter的代理。下面我们在DelegatingFilterProxy的doFilter方法上打上断点,跟踪shiro在一次登录请求中都会做哪些处理。


断点调试

1.入口:invokeDelegate(delegateToUse, request, response, filterChain)
// Let the delegate perform the actual doFilter operation.

2.接着执行OncePerRequestFilter的doFilter方法

    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
        if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
            log.trace("Filter '{}' already executed.  Proceeding without invoking this filter.", getName());
            filterChain.doFilter(request, response);
        } else //noinspection deprecation
            if (/* added in 1.2: */ !isEnabled(request, response) ||
                /* retain backwards compatibility: */ shouldNotFilter(request) ) {
            log.debug("Filter '{}' is not enabled for the current request.  Proceeding without invoking this filter.",
                    getName());
            filterChain.doFilter(request, response);
        } else {
            // Do invoke this filter...
            log.trace("Filter '{}' not yet executed.  Executing now.", getName());
            request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

            try {
                doFilterInternal(request, response, filterChain);
            } finally {
                // Once the request has finished, we're done and we don't
                // need to mark as 'already filtered' any more.
                request.removeAttribute(alreadyFilteredAttributeName);
            }
        }
    }

可以看到这个方法会先判断请求是否是过滤的了,在最后一个分支调用了doFilterInternal(request, response, filterChain);这个方法,我们跟进方法中看看。

3.断点跳进了AbstractShiroFilter中,观察这个类,发现他继承了OncePerRequestFilter 并重写了其中的doFilterInternal

public abstract class AbstractShiroFilter extends OncePerRequestFilter 

接下来看看doFilterInternal中的逻辑

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

        Throwable t = null;

        try {
            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);


            final Subject subject = createSubject(request, response);

            //noinspection unchecked
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });
        } catch (ExecutionException ex) {
            t = ex.getCause();
        } catch (Throwable throwable) {
            t = throwable;
        }

        if (t != null) {
            if (t instanceof ServletException) {
                throw (ServletException) t;
            }
            if (t instanceof IOException) {
                throw (IOException) t;
            }
            //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
            String msg = "Filtered request failed.";
            throw new ServletException(msg, t);
        }
    }

主要的逻辑就是创建一个subject,并在创建完成后异步执行一个callable任务用于更新 updateSessionLastAccessTime。接下里看看subject的创建过程。

4.createSubject()

    protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
        return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
    }

使用建造者模式建造了一个WebSubject对象,继续跟进

Builder的构造方法

 public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) {
            super(securityManager);
            if (request == null) {
                throw new IllegalArgumentException("ServletRequest argument cannot be null.");
            }
            if (response == null) {
                throw new IllegalArgumentException("ServletResponse argument cannot be null.");
            }
            setRequest(request);
            setResponse(response);
        }

build方法

 public WebSubject buildWebSubject() {
            //调用父类的buildSubject()
            Subject subject = super.buildSubject();
            if (!(subject instanceof WebSubject)) {
                String msg = "Subject implementation returned from the SecurityManager was not a " +
                        WebSubject.class.getName() + " implementation.  Please ensure a Web-enabled SecurityManager " +
                        "has been configured and made available to this builder.";
                throw new IllegalStateException(msg);
            }
            return (WebSubject) subject;
        }

可以看到WebSubject的build方法最终调用了父类的buildSubject方法,跟进这个方法。

5.跟进父类Subject的buildSubject方法

        public Subject buildSubject() {
            return this.securityManager.createSubject(this.subjectContext);
        }

发现调用的是securityManager的createSubject方法,继续跟进

public Subject createSubject(SubjectContext subjectContext) {
        //create a copy so we don't modify the argument's backing map:
        SubjectContext context = copy(subjectContext);

        //ensure that the context has a SecurityManager instance, and if not, add one:
        context = ensureSecurityManager(context);

        //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before
        //sending to the SubjectFactory.  The SubjectFactory should not need to know how to acquire sessions as the
        //process is often environment specific - better to shield the SF from these details:
        context = resolveSession(context);

        //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first
        //if possible before handing off to the SubjectFactory:
        context = resolvePrincipals(context);

        Subject subject = doCreateSubject(context);

        //save this subject for future reference if necessary:
        //(this is needed here in case rememberMe principals were resolved and they need to be stored in the
        //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation).
        //Added in 1.2:
        save(subject);

        return subject;
    }

方法的的作用注释已经说明的很清楚,我们需要注意的是doCreateSubject(context)这个方法,securityManger通过这个方法根据传入的subjectContext构建了一个Subject对象。

    protected Subject doCreateSubject(SubjectContext context) {
        return getSubjectFactory().createSubject(context);
    }

跟进发现securityManger使用了内部的subjectFactoy对象进行subject的创建。

public Subject createSubject(SubjectContext context) {
        if (!(context instanceof WebSubjectContext)) {
            return super.createSubject(context);
        }
        WebSubjectContext wsc = (WebSubjectContext) context;
        SecurityManager securityManager = wsc.resolveSecurityManager();
        Session session = wsc.resolveSession();
        boolean sessionEnabled = wsc.isSessionCreationEnabled();
        PrincipalCollection principals = wsc.resolvePrincipals();
        boolean authenticated = wsc.resolveAuthenticated();
        String host = wsc.resolveHost();
        ServletRequest request = wsc.resolveServletRequest();
        ServletResponse response = wsc.resolveServletResponse();

        return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled,
                request, response, securityManager);
    }

方法首先对传入的SubjectContext的类型做了一个判断,我们只关心wsc的情况。发现方法通过wsc获取了shiro框架中一系列重要对象如principal,session后构建了一个WebDelegatingSubject对象。先看看resolveSession这个方法。

6.SecurityManaget中的resolveSession

 protected SubjectContext resolveSession(SubjectContext context) {
        if (context.resolveSession() != null) {
            log.debug("Context already contains a session.  Returning.");
            return context;
        }
        try {
            //Context couldn't resolve it directly, let's see if we can since we have direct access to 
            //the session manager:
            Session session = resolveContextSession(context);
            if (session != null) {
                context.setSession(session);
            }
        } catch (InvalidSessionException e) {
            log.debug("Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous " +
                    "(session-less) Subject instance.", e);
        }
        return context;
    }

首先试图从SubjectContext中获取session,因此让我跟进一下这个方法:
sc中的resolveSession方法

    //SubjectContext.class
    public Session resolveSession() {
        Session session = getSession();
        if (session == null) {
            //try the Subject if it exists:
            Subject existingSubject = getSubject();
            if (existingSubject != null) {
                session = existingSubject.getSession(false);
            }
        }
        return session;
    }

首先会检查subjectContext中的session是否是null。因为此时session还没有与sc做绑定因此getSession方法必定返回null,跳入第二个分支试图从与sc绑定的subject中获取,同理此时subject也为null,因此return session 返回的一定是一个null对象。让我们回到securityManager中的resolveSession方法,接下来会执行session为null的那一个分支。即

try {
            //Context couldn't resolve it directly, let's see if we can since we have direct access to 
            //the session manager:
            Session session = resolveContextSession(context);
            if (session != null) {
                context.setSession(session);
            }
        } catch (InvalidSessionException e) {
            log.debug("Resolved SubjectContext context session is invalid.  Ignoring and creating an anonymous " +
                    "(session-less) Subject instance.", e);
        }

这个代码片段。通过resolveContextSession(context)方法获取session并在获取成功之后与context进行绑定(因此接下来如果再调用这个方法可以直接走从Context获取的分支)。于是我们分析的重点就转移到了resolveContextSession(context)这个方法上。

7.resolveContextSession(context)

    protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
        SessionKey key = getSessionKey(context);
        if (key != null) {
            return getSession(key);
        }
        return null;
    }

这里通过context获取了一个新的对象SessionKey,只有一个方法getSessionId(),通过注释可以得知通过SessionKey可以找到唯一指定的session。弄清楚SessionKey的作用后我们开始分析getSessionKey方法

//DefaultWebSecurityManager.class
protected SessionKey getSessionKey(SubjectContext context) {
        //首先判断是否是web环境
        if (WebUtils.isWeb(context)) {
            //获取sessionId
            Serializable sessionId = context.getSessionId();
            ServletRequest request = WebUtils.getRequest(context);
            ServletResponse response = WebUtils.getResponse(context);
            return new WebSessionKey(sessionId, request, response);
        } else {
            return super.getSessionKey(context);

        }
    }

context.getSessionId()

    public Serializable getSessionId() {
        return getTypedValue(SESSION_ID, Serializable.class);
    }

从context中根据键SESSION_ID进行取值,因为还没有进行设置因此返回的sessionId为null。

getSessionKey最终构造了一个WebSessionKey对象并返回。因此resolveContextSession方法走入执行getSession(key)方法的分支。

getSession(key)

//SessionSecurityManager
    public Session getSession(SessionKey key) throws SessionException {
        return this.sessionManager.getSession(key);
    }

继续跟进SessionManager的getSession(key)

//AbstractNativeSessionManager.class
    public Session getSession(SessionKey key) throws SessionException {
        //首先根据key寻找session
        Session session = lookupSession(key);
        return session != null ? createExposedSession(session, key) : null;
    }

lookupSession(key)

    private Session lookupSession(SessionKey key) throws SessionException {
        if (key == null) {
            throw new NullPointerException("SessionKey argument cannot be null.");
        }
        return doGetSession(key);
    }

doGetSession(key)

    @Override
    protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
        enableSessionValidationIfNecessary();

        log.trace("Attempting to retrieve session with key {}", key);

        Session s = retrieveSession(key);
        if (s != null) {
            validate(s, key);
        }
        return s;
    }

重点关注retrieveSession(key);这个方法,使用该方法获取session后使用validate方法校验后即可返回。
retrieveSession(key)

//这里使用的是我在CustomeSessionManager中覆写的retrieveSession
@Override
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        //通过SessionKey对象获取sessionId
        Serializable sessionId = getSessionId(sessionKey);
        ServletRequest request = null;
        if(sessionKey instanceof WebSessionKey){
            request = ((WebSessionKey)sessionKey).getServletRequest();
        }
        //尝试从request中取而不是每次都请求数据库
        if(request !=null && sessionId !=null){
            Session session = (Session) request.getAttribute(sessionId.toString());
            if(session != null){
                return session;
            }
        }
        //如果request中没有session则从数据库中请求并把请求结果设置给sessionKey
        Session session = super.retrieveSession(sessionKey);
        if(request !=null && sessionId != null){
            request.setAttribute(sessionId.toString(),session);
        }
        return session;
    }

首先关注getSessionId(sessionKey)这个方法,因为到目前为止我们的sessionKey对象中的sessionId属性仍然是空的。
getSessionId(sessionKey)

//DefaultWebSessionManager.class
 public Serializable getSessionId(SessionKey key) {
        Serializable id = super.getSessionId(key);
        if (id == null && WebUtils.isWeb(key)) {
            ServletRequest request = WebUtils.getRequest(key);
            ServletResponse response = WebUtils.getResponse(key);
            id = getSessionId(request, response);
        }
        return id;
    }

super.getSessionId(key);的逻辑很简单,就是获取传入sessionKey的sessionId属性,获取到的当然是空值因此走入if分支。从sessionKey中获取request和response对象然后通过getSessionId(request, response)方法生成sessionId。
getSessionId(request, response)

    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        return getReferencedSessionId(request, response);
    }

getReferencedSessionId(request, response)

    private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
        //试图从cookie中获取sessionId(这里已经可以看出shiro的session实现原理也是基于cookie的)
        String id = getSessionIdCookieValue(request, response);
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                    ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
        } else {//cookie被禁用的情况,使用url后缀裹挟sessionId的方式实现session
            //not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting):

            //try the URI path segment parameters first:
            id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME);

            if (id == null) {
                //not a URI path segment parameter, try the query parameters:
                String name = getSessionIdName();
                id = request.getParameter(name);
                if (id == null) {
                    //try lowercase:
                    id = request.getParameter(name.toLowerCase());
                }
            }
            if (id != null) {
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                        ShiroHttpServletRequest.URL_SESSION_ID_SOURCE);
            }
        }
        if (id != null) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            //automatically mark it valid here.  If it is invalid, the
            //onUnknownSession method below will be invoked and we'll remove the attribute at that time.
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
        }

        // always set rewrite flag - SHIRO-361
        request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());

        return id;
    }

getSessionIdCookieValue()

private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) {
        if (!isSessionIdCookieEnabled()) {
            log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie.");
            return null;
        }
        if (!(request instanceof HttpServletRequest)) {
            log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie.  Returning null.");
            return null;
        }
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response));
    }

readValue()

//SimpleCookie.class
public String readValue(HttpServletRequest request, HttpServletResponse ignored) {
        String name = getName();
        String value = null;
        javax.servlet.http.Cookie cookie = getCookie(request, name);
        if (cookie != null) {
            // Validate that the cookie is used at the correct place.
            String path = StringUtils.clean(getPath());
            if (path != null && !pathMatches(path, request.getRequestURI())) {
                log.warn("Found '{}' cookie at path '{}', but should be only used for '{}'", new Object[] { name, request.getRequestURI(), path});
            } else {
                value = cookie.getValue();
                log.debug("Found '{}' cookie value [{}]", name, value);
            }
        } else {
            log.trace("No '{}' cookie value", name);
        }

        return value;
    }

readValue()的逻辑很清晰不多做解释
此时结果一番折腾终于获得了sessionId,让我们回到retrieveSession方法继续往下执行

 Session session = super.retrieveSession(sessionKey);

super.retrieveSession(sessionKey)

//DefaultWebSessionManager.class
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        //虽然此时sessionKey中的sessionId依然是null但由于我们在
        //getReferencedSessionId方法中获取到sessionId后将sessionId存在了
        //request对象中,因此sessionId的值是从request中获取的
        Serializable sessionId = getSessionId(sessionKey);
        if (sessionId == null) {
            log.debug("Unable to resolve session ID from SessionKey [{}].  Returning null to indicate a " +
                    "session could not be found.", sessionKey);
            return null;
        }
        //根据sessionId去数据源取相应的session
        Session s = retrieveSessionFromDataSource(sessionId);
        if (s == null) {
            //session ID was provided, meaning one is expected to be found, but we couldn't find one:
            String msg = "Could not find session with ID [" + sessionId + "]";
            throw new UnknownSessionException(msg);
        }
        return s;
    }

这里需要重视 Session s = retrieveSessionFromDataSource(sessionId)这句代码,通过sessionId去相应的数据源获取对应的session,跟进一下。
retrieveSessionFromDataSource(sessionId)

    protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
        return sessionDAO.readSession(sessionId);
    }

可以看出调用了SessionDAO的readSession方法,由于sessionDAO是可以自由定义与替换的,所以我们可以根据实际场景更换相应的SessionDao。那么到这了就取得了session。可以看到shiro框架内部自身实现了一套session机制,因此shiro的session是可以脱离web容器使用的。


总结

我们从一次请求开始简单分析了shiro框架对于session的处理流程,下一篇博客准备以同样的模式分析shiro对于身份验证以及权限认证的处理流程。(最近比较懒散更新时间待定哈哈)。

猜你喜欢

转载自blog.csdn.net/m0_37556444/article/details/82120546