美文网首页
shiro session绑定流程分析

shiro session绑定流程分析

作者: david9 | 来源:发表于2020-03-09 21:17 被阅读0次

    背景

    项目中选择了shiro框架来作权限控制,没有用spring security(在其他项目用过,不灵活),但是出现一个问题,每次请求都会新生成一个session,打印线程栈信息如下:

    ...
        at org.apache.shiro.web.filter.AccessControlFilter.onPreHandle(AccessControlFilter.java:162)
        at org.apache.shiro.web.filter.PathMatchingFilter.isFilterChainContinued(PathMatchingFilter.java:203)
        at org.apache.shiro.web.filter.PathMatchingFilter.preHandle(PathMatchingFilter.java:178)
        at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:131)
        at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
        at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
        at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:108)
        at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:137)
        at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
        at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:66)
        at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
        at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
        at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
        at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
        at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:387)
        at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
        at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:123)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
        at org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter.doFilterInternal(HttpTraceFilter.java:90)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
    ...
    

    项目的配置:

     @Bean
      public SessionsSecurityManager securityManager(SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setSessionManager(sessionManager);
        securityManager.setCacheManager(new MemoryConstrainedCacheManager());
        securityManager.setRealm(customRealmLogic());
        return securityManager;
      }
    
      @Bean
      public SessionManager sessionManager(MemorySessionDAO memorySessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        return sessionManager;
      }
    

    可以看到请求首先进入OncePerRequestFilter,然后进入到AbstractShiroFilter.doFilterInternal(...)方法,代码如下:

    ...
                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;
                    }
                });
    ...
    

    第三行的createSubject(...)方法,最终会走到DefaultSecurityManager.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;
    ...
    

    ensureSecurityManager(context)其实是DefaultWebSecurityManager实例
    关键代码从resolveSession(context)开始,代码如下:

    ...
        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;
        }
    
        protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
            SessionKey key = getSessionKey(context);
            if (key != null) {
                return getSession(key);
            }
            return null;
        }
    
        protected SessionKey getSessionKey(SubjectContext context) {
            Serializable sessionId = context.getSessionId();
            if (sessionId != null) {
                return new DefaultSessionKey(sessionId);
            }
            return null;
        }
    ...
    

    注意当前是在DefaultSecurityManager类中,且这几个方法都是protected的,也就是说DefaultWebSecurityManager可以覆盖这些方法。
    resolveContextSession(context)看字面意思应该是从某个地方获取当前session,最终会调用getSessionKey(SubjectContext context)方法,这个方法被DefaultWebSecurityManager覆盖,代码如下:

        @Override
        protected SessionKey getSessionKey(SubjectContext context) {
            if (WebUtils.isWeb(context)) {
                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);
    
            }
        }
    

    我们这里应该走if流程,context.getSessionId()应该是null,因为没有发现SubjectContext.etSessionId(Serializable sessionId)的调用,接着,又会走到getSession(key)方法:

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

    这里就调用了sessionManager.getSession(key),因为我们的sessionManager是DefaultWebSessionManager,所以向DefaultWebSessionManager的父类查找getSession(key),在AbstractNativeSessionManager中发现如下代码:

    ...
        public Session getSession(SessionKey key) throws SessionException {
            Session session = lookupSession(key);
            return session != null ? createExposedSession(session, key) : null;
        }
    
        private Session lookupSession(SessionKey key) throws SessionException {
            if (key == null) {
                throw new NullPointerException("SessionKey argument cannot be null.");
            }
            return doGetSession(key);
        }
    ...
        protected abstract Session doGetSession(SessionKey key) throws InvalidSessionException;
    ...
    

    可以看到lookupSession(key)最终会调用doGetSession(SessionKey key) 方法,一直追踪到DefaultSessionManager:

    ...
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
            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;
            }
            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;
        }
    
        protected Serializable getSessionId(SessionKey sessionKey) {
            return sessionKey.getSessionId();
        }
    ...
    

    会调用getSessionId(sessionKey)方法,这个方法在DefaultWebSessionManager被覆盖,代码为:

    ...
       @Override
        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;
        }
    
        protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
            return getReferencedSessionId(request, response);
        }
    ...
        private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) {
            String id = getSessionIdCookieValue(request, response);
            if (id != null) {
                request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                        ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
            } else {
                //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;
        }
    ...
       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));
        }
    ...
    

    可以看到,最终是从cookie中获取sessionid,获取到sessionid之后,会执行DefaultSessionManager.retrieveSession(SessionKey sessionKey)方法:

    ...
        protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
            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;
            }
            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;
        }
    
        protected Serializable getSessionId(SessionKey sessionKey) {
            return sessionKey.getSessionId();
        }
    
        protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException {
            return sessionDAO.readSession(sessionId);
        }
    ...
    

    最终从sessionDAO(默认是MemorySessionDAO)中获取session
    回到DefaultSecurityManager中:

    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;
        }
    

    跟踪代码,最终Subject被保存在session中。

    问题解决

    分析了一遍代码,没有发现配置上的错误,打开浏览器F12观察,才看到是cookie中JSESSIONID的path有问题,不是/,导致http请求没有带上JSESSIONID,所以shiro每个请求都创建一个新的session,修改配置解决:

      @Bean
      public SessionManager sessionManager(MemorySessionDAO memorySessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(Constants.SESSION_TIMEOUT);
        Cookie cookie = new SimpleCookie("JSESSIONID");
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        sessionManager.setSessionIdCookie(cookie);
        return sessionManager;
      }
    

    相关文章

      网友评论

          本文标题:shiro session绑定流程分析

          本文链接:https://www.haomeiwen.com/subject/qqjmdhtx.html