• Shiro源码(六)-session 管理&前后端分离项目从请求头传递session信息


      之前在研究Shiro 源码的过程中,发现Shiro 会对request、response、session 进行包装。 下面研究其包装过程以及原理。

      Session是通过包装了request, 重写了其获取Session 的方法。 然后重写了一套Shiro 自己的Session 管理机制(这个session 和 Servlet的HeepSession 没有关系), 只是对外暴露的时候封装成一个ShiroHttpSession 对象(该对象内部包含Shiro 的Session), 最终ShiroHttpSession 相关的操作都会交给Shiro的Session。 Shiro的session 实现了一套自己的生成ID、创建Session、删除、修改、获取所有的等方法; 并且也有定时任务去处理过期的session 等策略。 并且SHiro 提供了可扩展的抽象类,基于抽象类可以快速实现Session 存到Redis 或其他操作。

    1. 使用ServletContainerSessionManager 走原来Servlet的一套机制

    1. shiro 配置

        // 权限管理,配置主要是Realm的管理认证
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    //         注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中
            securityManager.setRealms(Lists.newArrayList(new CustomRealm()));
            return securityManager;
        }

    2. 增加测试Controller, 查看相关类型

        @GetMapping("/login2")
        public String login2(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
            System.out.println(request);
            System.out.println(response);
            System.out.println(session);
    
            System.out.println("华丽的分割线1~~~~");
    
            HttpServletRequest request1 = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
            HttpServletResponse response1 = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
            HttpSession session1 = request1.getSession(false);
            System.out.println(request1);
            System.out.println(response1);
            System.out.println(session1);
            System.out.println("华丽的分割线2~~~~");
    
            Subject subject = SecurityUtils.getSubject();
            AuthenticationToken generateToken = new UsernamePasswordToken("zs", "111222");
            subject.login(generateToken);
    
            return "success";
        }

    3. shiro 配置该路径允许匿名访问

    4. 测试

    访问后日志如下:

    org.apache.shiro.web.servlet.ShiroHttpServletRequest@7adc6ef1
    org.apache.catalina.connector.ResponseFacade@4802cdcd
    org.apache.catalina.session.StandardSessionFacade@1374f85e
    华丽的分割线1~~~~
    org.apache.shiro.web.servlet.ShiroHttpServletRequest@7adc6ef1
    org.apache.catalina.connector.ResponseFacade@4802cdcd
    org.apache.catalina.session.StandardSessionFacade@1374f85e
    华丽的分割线2~~~~

      可以看到默认对Request 进行了包装,Response和sesson 仍然使用原来Servlet 中使用的对象。

    5. 原理

    1. org.apache.shiro.web.mgt.DefaultWebSecurityManager#DefaultWebSecurityManager() 构造如下:

        public DefaultWebSecurityManager() {
            super();
            DefaultWebSessionStorageEvaluator webEvalutator = new DefaultWebSessionStorageEvaluator();  
            ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(webEvalutator);
            this.sessionMode = HTTP_SESSION_MODE;
            setSubjectFactory(new DefaultWebSubjectFactory());
            setRememberMeManager(new CookieRememberMeManager());
            setSessionManager(new ServletContainerSessionManager());
            webEvalutator.setSessionManager(getSessionManager());
        }

    可以看到默认的sessionManager 是 org.apache.shiro.web.session.mgt.ServletContainerSessionManager:

    类图图像:

     源码如下:

    public class ServletContainerSessionManager implements WebSessionManager {
    
        //TODO - complete JavaDoc
    
        //TODO - read session timeout value from web.xml
    
        public ServletContainerSessionManager() {
        }
    
        public Session start(SessionContext context) throws AuthorizationException {
            return createSession(context);
        }
    
        public Session getSession(SessionKey key) throws SessionException {
            if (!WebUtils.isHttp(key)) {
                String msg = "SessionKey must be an HTTP compatible implementation.";
                throw new IllegalArgumentException(msg);
            }
    
            HttpServletRequest request = WebUtils.getHttpRequest(key);
    
            Session session = null;
    
            HttpSession httpSession = request.getSession(false);
            if (httpSession != null) {
                session = createSession(httpSession, request.getRemoteHost());
            }
    
            return session;
        }
    
        private String getHost(SessionContext context) {
            String host = context.getHost();
            if (host == null) {
                ServletRequest request = WebUtils.getRequest(context);
                if (request != null) {
                    host = request.getRemoteHost();
                }
            }
            return host;
    
        }
    
        /**
         * @since 1.0
         */
        protected Session createSession(SessionContext sessionContext) throws AuthorizationException {
            if (!WebUtils.isHttp(sessionContext)) {
                String msg = "SessionContext must be an HTTP compatible implementation.";
                throw new IllegalArgumentException(msg);
            }
    
            HttpServletRequest request = WebUtils.getHttpRequest(sessionContext);
    
            HttpSession httpSession = request.getSession();
    
            //SHIRO-240: DO NOT use the 'globalSessionTimeout' value here on the acquired session.
            //see: https://issues.apache.org/jira/browse/SHIRO-240
    
            String host = getHost(sessionContext);
    
            return createSession(httpSession, host);
        }
    
        protected Session createSession(HttpSession httpSession, String host) {
            return new HttpServletSession(httpSession, host);
        }
    
        /**
         * This implementation always delegates to the servlet container for sessions, so this method returns
         * {@code true} always.
         *
         * @return {@code true} always
         * @since 1.2
         */
        public boolean isServletContainerSessions() {
            return true;
        }
    }

    2. 代理代码查看:

    org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 是Shiro的过滤器执行的入口:

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

    (1) 包装Request的代码: org.apache.shiro.web.servlet.AbstractShiroFilter#prepareServletRequest

        protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) {
            ServletRequest toUse = request;
            if (request instanceof HttpServletRequest) {
                HttpServletRequest http = (HttpServletRequest) request;
                toUse = wrapServletRequest(http);
            }
            return toUse;
        }
    
        protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
            return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());
        }
    
        protected boolean isHttpSessions() {
            return getSecurityManager().isHttpSessionMode();
        }

    1》org.apache.shiro.web.servlet.ShiroHttpServletRequest#ShiroHttpServletRequest 构造如下:

    public class ShiroHttpServletRequest extends HttpServletRequestWrapper {
    
        public static final String COOKIE_SESSION_ID_SOURCE = "cookie";
        public static final String URL_SESSION_ID_SOURCE = "url";
        public static final String REFERENCED_SESSION_ID = ShiroHttpServletRequest.class.getName() + "_REQUESTED_SESSION_ID";
        public static final String REFERENCED_SESSION_ID_IS_VALID = ShiroHttpServletRequest.class.getName() + "_REQUESTED_SESSION_ID_VALID";
        public static final String REFERENCED_SESSION_IS_NEW = ShiroHttpServletRequest.class.getName() + "_REFERENCED_SESSION_IS_NEW";
        public static final String REFERENCED_SESSION_ID_SOURCE = ShiroHttpServletRequest.class.getName() + "REFERENCED_SESSION_ID_SOURCE";
        public static final String IDENTITY_REMOVED_KEY = ShiroHttpServletRequest.class.getName() + "_IDENTITY_REMOVED_KEY";
        public static final String SESSION_ID_URL_REWRITING_ENABLED = ShiroHttpServletRequest.class.getName() + "_SESSION_ID_URL_REWRITING_ENABLED";
    
        protected ServletContext servletContext = null;
    
        protected HttpSession session = null;
        protected boolean httpSessions = true;
    
        public ShiroHttpServletRequest(HttpServletRequest wrapped, ServletContext servletContext, boolean httpSessions) {
            super(wrapped);
            this.servletContext = servletContext;
            this.httpSessions = httpSessions;
        }

    2》判断isHttpSessions 是否是 httpSession 的方法如下:

    org.apache.shiro.web.mgt.DefaultWebSecurityManager#isHttpSessionMode:

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

      也就是判断是否是WebSessionManager, 然后调用 isServletContainerSessions 判断。 默认的ServletContainerSessionManager 返回true。 所以是HttpSessionMode。

    (2) 包装Response 的代码 org.apache.shiro.web.servlet.AbstractShiroFilter#prepareServletResponse

        protected ServletResponse prepareServletResponse(ServletRequest request, ServletResponse response, FilterChain chain) {
            ServletResponse toUse = response;
            if (!isHttpSessions() && (request instanceof ShiroHttpServletRequest) &&
                    (response instanceof HttpServletResponse)) {
                //the ShiroHttpServletResponse exists to support URL rewriting for session ids.  This is only needed if
                //using Shiro sessions (i.e. not simple HttpSession based sessions):
                toUse = wrapServletResponse((HttpServletResponse) response, (ShiroHttpServletRequest) request);
            }
            return toUse;
        }

      可以看到包装的条件是: !isHttpSessions() 并且request是 ShiroHttpServletRequest; 并且 response instanceof HttpServletResponse。 第一个条件不满足,所以不会进行包装。

    3》 session 进行包装的条件:

      createSubject(request, response); 创建Subject, 会进行解析相关session。

    (1) 调用到: org.apache.shiro.mgt.DefaultSecurityManager#resolveContextSession

        protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {
            SessionKey key = getSessionKey(context);
            if (key != null) {
                return getSession(key);
            }
            return null;
        }
    • 调用 org.apache.shiro.web.mgt.DefaultWebSecurityManager#getSessionKey 获取SessionKey
        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);
    
            }
        }

      第一次获取到的sessionId 为空,返回一个WebSessionKey 对象, sessionId 为空。

    • 调用org.apache.shiro.mgt.SessionsSecurityManager#getSession 获取session
        public Session getSession(SessionKey key) throws SessionException {
            return this.sessionManager.getSession(key);
        }

    继续调用调用到:org.apache.shiro.web.session.mgt.ServletContainerSessionManager#getSession

        public Session getSession(SessionKey key) throws SessionException {
            if (!WebUtils.isHttp(key)) {
                String msg = "SessionKey must be an HTTP compatible implementation.";
                throw new IllegalArgumentException(msg);
            }
    
            HttpServletRequest request = WebUtils.getHttpRequest(key);
    
            Session session = null;
    
            HttpSession httpSession = request.getSession(false);
            if (httpSession != null) {
                session = createSession(httpSession, request.getRemoteHost());
            }
    
            return session;
        }
    
        protected Session createSession(HttpSession httpSession, String host) {
            return new HttpServletSession(httpSession, host);
        }

    继续调用到: org.apache.shiro.web.servlet.ShiroHttpServletRequest#getSession(boolean)

        public HttpSession getSession(boolean create) {
    
            HttpSession httpSession;
    
            if (isHttpSessions()) {
                httpSession = super.getSession(false);
                if (httpSession == null && create) {
                    //Shiro 1.2: assert that creation is enabled (SHIRO-266):
                    if (WebUtils._isSessionCreationEnabled(this)) {
                        httpSession = super.getSession(create);
                    } else {
                        throw newNoSessionCreationException();
                    }
                }
            } else {
                boolean existing = getSubject().getSession(false) != null;
                
                if (this.session == null || !existing) {
                    Session shiroSession = getSubject().getSession(create);
                    if (shiroSession != null) {
                        this.session = new ShiroHttpSession(shiroSession, this, this.servletContext);
                        if (!existing) {
                            setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
                        }
                    } else if (this.session != null) {
                        this.session = null;
                    }
                }
                httpSession = this.session;
            }
    
            return httpSession;
        }

      可以看到isHttpSessions 是true, 所以走上面不包装的代码逻辑。也就是所有的session 都走的原来session的一套机制。

    2. 修改Sessionmanager为DefaultWebSessionManager走Shiro的机制

    1. 修改配置

        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    //         注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中
            securityManager.setRealms(Lists.newArrayList(new CustomRealm()));
            securityManager.setSessionManager(new DefaultWebSessionManager());
            return securityManager;
        }

    2. 测试

    查看控制台如下:

    org.apache.shiro.web.servlet.ShiroHttpServletRequest@675f5183
    org.apache.shiro.web.servlet.ShiroHttpServletResponse@6998a318
    org.apache.shiro.web.servlet.ShiroHttpSession@6da73af3
    华丽的分割线1~~~~
    org.apache.shiro.web.servlet.ShiroHttpServletRequest@675f5183
    org.apache.shiro.web.servlet.ShiroHttpServletResponse@6998a318
    org.apache.shiro.web.servlet.ShiroHttpSession@6da73af3
    华丽的分割线2~~~~

      可以看到对request、response、session 都进行了包装。查看相关对象如下:

    (1) request:

     (2) response:

    (3) session:

    4. 包装原理

    1. org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 在Filter 内部, Shiro 对request、response 进行了包装。

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

      prepareServletRequest 包装request; prepareServletResponse 包装response。  因为org.apache.shiro.web.session.mgt.DefaultWebSessionManager#isServletContainerSessions 返回是false, 所以取后是true。 那么满足包装的条件。

        protected ServletResponse prepareServletResponse(ServletRequest request, ServletResponse response, FilterChain chain) {
            ServletResponse toUse = response;
            if (!isHttpSessions() && (request instanceof ShiroHttpServletRequest) &&
                    (response instanceof HttpServletResponse)) {
                //the ShiroHttpServletResponse exists to support URL rewriting for session ids.  This is only needed if
                //using Shiro sessions (i.e. not simple HttpSession based sessions):
                toUse = wrapServletResponse((HttpServletResponse) response, (ShiroHttpServletRequest) request);
            }
            return toUse;
        }

    2. 进行后续调用  executeChain(request, response, chain); 用的都是包装后的request, response。 

    3. 过滤器执行完后会进入Servlet 调用, 调用SpringMVC 的方法org.springframework.web.servlet.DispatcherServlet#doService 之前会先进入其父类 org.springframework.web.servlet.FrameworkServlet#processRequest:

        protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
    
            long startTime = System.currentTimeMillis();
            Throwable failureCause = null;
    
            LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
            LocaleContext localeContext = buildLocaleContext(request);
    
            RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
    
            WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
            asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
    
            initContextHolders(request, localeContext, requestAttributes);
    
            try {
                doService(request, response);
            }
            catch (ServletException | IOException ex) {
                failureCause = ex;
                throw ex;
            }
            catch (Throwable ex) {
                failureCause = ex;
                throw new NestedServletException("Request processing failed", ex);
            }
    
            finally {
                resetContextHolders(request, previousLocaleContext, previousAttributes);
                if (requestAttributes != null) {
                    requestAttributes.requestCompleted();
                }
                logResult(request, response, failureCause, asyncManager);
                publishRequestHandledEvent(request, response, startTime, failureCause);
            }
        }

    1》 调用 buildRequestAttributes(request, response, previousAttributes); 将Request、Response 维护起来, 这里的request和response 对象都是包装后的对象。

    2》 org.springframework.web.servlet.FrameworkServlet#initContextHolders 将上面的对象保存到ThreadLocal 中, 也就是当前环境使用的request、response 以及传递到SpringMVC、Controller 的request、response 都是shiro 包装后的对象。

        private void initContextHolders(HttpServletRequest request,
                @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
    
            if (localeContext != null) {
                LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
            }
            if (requestAttributes != null) {
                RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
            }
        }

      RequestContextHolder 是Spring 记录请求上下文环境的对象,用的也是ThreadLocal 对象进行维护。

    3. session管理

      修改为DefaultWebSessionManager  之后,实际就是将session 交给shiro 管理。 

    1. 类图:

    2. 重要类

    1. org.apache.shiro.web.session.mgt.DefaultWebSessionManager#DefaultWebSessionManager 构造如下:

        public DefaultWebSessionManager() {
            Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
            cookie.setHttpOnly(true); //more secure, protects against XSS attacks
            this.sessionIdCookie = cookie;
            this.sessionIdCookieEnabled = true;
            this.sessionIdUrlRewritingEnabled = true;
        }

      sessionIdCookie 默认是读取名称为JSESSIONID 的cookie。

    父类构造: org.apache.shiro.session.mgt.DefaultSessionManager#DefaultSessionManager  可以看到创建了一个 MemorySessionDAO

        public DefaultSessionManager() {
            this.deleteInvalidSessions = true;
            this.sessionFactory = new SimpleSessionFactory();
            this.sessionDAO = new MemorySessionDAO();
        }

    涉及到的重要的类如下:

    (1) org.apache.shiro.session.mgt.SimpleSessionFactory 源码如下: 简单创建一个session

    package org.apache.shiro.session.mgt;
    
    import org.apache.shiro.session.Session;
    
    /**
     * {@code SessionFactory} implementation that generates {@link SimpleSession} instances.
     *
     * @since 1.0
     */
    public class SimpleSessionFactory implements SessionFactory {
    
        /**
         * Creates a new {@link SimpleSession SimpleSession} instance retaining the context's
         * {@link SessionContext#getHost() host} if one can be found.
         *
         * @param initData the initialization data to be used during {@link Session} creation.
         * @return a new {@link SimpleSession SimpleSession} instance
         */
        public Session createSession(SessionContext initData) {
            if (initData != null) {
                String host = initData.getHost();
                if (host != null) {
                    return new SimpleSession(host);
                }
            }
            return new SimpleSession();
        }
    }

    (2) org.apache.shiro.session.mgt.eis.SessionDAO 是一个接口,提供了session 的 增删改查

    package org.apache.shiro.session.mgt.eis;
    
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    
    import java.io.Serializable;
    import java.util.Collection;
    
    
    /**
     * Data Access Object design pattern specification to enable {@link Session} access to an
     * EIS (Enterprise Information System).  It provides your four typical CRUD methods:
     * {@link #create}, {@link #readSession(java.io.Serializable)}, {@link #update(org.apache.shiro.session.Session)},
     * and {@link #delete(org.apache.shiro.session.Session)}.
     * <p/>
     * The remaining {@link #getActiveSessions()} method exists as a support mechanism to pre-emptively orphaned sessions,
     * typically by {@link org.apache.shiro.session.mgt.ValidatingSessionManager ValidatingSessionManager}s), and should
     * be as efficient as possible, especially if there are thousands of active sessions.  Large scale/high performance
     * implementations will often return a subset of the total active sessions and perform validation a little more
     * frequently, rather than return a massive set and infrequently validate.
     *
     * @since 0.1
     */
    public interface SessionDAO {
    
        /**
         * Inserts a new Session record into the underling EIS (e.g. Relational database, file system, persistent cache,
         * etc, depending on the DAO implementation).
         * <p/>
         * After this method is invoked, the {@link org.apache.shiro.session.Session#getId()}
         * method executed on the argument must return a valid session identifier.  That is, the following should
         * always be true:
         * <pre>
         * Serializable id = create( session );
         * id.equals( session.getId() ) == true</pre>
         * <p/>
         * Implementations are free to throw any exceptions that might occur due to
         * integrity violation constraints or other EIS related errors.
         *
         * @param session the {@link org.apache.shiro.session.Session} object to create in the EIS.
         * @return the EIS id (e.g. primary key) of the created {@code Session} object.
         */
        Serializable create(Session session);
    
        /**
         * Retrieves the session from the EIS uniquely identified by the specified
         * {@code sessionId}.
         *
         * @param sessionId the system-wide unique identifier of the Session object to retrieve from
         *                  the EIS.
         * @return the persisted session in the EIS identified by {@code sessionId}.
         * @throws UnknownSessionException if there is no EIS record for any session with the
         *                                 specified {@code sessionId}
         */
        Session readSession(Serializable sessionId) throws UnknownSessionException;
    
        /**
         * Updates (persists) data from a previously created Session instance in the EIS identified by
         * {@code {@link Session#getId() session.getId()}}.  This effectively propagates
         * the data in the argument to the EIS record previously saved.
         * <p/>
         * In addition to UnknownSessionException, implementations are free to throw any other
         * exceptions that might occur due to integrity violation constraints or other EIS related
         * errors.
         *
         * @param session the Session to update
         * @throws org.apache.shiro.session.UnknownSessionException
         *          if no existing EIS session record exists with the
         *          identifier of {@link Session#getId() session.getSessionId()}
         */
        void update(Session session) throws UnknownSessionException;
    
        /**
         * Deletes the associated EIS record of the specified {@code session}.  If there never
         * existed a session EIS record with the identifier of
         * {@link Session#getId() session.getId()}, then this method does nothing.
         *
         * @param session the session to delete.
         */
        void delete(Session session);
    
        /**
         * Returns all sessions in the EIS that are considered active, meaning all sessions that
         * haven't been stopped/expired.  This is primarily used to validate potential orphans.
         * <p/>
         * If there are no active sessions in the EIS, this method may return an empty collection or {@code null}.
         * <h4>Performance</h4>
         * This method should be as efficient as possible, especially in larger systems where there might be
         * thousands of active sessions.  Large scale/high performance
         * implementations will often return a subset of the total active sessions and perform validation a little more
         * frequently, rather than return a massive set and validate infrequently.  If efficient and possible, it would
         * make sense to return the oldest unstopped sessions available, ordered by
         * {@link org.apache.shiro.session.Session#getLastAccessTime() lastAccessTime}.
         * <h4>Smart Results</h4>
         * <em>Ideally</em> this method would only return active sessions that the EIS was certain should be invalided.
         * Typically that is any session that is not stopped and where its lastAccessTimestamp is older than the session
         * timeout.
         * <p/>
         * For example, if sessions were backed by a relational database or SQL-92 'query-able' enterprise cache, you might
         * return something similar to the results returned by this query (assuming
         * {@link org.apache.shiro.session.mgt.SimpleSession SimpleSession}s were being stored):
         * <pre>
         * select * from sessions s where s.lastAccessTimestamp < ? and s.stopTimestamp is null
         * </pre>
         * where the {@code ?} parameter is a date instance equal to 'now' minus the session timeout
         * (e.g. now - 30 minutes).
         *
         * @return a Collection of {@code Session}s that are considered active, or an
         *         empty collection or {@code null} if there are no active sessions.
         */
        Collection<Session> getActiveSessions();
    }

    org.apache.shiro.session.mgt.eis.AbstractSessionDAO 抽象dao:

    package org.apache.shiro.session.mgt.eis;
    
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.session.mgt.SimpleSession;
    
    import java.io.Serializable;
    
    
    /**
     * An abstract {@code SessionDAO} implementation that performs some sanity checks on session creation and reading and
     * allows for pluggable Session ID generation strategies if desired.  The {@code SessionDAO}
     * {@link SessionDAO#update update} and {@link SessionDAO#delete delete} methods are left to
     * subclasses.
     * <h3>Session ID Generation</h3>
     * This class also allows for plugging in a {@link SessionIdGenerator} for custom ID generation strategies.  This is
     * optional, as the default generator is probably sufficient for most cases.  Subclass implementations that do use a
     * generator (default or custom) will want to call the
     * {@link #generateSessionId(org.apache.shiro.session.Session)} method from within their {@link #doCreate}
     * implementations.
     * <p/>
     * Subclass implementations that rely on the EIS data store to generate the ID automatically (e.g. when the session
     * ID is also an auto-generated primary key), they can simply ignore the {@code SessionIdGenerator} concept
     * entirely and just return the data store's ID from the {@link #doCreate} implementation.
     *
     * @since 1.0
     */
    public abstract class AbstractSessionDAO implements SessionDAO {
    
        /**
         * Optional SessionIdGenerator instance available to subclasses via the
         * {@link #generateSessionId(org.apache.shiro.session.Session)} method.
         */
        private SessionIdGenerator sessionIdGenerator;
    
        /**
         * Default no-arg constructor that defaults the {@link #setSessionIdGenerator sessionIdGenerator} to be a
         * {@link org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator}.
         */
        public AbstractSessionDAO() {
            this.sessionIdGenerator = new JavaUuidSessionIdGenerator();
        }
    
        /**
         * Returns the {@code SessionIdGenerator} used by the {@link #generateSessionId(org.apache.shiro.session.Session)}
         * method.  Unless overridden by the {@link #setSessionIdGenerator(SessionIdGenerator)} method, the default instance
         * is a {@link JavaUuidSessionIdGenerator}.
         *
         * @return the {@code SessionIdGenerator} used by the {@link #generateSessionId(org.apache.shiro.session.Session)}
         *         method.
         */
        public SessionIdGenerator getSessionIdGenerator() {
            return sessionIdGenerator;
        }
    
        /**
         * Sets the {@code SessionIdGenerator} used by the {@link #generateSessionId(org.apache.shiro.session.Session)}
         * method.  Unless overridden by this method, the default instance ss a {@link JavaUuidSessionIdGenerator}.
         *
         * @param sessionIdGenerator the {@code SessionIdGenerator} to use in the
         *                           {@link #generateSessionId(org.apache.shiro.session.Session)} method.
         */
        public void setSessionIdGenerator(SessionIdGenerator sessionIdGenerator) {
            this.sessionIdGenerator = sessionIdGenerator;
        }
    
        /**
         * Generates a new ID to be applied to the specified {@code session} instance.  This method is usually called
         * from within a subclass's {@link #doCreate} implementation where they assign the returned id to the session
         * instance and then create a record with this ID in the EIS data store.
         * <p/>
         * Subclass implementations backed by EIS data stores that auto-generate IDs during record creation, such as
         * relational databases, don't need to use this method or the {@link #getSessionIdGenerator() sessionIdGenerator}
         * attribute - they can simply return the data store's generated ID from the {@link #doCreate} implementation
         * if desired.
         * <p/>
         * This implementation uses the {@link #setSessionIdGenerator configured} {@link SessionIdGenerator} to create
         * the ID.
         *
         * @param session the new session instance for which an ID will be generated and then assigned
         * @return the generated ID to assign
         */
        protected Serializable generateSessionId(Session session) {
            if (this.sessionIdGenerator == null) {
                String msg = "sessionIdGenerator attribute has not been configured.";
                throw new IllegalStateException(msg);
            }
            return this.sessionIdGenerator.generateId(session);
        }
    
        /**
         * Creates the session by delegating EIS creation to subclasses via the {@link #doCreate} method, and then
         * asserting that the returned sessionId is not null.
         *
         * @param session Session object to create in the EIS and associate with an ID.
         */
        public Serializable create(Session session) {
            Serializable sessionId = doCreate(session);
            verifySessionId(sessionId);
            return sessionId;
        }
    
        /**
         * Ensures the sessionId returned from the subclass implementation of {@link #doCreate} is not null and not
         * already in use.
         *
         * @param sessionId session id returned from the subclass implementation of {@link #doCreate}
         */
        private void verifySessionId(Serializable sessionId) {
            if (sessionId == null) {
                String msg = "sessionId returned from doCreate implementation is null.  Please verify the implementation.";
                throw new IllegalStateException(msg);
            }
        }
    
        /**
         * Utility method available to subclasses that wish to
         * assign a generated session ID to the session instance directly.  This method is not used by the
         * {@code AbstractSessionDAO} implementation directly, but it is provided so subclasses don't
         * need to know the {@code Session} implementation if they don't need to.
         * <p/>
         * This default implementation casts the argument to a {@link SimpleSession}, Shiro's default EIS implementation.
         *
         * @param session   the session instance to which the sessionId will be applied
         * @param sessionId the id to assign to the specified session instance.
         */
        protected void assignSessionId(Session session, Serializable sessionId) {
            ((SimpleSession) session).setId(sessionId);
        }
    
        /**
         * Subclass hook to actually persist the given <tt>Session</tt> instance to the underlying EIS.
         *
         * @param session the Session instance to persist to the EIS.
         * @return the id of the session created in the EIS (i.e. this is almost always a primary key and should be the
         *         value returned from {@link org.apache.shiro.session.Session#getId() Session.getId()}.
         */
        protected abstract Serializable doCreate(Session session);
    
        /**
         * Retrieves the Session object from the underlying EIS identified by <tt>sessionId</tt> by delegating to
         * the {@link #doReadSession(java.io.Serializable)} method.  If {@code null} is returned from that method, an
         * {@link UnknownSessionException} will be thrown.
         *
         * @param sessionId the id of the session to retrieve from the EIS.
         * @return the session identified by <tt>sessionId</tt> in the EIS.
         * @throws UnknownSessionException if the id specified does not correspond to any session in the EIS.
         */
        public Session readSession(Serializable sessionId) throws UnknownSessionException {
            Session s = doReadSession(sessionId);
            if (s == null) {
                throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
            }
            return s;
        }
    
        /**
         * Subclass implementation hook that retrieves the Session object from the underlying EIS or {@code null} if a
         * session with that ID could not be found.
         *
         * @param sessionId the id of the <tt>Session</tt> to retrieve.
         * @return the Session in the EIS identified by <tt>sessionId</tt> or {@code null} if a
         *         session with that ID could not be found.
         */
        protected abstract Session doReadSession(Serializable sessionId);
    
    }
    View Code

    下面有两个具体的实现:

    org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO 和 org.apache.shiro.session.mgt.eis.MemorySessionDAO:(默认用的这个DAO) - 可以看到session默认是存在自己内部的map 中

    package org.apache.shiro.session.mgt.eis;
    
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.util.CollectionUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.io.Serializable;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.ConcurrentMap;
    
    
    /**
     * Simple memory-based implementation of the SessionDAO that stores all of its sessions in an in-memory
     * {@link ConcurrentMap}.  <b>This implementation does not page to disk and is therefore unsuitable for applications
     * that could experience a large amount of sessions</b> and would therefore cause {@code OutOfMemoryException}s.  It is
     * <em>not</em> recommended for production use in most environments.
     * <h2>Memory Restrictions</h2>
     * If your application is expected to host many sessions beyond what can be stored in the
     * memory available to the JVM, it is highly recommended to use a different {@code SessionDAO} implementation which
     * uses a more expansive or permanent backing data store.
     * <p/>
     * In this case, it is recommended to instead use a custom
     * {@link CachingSessionDAO} implementation that communicates with a higher-capacity data store of your choice
     * (file system, database, etc).
     * <h2>Changes in 1.0</h2>
     * This implementation prior to 1.0 used to subclass the {@link CachingSessionDAO}, but this caused problems with many
     * cache implementations that would expunge entries due to TTL settings, resulting in Sessions that would be randomly
     * (and permanently) lost.  The Shiro 1.0 release refactored this implementation to be 100% memory-based (without
     * {@code Cache} usage to avoid this problem.
     *
     * @see CachingSessionDAO
     * @since 0.1
     */
    public class MemorySessionDAO extends AbstractSessionDAO {
    
        private static final Logger log = LoggerFactory.getLogger(MemorySessionDAO.class);
    
        private ConcurrentMap<Serializable, Session> sessions;
    
        public MemorySessionDAO() {
            this.sessions = new ConcurrentHashMap<Serializable, Session>();
        }
    
        protected Serializable doCreate(Session session) {
            Serializable sessionId = generateSessionId(session);
            assignSessionId(session, sessionId);
            storeSession(sessionId, session);
            return sessionId;
        }
    
        protected Session storeSession(Serializable id, Session session) {
            if (id == null) {
                throw new NullPointerException("id argument cannot be null.");
            }
            return sessions.putIfAbsent(id, session);
        }
    
        protected Session doReadSession(Serializable sessionId) {
            return sessions.get(sessionId);
        }
    
        public void update(Session session) throws UnknownSessionException {
            storeSession(session.getId(), session);
        }
    
        public void delete(Session session) {
            if (session == null) {
                throw new NullPointerException("session argument cannot be null.");
            }
            Serializable id = session.getId();
            if (id != null) {
                sessions.remove(id);
            }
        }
    
        public Collection<Session> getActiveSessions() {
            Collection<Session> values = sessions.values();
            if (CollectionUtils.isEmpty(values)) {
                return Collections.emptySet();
            } else {
                return Collections.unmodifiableCollection(values);
            }
        }
    
    }
    View Code

    (3) 关于session 实现定时任务处理:

    org.apache.shiro.session.mgt.ValidatingSessionManager 接口如下:

    package org.apache.shiro.session.mgt;
    
    /**
     * A ValidatingSessionManager is a SessionManager that can proactively validate any or all sessions
     * that may be expired.
     *
     * @since 0.1
     */
    public interface ValidatingSessionManager extends SessionManager {
    
        /**
         * Performs session validation for all open/active sessions in the system (those that
         * have not been stopped or expired), and validates each one.  If a session is
         * found to be invalid (e.g. it has expired), it is updated and saved to the EIS.
         * <p/>
         * This method is necessary in order to handle orphaned sessions and is expected to be run at
         * a regular interval, such as once an hour, once a day or once a week, etc.
         * The &quot;best&quot; frequency to run this method is entirely dependent upon the application
         * and would be based on factors such as performance, average number of active users, hours of
         * least activity, and other things.
         * <p/>
         * Most enterprise applications use a request/response programming model.
         * This is obvious in the case of web applications due to the HTTP protocol, but it is
         * equally true of remote client applications making remote method invocations.  The server
         * essentially sits idle and only &quot;works&quot; when responding to client requests and/or
         * method invocations.  This type of model is particularly efficient since it means the
         * security system only has to validate a session during those cases.  Such
         * &quot;lazy&quot; behavior enables the system to lie stateless and/or idle and only incur
         * overhead for session validation when necessary.
         * <p/>
         * However, if a client forgets to log-out, or in the event of a server failure, it is
         * possible for sessions to be orphaned since no further requests would utilize that session.
         * Because of these lower-probability cases, it might be required to regularly clean-up the sessions
         * maintained by the system, especially if sessions are backed by a persistent data store.
         * <p/>
         * Even in applications that aren't primarily based on a request/response model,
         * such as those that use enterprise asynchronous messaging (where data is pushed to
         * a client without first receiving a client request), it is almost always acceptable to
         * utilize this lazy approach and run this method at defined interval.
         * <p/>
         * Systems that want to proactively validate individual sessions may simply call the
         * {@link #getSession(SessionKey) getSession(SessionKey)} method on any
         * {@code ValidatingSessionManager} instance as that method is expected to
         * validate the session before retrieving it.  Note that even with proactive calls to {@code getSession},
         * this {@code validateSessions()} method should be invoked regularly anyway to <em>guarantee</em> no
         * orphans exist.
         * <p/>
         * <b>Note:</b> Shiro supports automatic execution of this method at a regular interval
         * by using {@link SessionValidationScheduler}s.  The Shiro default SecurityManager implementations
         * needing session validation will create and use one by default if one is not provided by the
         * application configuration.
         */
        void validateSessions();
    }
    View Code

    org.apache.shiro.session.mgt.AbstractValidatingSessionManager#validateSessions 方法如下:

        /**
         * @see ValidatingSessionManager#validateSessions()
         */
        public void validateSessions() {
            if (log.isInfoEnabled()) {
                log.info("Validating all active sessions...");
            }
    
            int invalidCount = 0;
    
            Collection<Session> activeSessions = getActiveSessions();
    
            if (activeSessions != null && !activeSessions.isEmpty()) {
                for (Session s : activeSessions) {
                    try {
                        //simulate a lookup key to satisfy the method signature.
                        //this could probably stand to be cleaned up in future versions:
                        SessionKey key = new DefaultSessionKey(s.getId());
                        validate(s, key);
                    } catch (InvalidSessionException e) {
                        if (log.isDebugEnabled()) {
                            boolean expired = (e instanceof ExpiredSessionException);
                            String msg = "Invalidated session with id [" + s.getId() + "]" +
                                    (expired ? " (expired)" : " (stopped)");
                            log.debug(msg);
                        }
                        invalidCount++;
                    }
                }
            }
    
            if (log.isInfoEnabled()) {
                String msg = "Finished session validation.";
                if (invalidCount > 0) {
                    msg += "  [" + invalidCount + "] sessions were stopped.";
                } else {
                    msg += "  No sessions were stopped.";
                }
                log.info(msg);
            }
        }
    
        protected void validate(Session session, SessionKey key) throws InvalidSessionException {
            try {
                doValidate(session);
            } catch (ExpiredSessionException ese) {
                onExpiration(session, ese, key);
                throw ese;
            } catch (InvalidSessionException ise) {
                onInvalidation(session, ise, key);
                throw ise;
            }
        }

      在org.apache.shiro.session.mgt.AbstractValidatingSessionManager#createSession 方法会检测是否开启validation 的定时任务。

        protected Session createSession(SessionContext context) throws AuthorizationException {
            enableSessionValidationIfNecessary();
            return doCreateSession(context);
        }
    
        private void enableSessionValidationIfNecessary() {
            SessionValidationScheduler scheduler = getSessionValidationScheduler();
            if (isSessionValidationSchedulerEnabled() && (scheduler == null || !scheduler.isEnabled())) {
                enableSessionValidation();
            }
        }
    
        protected synchronized void enableSessionValidation() {
            SessionValidationScheduler scheduler = getSessionValidationScheduler();
            if (scheduler == null) {
                scheduler = createSessionValidationScheduler();
                setSessionValidationScheduler(scheduler);
            }
            // it is possible that that a scheduler was already created and set via 'setSessionValidationScheduler()'
            // but would not have been enabled/started yet
            if (!scheduler.isEnabled()) {
                if (log.isInfoEnabled()) {
                    log.info("Enabling session validation scheduler...");
                }
                scheduler.enableSessionValidation();
                afterSessionValidationEnabled();
            }
        }
    
        protected SessionValidationScheduler createSessionValidationScheduler() {
            ExecutorServiceSessionValidationScheduler scheduler;
    
            if (log.isDebugEnabled()) {
                log.debug("No sessionValidationScheduler set.  Attempting to create default instance.");
            }
            scheduler = new ExecutorServiceSessionValidationScheduler(this);
            scheduler.setInterval(getSessionValidationInterval());
            if (log.isTraceEnabled()) {
                log.trace("Created default SessionValidationScheduler instance of type [" + scheduler.getClass().getName() + "].");
            }
            return scheduler;
        }

      这里就是创建了一个定时任务, 然后调用 org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler#enableSessionValidation:

        public void enableSessionValidation() {
            if (this.interval > 0l) {
                this.service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {  
                    private final AtomicInteger count = new AtomicInteger(1);
    
                    public Thread newThread(Runnable r) {  
                        Thread thread = new Thread(r);  
                        thread.setDaemon(true);  
                        thread.setName(threadNamePrefix + count.getAndIncrement());
                        return thread;  
                    }  
                });                  
                this.service.scheduleAtFixedRate(this, interval, interval, TimeUnit.MILLISECONDS);
            }
            this.enabled = true;
        }

    3. Session 创建以及处理

    1. 创建:

    org.apache.shiro.web.servlet.ShiroHttpServletRequest#getSession(boolean) 开始创建

        public HttpSession getSession(boolean create) {
    
            HttpSession httpSession;
    
            if (isHttpSessions()) {
                httpSession = super.getSession(false);
                if (httpSession == null && create) {
                    //Shiro 1.2: assert that creation is enabled (SHIRO-266):
                    if (WebUtils._isSessionCreationEnabled(this)) {
                        httpSession = super.getSession(create);
                    } else {
                        throw newNoSessionCreationException();
                    }
                }
            } else {
                boolean existing = getSubject().getSession(false) != null;
                
                if (this.session == null || !existing) {
                    Session shiroSession = getSubject().getSession(create);
                    if (shiroSession != null) {
                        this.session = new ShiroHttpSession(shiroSession, this, this.servletContext);
                        if (!existing) {
                            setAttribute(REFERENCED_SESSION_IS_NEW, Boolean.TRUE);
                        }
                    } else if (this.session != null) {
                        this.session = null;
                    }
                }
                httpSession = this.session;
            }
    
            return httpSession;
        }

      create 为true, 所以走org.apache.shiro.subject.support.DelegatingSubject#getSession(boolean)

    (1) 继续调用org.apache.shiro.subject.support.DelegatingSubject#getSession(boolean):

        public Session getSession(boolean create) {
            if (log.isTraceEnabled()) {
                log.trace("attempting to get session; create = " + create +
                        "; session is null = " + (this.session == null) +
                        "; session has id = " + (this.session != null && session.getId() != null));
            }
    
            if (this.session == null && create) {
    
                //added in 1.2:
                if (!isSessionCreationEnabled()) {
                    String msg = "Session creation has been disabled for the current subject.  This exception indicates " +
                            "that there is either a programming error (using a session when it should never be " +
                            "used) or that Shiro's configuration needs to be adjusted to allow Sessions to be created " +
                            "for the current Subject.  See the " + DisabledSessionException.class.getName() + " JavaDoc " +
                            "for more.";
                    throw new DisabledSessionException(msg);
                }
    
                log.trace("Starting session for host {}", getHost());
                SessionContext sessionContext = createSessionContext();
                Session session = this.securityManager.start(sessionContext);
                this.session = decorate(session);
            }
            return this.session;
        }

    调用 org.apache.shiro.mgt.SessionsSecurityManager#start: 创建session

        public Session start(SessionContext context) throws AuthorizationException {
            return this.sessionManager.start(context);
        }

    继续调用到:org.apache.shiro.session.mgt.AbstractNativeSessionManager#start (创建完session 之后使用全局失效时间,然后进行包装一下返回)

        public Session start(SessionContext context) {
            Session session = createSession(context);
            applyGlobalSessionTimeout(session);
            onStart(session, context);
            notifyStart(session);
            //Don't expose the EIS-tier Session object to the client-tier:
            return createExposedSession(session, context);
        }

    1》org.apache.shiro.session.mgt.AbstractValidatingSessionManager#createSession:

        protected Session createSession(SessionContext context) throws AuthorizationException {
            // 开启定时验证任务
            enableSessionValidationIfNecessary();
            return doCreateSession(context);
        }

    org.apache.shiro.session.mgt.DefaultSessionManager#doCreateSession:

        protected Session doCreateSession(SessionContext context) {
            Session s = newSessionInstance(context);
            if (log.isTraceEnabled()) {
                log.trace("Creating session for host {}", s.getHost());
            }
            create(s);
            return s;
        }

      调用 newSessionInstance 调用到org.apache.shiro.session.mgt.SimpleSessionFactory#createSession 创建session

        public Session createSession(SessionContext initData) {
            if (initData != null) {
                String host = initData.getHost();
                if (host != null) {
                    return new SimpleSession(host);
                }
            }
            return new SimpleSession();
        }

      然后调用org.apache.shiro.session.mgt.DefaultSessionManager#create 创建:

        protected void create(Session session) {
            if (log.isDebugEnabled()) {
                log.debug("Creating new EIS record for new session instance [" + session + "]");
            }
            sessionDAO.create(session);
        }

      然后调用: org.apache.shiro.session.mgt.eis.AbstractSessionDAO#create

        public Serializable create(Session session) {
            Serializable sessionId = doCreate(session);
            verifySessionId(sessionId);
            return sessionId;
        }

      然后调用org.apache.shiro.session.mgt.eis.MemorySessionDAO#doCreate 创建:(生成ID, 赋值, 存储)

        protected Serializable doCreate(Session session) {
            Serializable sessionId = generateSessionId(session);
            assignSessionId(session, sessionId);
            storeSession(sessionId, session);
            return sessionId;
        }

    2》 org.apache.shiro.web.session.mgt.DefaultWebSessionManager#createExposedSession(org.apache.shiro.session.Session, org.apache.shiro.session.mgt.SessionContext) 包装方法如下:

        protected Session createExposedSession(Session session, SessionContext context) {
            if (!WebUtils.isWeb(context)) {
                return super.createExposedSession(session, context);
            }
            ServletRequest request = WebUtils.getRequest(context);
            ServletResponse response = WebUtils.getResponse(context);
            SessionKey key = new WebSessionKey(session.getId(), request, response);
            return new DelegatingSession(this, key);
        }

    3》 org.apache.shiro.subject.support.DelegatingSubject#decorate 装饰:

        protected Session decorate(Session session) {
            if (session == null) {
                throw new IllegalArgumentException("session cannot be null");
            }
            return new StoppingAwareProxiedSession(session, this);
        }

    (2) 包装成 org.apache.shiro.web.servlet.ShiroHttpSession#ShiroHttpSession:

        public ShiroHttpSession(Session session, HttpServletRequest currentRequest, ServletContext servletContext) {
            if (session instanceof HttpServletSession) {
                String msg = "Session constructor argument cannot be an instance of HttpServletSession.  This is enforced to " +
                        "prevent circular dependencies and infinite loops.";
                throw new IllegalArgumentException(msg);
            }
            this.session = session;
            this.currentRequest = currentRequest;
            this.servletContext = servletContext;
        }

     (3) 创建完成之后设置到当前request 的内部属性中, 后续通过request 获取的session 都是上面创建且装饰的ShiroHttpSession 对象。

    总结:

    1. 可以看到。 Shiro 是有一套自己的session 机制。最后将自己的session 封装成ShiroHttpSession 转换为HttpSession。

    2. shiro 的session 的类图如下, 该Session 接口与Servlet 中的HttpSession 没有关系:

    3. org.apache.shiro.web.servlet.ShiroHttpSession 是对外暴露的的HttpSession, 其实现了接口javax.servlet.http.HttpSession。 源码如下:

    package org.apache.shiro.web.servlet;
    
    import org.apache.shiro.session.InvalidSessionException;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.web.session.HttpServletSession;
    
    import javax.servlet.ServletContext;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpSession;
    import javax.servlet.http.HttpSessionBindingEvent;
    import javax.servlet.http.HttpSessionBindingListener;
    import java.util.*;
    
    
    /**
     * Wrapper class that uses a Shiro {@link Session Session} under the hood for all session operations instead of the
     * Servlet Container's session mechanism.  This is required in heterogeneous client environments where the Session
     * is used on both the business tier as well as in multiple client technologies (web, swing, flash, etc) since
     * Servlet container sessions alone cannot support this feature.
     *
     * @since 0.2
     */
    public class ShiroHttpSession implements HttpSession {
    
        //TODO - complete JavaDoc
    
        public static final String DEFAULT_SESSION_ID_NAME = "JSESSIONID";
    
        private static final Enumeration EMPTY_ENUMERATION = new Enumeration() {
            public boolean hasMoreElements() {
                return false;
            }
    
            public Object nextElement() {
                return null;
            }
        };
    
        @SuppressWarnings({"deprecation"})
        private static final javax.servlet.http.HttpSessionContext HTTP_SESSION_CONTEXT =
                new javax.servlet.http.HttpSessionContext() {
                    public HttpSession getSession(String s) {
                        return null;
                    }
    
                    public Enumeration getIds() {
                        return EMPTY_ENUMERATION;
                    }
                };
    
        protected ServletContext servletContext = null;
        protected HttpServletRequest currentRequest = null;
        protected Session session = null; //'real' Shiro Session
    
        public ShiroHttpSession(Session session, HttpServletRequest currentRequest, ServletContext servletContext) {
            if (session instanceof HttpServletSession) {
                String msg = "Session constructor argument cannot be an instance of HttpServletSession.  This is enforced to " +
                        "prevent circular dependencies and infinite loops.";
                throw new IllegalArgumentException(msg);
            }
            this.session = session;
            this.currentRequest = currentRequest;
            this.servletContext = servletContext;
        }
    
        public Session getSession() {
            return this.session;
        }
    
        public long getCreationTime() {
            try {
                return getSession().getStartTimestamp().getTime();
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }
    
        public String getId() {
            return getSession().getId().toString();
        }
    
        public long getLastAccessedTime() {
            return getSession().getLastAccessTime().getTime();
        }
    
        public ServletContext getServletContext() {
            return this.servletContext;
        }
    
        public void setMaxInactiveInterval(int i) {
            try {
                getSession().setTimeout(i * 1000L);
            } catch (InvalidSessionException e) {
                throw new IllegalStateException(e);
            }
        }
    
        public int getMaxInactiveInterval() {
            try {
                return (new Long(getSession().getTimeout() / 1000)).intValue();
            } catch (InvalidSessionException e) {
                throw new IllegalStateException(e);
            }
        }
    
        @SuppressWarnings({"deprecation"})
        public javax.servlet.http.HttpSessionContext getSessionContext() {
            return HTTP_SESSION_CONTEXT;
        }
    
        public Object getAttribute(String s) {
            try {
                return getSession().getAttribute(s);
            } catch (InvalidSessionException e) {
                throw new IllegalStateException(e);
            }
        }
    
        public Object getValue(String s) {
            return getAttribute(s);
        }
    
        @SuppressWarnings({"unchecked"})
        protected Set<String> getKeyNames() {
            Collection<Object> keySet;
            try {
                keySet = getSession().getAttributeKeys();
            } catch (InvalidSessionException e) {
                throw new IllegalStateException(e);
            }
            Set<String> keyNames;
            if (keySet != null && !keySet.isEmpty()) {
                keyNames = new HashSet<String>(keySet.size());
                for (Object o : keySet) {
                    keyNames.add(o.toString());
                }
            } else {
                keyNames = Collections.EMPTY_SET;
            }
            return keyNames;
        }
    
        public Enumeration getAttributeNames() {
            Set<String> keyNames = getKeyNames();
            final Iterator iterator = keyNames.iterator();
            return new Enumeration() {
                public boolean hasMoreElements() {
                    return iterator.hasNext();
                }
    
                public Object nextElement() {
                    return iterator.next();
                }
            };
        }
    
        public String[] getValueNames() {
            Set<String> keyNames = getKeyNames();
            String[] array = new String[keyNames.size()];
            if (keyNames.size() > 0) {
                array = keyNames.toArray(array);
            }
            return array;
        }
    
        protected void afterBound(String s, Object o) {
            if (o instanceof HttpSessionBindingListener) {
                HttpSessionBindingListener listener = (HttpSessionBindingListener) o;
                HttpSessionBindingEvent event = new HttpSessionBindingEvent(this, s, o);
                listener.valueBound(event);
            }
        }
    
        protected void afterUnbound(String s, Object o) {
            if (o instanceof HttpSessionBindingListener) {
                HttpSessionBindingListener listener = (HttpSessionBindingListener) o;
                HttpSessionBindingEvent event = new HttpSessionBindingEvent(this, s, o);
                listener.valueUnbound(event);
            }
        }
    
        public void setAttribute(String s, Object o) {
            try {
                getSession().setAttribute(s, o);
                afterBound(s, o);
            } catch (InvalidSessionException e) {
                //noinspection finally
                try {
                    afterUnbound(s, o);
                } finally {
                    //noinspection ThrowFromFinallyBlock
                    throw new IllegalStateException(e);
                }
            }
        }
    
        public void putValue(String s, Object o) {
            setAttribute(s, o);
        }
    
        public void removeAttribute(String s) {
            try {
                Object attribute = getSession().removeAttribute(s);
                afterUnbound(s, attribute);
            } catch (InvalidSessionException e) {
                throw new IllegalStateException(e);
            }
        }
    
        public void removeValue(String s) {
            removeAttribute(s);
        }
    
        public void invalidate() {
            try {
                getSession().stop();
            } catch (InvalidSessionException e) {
                throw new IllegalStateException(e);
            }
        }
    
        public boolean isNew() {
            Boolean value = (Boolean) currentRequest.getAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW);
            return value != null && value.equals(Boolean.TRUE);
        }
    }

     2. 获取

    1. org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 入口会创建Subject 

    2. 调用org.apache.shiro.session.mgt.DefaultSessionManager#retrieveSession :

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

    1》 调用到: org.apache.shiro.web.session.mgt.DefaultWebSessionManager#getReferencedSessionId   实际也是从cookie 中拿名字为JSESSIONID的值作为sessionId

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

    2》  retrieveSessionFromDataSource 调用到: org.apache.shiro.session.mgt.eis.MemorySessionDAO#doReadSession

        protected Session doReadSession(Serializable sessionId) {
            return sessions.get(sessionId);
        }

      实际就是从缓存Map 中获取数据。

    3. 如果获取的时候没获取到,那么session 为空, 如果获取之后就调用 org.apache.shiro.session.mgt.AbstractValidatingSessionManager#validate 进行验证

        protected void validate(Session session, SessionKey key) throws InvalidSessionException {
            try {
                doValidate(session);
            } catch (ExpiredSessionException ese) {
                onExpiration(session, ese, key);
                throw ese;
            } catch (InvalidSessionException ise) {
                onInvalidation(session, ise, key);
                throw ise;
            }
        }

    继续调用: org.apache.shiro.session.mgt.SimpleSession#validate

        public void validate() throws InvalidSessionException {
            //check for stopped:
            if (isStopped()) {
                //timestamp is set, so the session is considered stopped:
                String msg = "Session with id [" + getId() + "] has been " +
                        "explicitly stopped.  No further interaction under this session is " +
                        "allowed.";
                throw new StoppedSessionException(msg);
            }
    
            //check for expiration
            if (isTimedOut()) {
                expire();
    
                //throw an exception explaining details of why it expired:
                Date lastAccessTime = getLastAccessTime();
                long timeout = getTimeout();
    
                Serializable sessionId = getId();
    
                DateFormat df = DateFormat.getInstance();
                String msg = "Session with id [" + sessionId + "] has expired. " +
                        "Last access time: " + df.format(lastAccessTime) +
                        ".  Current time: " + df.format(new Date()) +
                        ".  Session timeout is set to " + timeout / MILLIS_PER_SECOND + " seconds (" +
                        timeout / MILLIS_PER_MINUTE + " minutes)";
                if (log.isTraceEnabled()) {
                    log.trace(msg);
                }
                throw new ExpiredSessionException(msg);
            }
        }

      可以看到session 的过期也是用每次访问固定续期,基于 org.apache.shiro.session.mgt.SimpleSession#lastAccessTime 与当前时间进行比多。

    4. 修改session 存放到redis

      在上面简单了解到Session 交给Shiro 管理之后,所有的操作都是通过SessionDAO 接口进行的,如果我们想redis 存到redis 中, 只需要重写一个SessionDAO, 其中 AbstractSessionDAO 作为一个抽象类,我们继承该类实现几个抽象方法即可。

    1. pom 引入:

            <dependency>
                <groupId>org.crazycake</groupId>
                <artifactId>shiro-redis</artifactId>
                <version>3.2.2</version>
            </dependency>

     2. 重新设置SessionManager

        @Bean
        public SessionManager sessionManager() {
            DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
            RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
            RedisManager redisManager = new RedisManager();
            redisManager.setHost("127.0.0.1:6379");
            redisSessionDAO.setRedisManager(redisManager);
            defaultWebSessionManager.setSessionDAO(redisSessionDAO);
            return  defaultWebSessionManager;
        }

    3. 测试: 访问后查看redis:

    127.0.0.1:6379> keys *
    1) "shiro:session:66745c77-625b-4c74-8799-2f9db8c8fe47"
    127.0.0.1:6379> type "shiro:session:66745c77-625b-4c74-8799-2f9db8c8fe47"
    string
    127.0.0.1:6379> get "shiro:session:66745c77-625b-4c74-8799-2f9db8c8fe47"
    "xacxedx00x05srx00*org.apache.shiro.session.mgt.SimpleSessionx9dx1cxa1xb8xd5x8cbnx03x00x00xpwx02x00xdbtx00$66745c77-625b-4c74-8799-2f9db8c8fe47srx00x0ejava.util.Datehjx81x01KYtx19x03x00x00xpwx00x00x01|xd6xd0x05x1bxqx00~x00x04wx19x00x00x00x00x00x1bw@x00x0f0:0:0:0:0:0:0:1srx00x11java.util.HashMapx05axdaxc1xc3x16`xd1x03x00x02Fx00
    loadFactorIx00	thresholdxp?@x00x00x00x00x00x0cwx00x00x00x10x00x00x00x03tx00x04testtx00x06value2tx00Porg.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEYsrx00x11java.lang.Booleanxcd rx80xd5x9cxfaxeex02x00x01Zx00x05valuexpx01tx00Morg.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEYsrx002org.apache.shiro.subject.SimplePrincipalCollectionxa8x7fX%xc6xa3Jx03x00x01Lx00x0frealmPrincipalstx00x0fLjava/util/Map;xpsrx00x17java.util.LinkedHashMap4xc0N\x10lxc0xfbx02x00x01Zx00x0baccessOrderxqx00~x00x05?@x00x00x00x00x00x0cwx00x00x00x10x00x00x00x01tx00$com.zd.bx.config.shiro.CustomRealm_0srx00x17java.util.LinkedHashSetxd8lxd7Zx95xdd*x1ex02x00x00xrx00x11java.util.HashSetxbaDx85x95x96xb8xb74x03x00x00xpwx0cx00x00x00x10?@x00x00x00x00x00x01srx00x18com.zd.bx.bean.user.Userxa19zxfeMxa09xb2x02x00x10Lx00aaddresstx00x12Ljava/lang/String;Lx00x0bdepartmentstx00x0fLjava/util/Set;Lx00
    dingUserIdqx00~x00x17Lx00x05emailqx00~x00x17Lx00fullnameqx00~x00x17Lx00numIndextx00x13Ljava/lang/Integer;Lx00passwordqx00~x00x17Lx00x05phoneqx00~x00x17Lx00x05rolesqx00~x00x18Lx00x1aselectDeptAndPositionNodesqx00~x00x18Lx00x03sexqx00~x00x17Lx00
    updatetimetx00x10Ljava/util/Date;Lx00	userblankqx00~x00x17Lx00usercodeqx00~x00x17Lx00usernameqx00~x00x17Lx00x0cweixinUserIdqx00~x00x17xrx00%com.zd.bx.bean.AbstractSequenceEntityxf6xa2xa9xec`xc5x01xb2x02x00x01Jx00x02idxrx00x1dcom.zd.bx.bean.AbstractEntityx03rxadxf5xbbx04xcaxa1x02x00x03Lx00
    createtimeqx00~x00x1aLx00acreatorqx00~x00x17Lx00
    uniqueCodeqx00~x00x17xpsqx00~x00x03wx00x00x01|xd6xd0x05 xtx00x00tx00$31f53619-75f9-4aa2-86ce-22b518319a74x00x00x00x00x00x00x00x00psqx00~x00x14wx0cx00x00x00x10?@x00x00x00x00x00x00xpppsrx00x11java.lang.Integerx12xe2xa0xa4xf7x81x878x02x00x01Ix00x05valuexrx00x10java.lang.Numberx86xacx95x1dx0bx94xe0x8bx02x00x00xpx00x00x03xe7tx00x06111222psqx00~x00x14wx0cx00x00x00x10?@x00x00x00x00x00x00xsqx00~x00x14wx0cx00x00x00x10?@x00x00x00x00x00x00xppppppxxx00wx01x01qx00~x00x11xxx"

    4. 源码查看:

    (1) org.crazycake.shiro.RedisManager:

    package org.crazycake.shiro;
    
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.Protocol;
    
    public class RedisManager extends WorkAloneRedisManager implements IRedisManager {
    
        private static final String DEFAULT_HOST = "127.0.0.1:6379";
        private String host = DEFAULT_HOST;
    
        // timeout for jedis try to connect to redis server, not expire time! In milliseconds
        private int timeout = Protocol.DEFAULT_TIMEOUT;
    
        private String password;
    
        private int database = Protocol.DEFAULT_DATABASE;
    
        private JedisPool jedisPool;
    
        private void init() {
            synchronized (this) {
                if (jedisPool == null) {
                    String[] hostAndPort = host.split(":");
                    jedisPool = new JedisPool(getJedisPoolConfig(), hostAndPort[0], Integer.parseInt(hostAndPort[1]), timeout, password, database);
                }
            }
        }
    
        @Override
        protected Jedis getJedis() {
            if (jedisPool == null) {
                init();
            }
            return jedisPool.getResource();
        }
    
        public String getHost() {
            return host;
        }
    
        public void setHost(String host) {
            this.host = host;
        }
    
        public int getTimeout() {
            return timeout;
        }
    
        public void setTimeout(int timeout) {
            this.timeout = timeout;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public int getDatabase() {
            return database;
        }
    
        public void setDatabase(int database) {
            this.database = database;
        }
    
        public JedisPool getJedisPool() {
            return jedisPool;
        }
    
        public void setJedisPool(JedisPool jedisPool) {
            this.jedisPool = jedisPool;
        }
    }
    View Code

    (2) org.crazycake.shiro.RedisSessionDAO

    package org.crazycake.shiro;
    
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
    import org.crazycake.shiro.exception.SerializationException;
    import org.crazycake.shiro.serializer.ObjectSerializer;
    import org.crazycake.shiro.serializer.RedisSerializer;
    import org.crazycake.shiro.serializer.StringSerializer;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.io.Serializable;
    import java.util.*;
    
    public class RedisSessionDAO extends AbstractSessionDAO {
    
        private static Logger logger = LoggerFactory.getLogger(RedisSessionDAO.class);
    
        private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:";
        private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX;
    
        private static final long DEFAULT_SESSION_IN_MEMORY_TIMEOUT = 1000L;
        /**
         * doReadSession be called about 10 times when login.
         * Save Session in ThreadLocal to resolve this problem. sessionInMemoryTimeout is expiration of Session in ThreadLocal.
         * The default value is 1000 milliseconds (1s).
         * Most of time, you don't need to change it.
         */
        private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT;
    
        private static final boolean DEFAULT_SESSION_IN_MEMORY_ENABLED = true;
    
        private boolean sessionInMemoryEnabled = DEFAULT_SESSION_IN_MEMORY_ENABLED;
    
        // expire time in seconds
        private static final int DEFAULT_EXPIRE = -2;
        private static final int NO_EXPIRE = -1;
    
        /**
         * Please make sure expire is longer than sesion.getTimeout()
         */
        private int expire = DEFAULT_EXPIRE;
    
        private static final int MILLISECONDS_IN_A_SECOND = 1000;
    
        private IRedisManager redisManager;
        private RedisSerializer keySerializer = new StringSerializer();
        private RedisSerializer valueSerializer = new ObjectSerializer();
        private static ThreadLocal sessionsInThread = new ThreadLocal();
        
        @Override
        public void update(Session session) throws UnknownSessionException {
            this.saveSession(session);
            if (this.sessionInMemoryEnabled) {
                this.setSessionToThreadLocal(session.getId(), session);
            }
        }
        
        /**
         * save session
         * @param session
         * @throws UnknownSessionException
         */
        private void saveSession(Session session) throws UnknownSessionException {
            if (session == null || session.getId() == null) {
                logger.error("session or session id is null");
                throw new UnknownSessionException("session or session id is null");
            }
            byte[] key;
            byte[] value;
            try {
                key = keySerializer.serialize(getRedisSessionKey(session.getId()));
                value = valueSerializer.serialize(session);
            } catch (SerializationException e) {
                logger.error("serialize session error. session id=" + session.getId());
                throw new UnknownSessionException(e);
            }
            if (expire == DEFAULT_EXPIRE) {
                this.redisManager.set(key, value, (int) (session.getTimeout() / MILLISECONDS_IN_A_SECOND));
                return;
            }
            if (expire != NO_EXPIRE && expire * MILLISECONDS_IN_A_SECOND < session.getTimeout()) {
                logger.warn("Redis session expire time: "
                        + (expire * MILLISECONDS_IN_A_SECOND)
                        + " is less than Session timeout: "
                        + session.getTimeout()
                        + " . It may cause some problems.");
            }
            this.redisManager.set(key, value, expire);
        }
    
        @Override
        public void delete(Session session) {
            if (session == null || session.getId() == null) {
                logger.error("session or session id is null");
                return;
            }
            try {
                redisManager.del(keySerializer.serialize(getRedisSessionKey(session.getId())));
            } catch (SerializationException e) {
                logger.error("delete session error. session id=" + session.getId());
            }
        }
    
        @Override
        public Collection<Session> getActiveSessions() {
            Set<Session> sessions = new HashSet<Session>();
            try {
                Set<byte[]> keys = redisManager.keys(this.keySerializer.serialize(this.keyPrefix + "*"));
                if (keys != null && keys.size() > 0) {
                    for (byte[] key:keys) {
                        Session s = (Session) valueSerializer.deserialize(redisManager.get(key));
                        sessions.add(s);
                    }
                }
            } catch (SerializationException e) {
                logger.error("get active sessions error.");
            }
            return sessions;
        }
    
        @Override
        protected Serializable doCreate(Session session) {
            if (session == null) {
                logger.error("session is null");
                throw new UnknownSessionException("session is null");
            }
            Serializable sessionId = this.generateSessionId(session);  
            this.assignSessionId(session, sessionId);
            this.saveSession(session);
            return sessionId;
        }
    
        @Override
        protected Session doReadSession(Serializable sessionId) {
            if (sessionId == null) {
                logger.warn("session id is null");
                return null;
            }
                if (this.sessionInMemoryEnabled) {
                Session session = getSessionFromThreadLocal(sessionId);
                if (session != null) {
                    return session;
                }
            }
    
            Session session = null;
            logger.debug("read session from redis");
            try {
                session = (Session) valueSerializer.deserialize(redisManager.get(keySerializer.serialize(getRedisSessionKey(sessionId))));
                if (this.sessionInMemoryEnabled) {
                    setSessionToThreadLocal(sessionId, session);
                }
            } catch (SerializationException e) {
                logger.error("read session error. settionId=" + sessionId);
            }
            return session;
        }
    
        private void setSessionToThreadLocal(Serializable sessionId, Session s) {
            Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
            if (sessionMap == null) {
                sessionMap = new HashMap<Serializable, SessionInMemory>();
                sessionsInThread.set(sessionMap);
            }
            SessionInMemory sessionInMemory = new SessionInMemory();
            sessionInMemory.setCreateTime(new Date());
            sessionInMemory.setSession(s);
            sessionMap.put(sessionId, sessionInMemory);
        }
    
        private Session getSessionFromThreadLocal(Serializable sessionId) {
            Session s = null;
    
            if (sessionsInThread.get() == null) {
                return null;
            }
    
            Map<Serializable, SessionInMemory> sessionMap = (Map<Serializable, SessionInMemory>) sessionsInThread.get();
            SessionInMemory sessionInMemory = sessionMap.get(sessionId);
            if (sessionInMemory == null) {
                return null;
            }
            Date now = new Date();
            long duration = now.getTime() - sessionInMemory.getCreateTime().getTime();
            if (duration < sessionInMemoryTimeout) {
                s = sessionInMemory.getSession();
                logger.debug("read session from memory");
            } else {
                sessionMap.remove(sessionId);
            }
    
            return s;
        }
    
        private String getRedisSessionKey(Serializable sessionId) {
            return this.keyPrefix + sessionId;
        }
    
        public IRedisManager getRedisManager() {
            return redisManager;
        }
    
        public void setRedisManager(IRedisManager redisManager) {
            this.redisManager = redisManager;
        }
    
        public String getKeyPrefix() {
            return keyPrefix;
        }
    
        public void setKeyPrefix(String keyPrefix) {
            this.keyPrefix = keyPrefix;
        }
    
        public RedisSerializer getKeySerializer() {
            return keySerializer;
        }
    
        public void setKeySerializer(RedisSerializer keySerializer) {
            this.keySerializer = keySerializer;
        }
    
        public RedisSerializer getValueSerializer() {
            return valueSerializer;
        }
    
        public void setValueSerializer(RedisSerializer valueSerializer) {
            this.valueSerializer = valueSerializer;
        }
    
        public long getSessionInMemoryTimeout() {
            return sessionInMemoryTimeout;
        }
    
        public void setSessionInMemoryTimeout(long sessionInMemoryTimeout) {
            this.sessionInMemoryTimeout = sessionInMemoryTimeout;
        }
    
        public int getExpire() {
            return expire;
        }
    
        public void setExpire(int expire) {
            this.expire = expire;
        }
    
        public boolean getSessionInMemoryEnabled() {
            return sessionInMemoryEnabled;
        }
    
        public void setSessionInMemoryEnabled(boolean sessionInMemoryEnabled) {
            this.sessionInMemoryEnabled = sessionInMemoryEnabled;
        }
    }
    View Code

    5. 前后端分离项目session标识从header中获取 

      有的时候我们可能在做前后端分离项目时,生成的token信息可能从head中传递。这时候我们可以复用这一套流程。也就是修改session获取方式。

      原来获取是从org.apache.shiro.web.session.mgt.DefaultWebSessionManager#getSessionId(javax.servlet.ServletRequest, javax.servlet.ServletResponse) 为入口,从cookie 中获取key 为JSESSION的值。所以如果想从自己的header 获取,重写该方法即可。这样还可以复用shiro的逻辑。

      登录成功之后前端每次请求在自己的请求头携带名称为mytoken的header,值为登录成功时的SESSIONID。

    package com.zd.bx.config.shiro;
    
    import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import java.io.Serializable;
    
    public class MySessionManager extends DefaultWebSessionManager {
    
        @Override
        protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
            HttpServletRequest request1 = (HttpServletRequest) request;
            String mytoken = request1.getHeader("mytoken");
            System.out.println(mytoken);
            return mytoken;
        }
    }

      然后切换sessionManager 为上面Manager 即可。

    补充: 如果session 交给session 管理,可以看到其失效时间也是根据最后一次访问时间递推。

    修改lastAccessTime 是在:org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 入口调用org.apache.shiro.web.servlet.AbstractShiroFilter#updateSessionLastAccessTime:

        protected void updateSessionLastAccessTime(ServletRequest request, ServletResponse response) {
            if (!isHttpSessions()) { //'native' sessions
                Subject subject = SecurityUtils.getSubject();
                //Subject should never _ever_ be null, but just in case:
                if (subject != null) {
                    Session session = subject.getSession(false);
                    if (session != null) {
                        try {
                            session.touch();
                        } catch (Throwable t) {
                            log.error("session.touch() method invocation has failed.  Unable to update " +
                                    "the corresponding session's last access time based on the incoming request.", t);
                        }
                    }
                }
            }
        }

    在 !isHttpSessions  的条件下(该条件下session 由shiro 管理), 调用: org.apache.shiro.session.mgt.SimpleSession#touch

        public void touch() {
            this.lastAccessTime = new Date();
        }

    补充: shiro 管理session 后相关相关属性存放原理

    1. 如下方法:

    session1.setAttribute("test", "value2");

    调用到: org.apache.shiro.web.servlet.ShiroHttpSession#setAttribute

        public void setAttribute(String s, Object o) {
            try {
                getSession().setAttribute(s, o);
                afterBound(s, o);
            } catch (InvalidSessionException e) {
                //noinspection finally
                try {
                    afterUnbound(s, o);
                } finally {
                    //noinspection ThrowFromFinallyBlock
                    throw new IllegalStateException(e);
                }
            }
        }

    继续调用: org.apache.shiro.session.ProxiedSession#setAttribute

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

    继续调用:org.apache.shiro.session.mgt.DelegatingSession#setAttribute

        public void setAttribute(Object attributeKey, Object value) throws InvalidSessionException {
            if (value == null) {
                removeAttribute(attributeKey);
            } else {
                sessionManager.setAttribute(this.key, attributeKey, value);
            }
        }

    继续调用: org.apache.shiro.session.mgt.AbstractNativeSessionManager#setAttribute

        public void setAttribute(SessionKey sessionKey, Object attributeKey, Object value) throws InvalidSessionException {
            if (value == null) {
                removeAttribute(sessionKey, attributeKey);
            } else {
                Session s = lookupRequiredSession(sessionKey);
                s.setAttribute(attributeKey, value);
                onChange(s);
            }
        }

    继续调用: org.apache.shiro.session.mgt.SimpleSession#setAttribute (也就是最终相关属性存放的是在一个Map中)

        private transient Map<Object, Object> attributes;
    
        public void setAttribute(Object key, Object value) {
            if (value == null) {
                removeAttribute(key);
            } else {
                getAttributesLazy().put(key, value);
            }
        }
    
        private Map<Object, Object> getAttributesLazy() {
            Map<Object, Object> attributes = getAttributes();
            if (attributes == null) {
                attributes = new HashMap<Object, Object>();
                setAttributes(attributes);
            }
            return attributes;
        }

    2. 最终经过上面调用放到了org.apache.shiro.session.mgt.SimpleSession#attributes 属性中。 org.apache.shiro.session.mgt.SimpleSession 会存放在内存中, 所以每个 JSESSIONID 对应的Session 放的东西不会丢失。

    【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】
  • 相关阅读:
    程序调试的利器GDB
    Building a Android Development Environment
    手机的串号IMEI/ESN标示位置图解摩托罗拉官方教程
    Linux调试信息输出串口设备号的设置
    git忽略文件提交
    Spring @Transactional事务传播范围以及隔离级别
    自定义springbootstarter
    maven 命令将jar包安装到本地仓库
    oracle 常用命令
    Oracle 用户(user)和模式(schema)的区别
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/15489672.html
Copyright © 2020-2023  润新知