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"
"\xac\xed\x00\x05sr\x00*org.apache.shiro.session.mgt.SimpleSession\x9d\x1c\xa1\xb8\xd5\x8cbn\x03\x00\x00xpw\x02\x00\xdbt\x00$66745c77-625b-4c74-8799-2f9db8c8fe47sr\x00\x0ejava.util.Datehj\x81\x01KYt\x19\x03\x00\x00xpw\b\x00\x00\x01|\xd6\xd0\x05\x1bxq\x00~\x00\x04w\x19\x00\x00\x00\x00\x00\x1bw@\x00\x0f0:0:0:0:0:0:0:1sr\x00\x11java.util.HashMap\x05\a\xda\xc1\xc3\x16`\xd1\x03\x00\x02F\x00\nloadFactorI\x00\tthresholdxp?@\x00\x00\x00\x00\x00\x0cw\b\x00\x00\x00\x10\x00\x00\x00\x03t\x00\x04testt\x00\x06value2t\x00Porg.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEYsr\x00\x11java.lang.Boolean\xcd r\x80\xd5\x9c\xfa\xee\x02\x00\x01Z\x00\x05valuexp\x01t\x00Morg.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEYsr\x002org.apache.shiro.subject.SimplePrincipalCollection\xa8\x7fX%\xc6\xa3\bJ\x03\x00\x01L\x00\x0frealmPrincipalst\x00\x0fLjava/util/Map;xpsr\x00\x17java.util.LinkedHashMap4\xc0N\\\x10l\xc0\xfb\x02\x00\x01Z\x00\x0baccessOrderxq\x00~\x00\x05?@\x00\x00\x00\x00\x00\x0cw\b\x00\x00\x00\x10\x00\x00\x00\x01t\x00$com.zd.bx.config.shiro.CustomRealm_0sr\x00\x17java.util.LinkedHashSet\xd8l\xd7Z\x95\xdd*\x1e\x02\x00\x00xr\x00\x11java.util.HashSet\xbaD\x85\x95\x96\xb8\xb74\x03\x00\x00xpw\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x01sr\x00\x18com.zd.bx.bean.user.User\xa19z\xfeM\xa09\xb2\x02\x00\x10L\x00\aaddresst\x00\x12Ljava/lang/String;L\x00\x0bdepartmentst\x00\x0fLjava/util/Set;L\x00\ndingUserIdq\x00~\x00\x17L\x00\x05emailq\x00~\x00\x17L\x00\bfullnameq\x00~\x00\x17L\x00\bnumIndext\x00\x13Ljava/lang/Integer;L\x00\bpasswordq\x00~\x00\x17L\x00\x05phoneq\x00~\x00\x17L\x00\x05rolesq\x00~\x00\x18L\x00\x1aselectDeptAndPositionNodesq\x00~\x00\x18L\x00\x03sexq\x00~\x00\x17L\x00\nupdatetimet\x00\x10Ljava/util/Date;L\x00\tuserblankq\x00~\x00\x17L\x00\busercodeq\x00~\x00\x17L\x00\busernameq\x00~\x00\x17L\x00\x0cweixinUserIdq\x00~\x00\x17xr\x00%com.zd.bx.bean.AbstractSequenceEntity\xf6\xa2\xa9\xec`\xc5\x01\xb2\x02\x00\x01J\x00\x02idxr\x00\x1dcom.zd.bx.bean.AbstractEntity\x03r\xad\xf5\xbb\x04\xca\xa1\x02\x00\x03L\x00\ncreatetimeq\x00~\x00\x1aL\x00\acreatorq\x00~\x00\x17L\x00\nuniqueCodeq\x00~\x00\x17xpsq\x00~\x00\x03w\b\x00\x00\x01|\xd6\xd0\x05 xt\x00\x00t\x00$31f53619-75f9-4aa2-86ce-22b518319a74\x00\x00\x00\x00\x00\x00\x00\x00psq\x00~\x00\x14w\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x00xpppsr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x03\xe7t\x00\x06111222psq\x00~\x00\x14w\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x00xsq\x00~\x00\x14w\x0c\x00\x00\x00\x10?@\x00\x00\x00\x00\x00\x00xppppppxx\x00w\x01\x01q\x00~\x00\x11xxx"

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 放的东西不会丢失。

 

posted @ 2021-10-31 22:53  QiaoZhi  阅读(1420)  评论(0编辑  收藏  举报