2018-02-17

作者: 紫霞等了至尊宝五百年 | 来源:发表于2018-02-18 02:27 被阅读29次

    最近要在项目中做用户踢线的功能,由于项目使用spring session来管理用户session,因此特地翻了翻spring session的源码,看看spring session是如何管理的。我们使用redis来存储session,因此本文只对session在redis中的存储结构以及管理做解析。

    1 spring session使用
    Spring Session对HTTP的支持是通过标准的servlet filter来实现的,这个filter必须要配置为拦截所有的web应用请求,并且它最好是filter链中的第一个filter。Spring Session filter会确保随后调用javax.servlet.http.HttpServletRequest的getSession()方法时,都会返回Spring Session的HttpSession实例,而不是应用服务器默认的HttpSession。

    spring session通过注解@EnableRedisHttpSession或者xml配置

    <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
    来设置spring session的一些参数,比如session的最大活跃时间(maxInactiveIntervalInSeconds),redis命名空间(redisNamespace),session写入到redis的时机(FlushMode)以及如何序列化写到redis中的session value等等。

    要想使用spring session,还需要创建名为springSessionRepositoryFilter的SessionRepositoryFilter类。该类实现了Sevlet Filter接口,当请求穿越sevlet filter链时应该首先经过springSessionRepositoryFilter,这样在后面获取session的时候,得到的将是spring session。为了springSessonRepositoryFilter作为filter链中的第一个,spring session提供了AbstractHttpSessionApplicationInitializer类, 它实现了WebApplicationInitializer类,在onStartup方法中将springSessionRepositoryFilter加入到其他fitler链前面。

    public abstract class AbstractHttpSessionApplicationInitializer
            implements WebApplicationInitializer {
        /**
         * The default name for Spring Session's repository filter.
         */
        public static final String DEFAULT_FILTER_NAME = "springSessionRepositoryFilter";
        public void onStartup(ServletContext servletContext) throws ServletException {
                ......
            insertSessionRepositoryFilter(servletContext);
            afterSessionRepositoryFilter(servletContext);
        }
        /**
         * Registers the springSessionRepositoryFilter.
         * @param servletContext the {@link ServletContext}
         */
        private void insertSessionRepositoryFilter(ServletContext servletContext) {
            String filterName = DEFAULT_FILTER_NAME;
                 
            DelegatingFilterProxy springSessionRepositoryFilter = new DelegatingFilterProxy(
                    filterName);
            String contextAttribute = getWebApplicationContextAttribute();
            if (contextAttribute != null) {
                springSessionRepositoryFilter.setContextAttribute(contextAttribute);
            }
            registerFilter(servletContext, true, filterName, springSessionRepositoryFilter);
        }
    }
    

    或者也可以在web.xml里面将springSessionRepositoryFilter加入到filter配置的第一个
    该filter最终会把请求代理给具体的一个filter,通过入参的常量可看出它是委派给springSessionRepositoryFilter这样一个具体的filter(由spring容器管理)



    DelegatingFilterProxy.png

    查看其父类

    public abstract class GenericFilterBean implements
            Filter, BeanNameAware, EnvironmentAware, ServletContextAware, InitializingBean, DisposableBean {
    
        /** Logger available to subclasses */
        protected final Log logger = LogFactory.getLog(getClass());
    
        /**
         * Set of required properties (Strings) that must be supplied as
         * config parameters to this filter.
         */
        private final Set<String> requiredProperties = new HashSet<String>();
    
        private FilterConfig filterConfig;
    
        private String beanName;
    
        private Environment environment = new StandardServletEnvironment();
    
        private ServletContext servletContext;
    
        /**
         * Calls the {@code initFilterBean()} method that might
         * contain custom initialization of a subclass.
         * <p>Only relevant in case of initialization as bean, where the
         * standard {@code init(FilterConfig)} method won't be called.
         * @see #initFilterBean()
         * @see #init(javax.servlet.FilterConfig)
         */
        @Override
        public void afterPropertiesSet() throws ServletException {
            initFilterBean();
        }
    
    
        /**
         * Subclasses can invoke this method to specify that this property
         * (which must match a JavaBean property they expose) is mandatory,
         * and must be supplied as a config parameter. This should be called
         * from the constructor of a subclass.
         * <p>This method is only relevant in case of traditional initialization
         * driven by a FilterConfig instance.
         * @param property name of the required property
         */
        protected final void addRequiredProperty(String property) {
            this.requiredProperties.add(property);
        }
    
        /**
         * Standard way of initializing this filter.
         * Map config parameters onto bean properties of this filter, and
         * invoke subclass initialization.
         * @param filterConfig the configuration for this filter
         * @throws ServletException if bean properties are invalid (or required
         * properties are missing), or if subclass initialization fails.
         * @see #initFilterBean
         */
        @Override
        public final void init(FilterConfig filterConfig) throws ServletException {
            Assert.notNull(filterConfig, "FilterConfig must not be null");
            if (logger.isDebugEnabled()) {
                logger.debug("Initializing filter '" + filterConfig.getFilterName() + "'");
            }
    
            this.filterConfig = filterConfig;
    
            // Set bean properties from init parameters.
            try {
                PropertyValues pvs = new FilterConfigPropertyValues(filterConfig, this.requiredProperties);
                BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
                ResourceLoader resourceLoader = new ServletContextResourceLoader(filterConfig.getServletContext());
                bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));
                initBeanWrapper(bw);
                bw.setPropertyValues(pvs, true);
            }
            catch (BeansException ex) {
                String msg = "Failed to set bean properties on filter '" +
                    filterConfig.getFilterName() + "': " + ex.getMessage();
                logger.error(msg, ex);
                throw new NestedServletException(msg, ex);
            }
    
            // Let subclasses do whatever initialization they like.
            //初始化filter bean
            initFilterBean();
    
            if (logger.isDebugEnabled()) {
                logger.debug("Filter '" + filterConfig.getFilterName() + "' configured successfully");
            }
        }
    
        /**
         * Initialize the BeanWrapper for this GenericFilterBean,
         * possibly with custom editors.
         * <p>This default implementation is empty.
         * @param bw the BeanWrapper to initialize
         * @throws BeansException if thrown by BeanWrapper methods
         * @see org.springframework.beans.BeanWrapper#registerCustomEditor
         */
        protected void initBeanWrapper(BeanWrapper bw) throws BeansException {
        }
    
    
        /**
         * Make the FilterConfig of this filter available, if any.
         * Analogous to GenericServlet's {@code getServletConfig()}.
         * <p>Public to resemble the {@code getFilterConfig()} method
         * of the Servlet Filter version that shipped with WebLogic 6.1.
         * @return the FilterConfig instance, or {@code null} if none available
         * @see javax.servlet.GenericServlet#getServletConfig()
         */
        public final FilterConfig getFilterConfig() {
            return this.filterConfig;
        }
    
        /**
         * Make the name of this filter available to subclasses.
         * Analogous to GenericServlet's {@code getServletName()}.
         * <p>Takes the FilterConfig's filter name by default.
         * If initialized as bean in a Spring application context,
         * it falls back to the bean name as defined in the bean factory.
         * @return the filter name, or {@code null} if none available
         * @see javax.servlet.GenericServlet#getServletName()
         * @see javax.servlet.FilterConfig#getFilterName()
         * @see #setBeanName
         */
        protected final String getFilterName() {
            return (this.filterConfig != null ? this.filterConfig.getFilterName() : this.beanName);
        }
    
        /**
         * Make the ServletContext of this filter available to subclasses.
         * Analogous to GenericServlet's {@code getServletContext()}.
         * <p>Takes the FilterConfig's ServletContext by default.
         * If initialized as bean in a Spring application context,
         * it falls back to the ServletContext that the bean factory runs in.
         * @return the ServletContext instance, or {@code null} if none available
         * @see javax.servlet.GenericServlet#getServletContext()
         * @see javax.servlet.FilterConfig#getServletContext()
         * @see #setServletContext
         */
        protected final ServletContext getServletContext() {
            return (this.filterConfig != null ? this.filterConfig.getServletContext() : this.servletContext);
        }
    
    
        /**
         * Subclasses may override this to perform custom initialization.
         * All bean properties of this filter will have been set before this
         * method is invoked.
         */
        protected void initFilterBean() throws ServletException {
        }
    
        /**
         * Subclasses may override this to perform custom filter shutdown.
         * <p>Note: This method will be called from standard filter destruction
         * as well as filter bean destruction in a Spring application context.
         * <p>This default implementation is empty.
         */
        @Override
        public void destroy() {
        }
    
    
        /**
         * PropertyValues implementation created from FilterConfig init parameters.
         */
        @SuppressWarnings("serial")
        private static class FilterConfigPropertyValues extends MutablePropertyValues {
    
            /**
             * Create new FilterConfigPropertyValues.
             */
            public FilterConfigPropertyValues(FilterConfig config, Set<String> requiredProperties)
                throws ServletException {
    
                Set<String> missingProps = (requiredProperties != null && !requiredProperties.isEmpty()) ?
                        new HashSet<String>(requiredProperties) : null;
    
                Enumeration<?> en = config.getInitParameterNames();
                while (en.hasMoreElements()) {
                    String property = (String) en.nextElement();
                    Object value = config.getInitParameter(property);
                    addPropertyValue(new PropertyValue(property, value));
                    if (missingProps != null) {
                        missingProps.remove(property);
                    }
                }
    
                // Fail if we are still missing properties.
                if (missingProps != null && missingProps.size() > 0) {
                    throw new ServletException(
                        "Initialization from FilterConfig for filter '" + config.getFilterName() +
                        "' failed; the following required properties were missing: " +
                        StringUtils.collectionToDelimitedString(missingProps, ", "));
                }
            }
        }
    
    }
    
    

    查看其真正实现方法的子类

    public class DelegatingFilterProxy extends GenericFilterBean {
    
        private String contextAttribute;
    
        private WebApplicationContext webApplicationContext;
    
        private String targetBeanName;
    
        private boolean targetFilterLifecycle = false;
    
        private volatile Filter delegate;
    
        private final Object delegateMonitor = new Object();
    
        public DelegatingFilterProxy() {
        }
    
        public DelegatingFilterProxy(Filter delegate) {
            Assert.notNull(delegate, "delegate Filter object must not be null");
            this.delegate = delegate;
        }
    
        public DelegatingFilterProxy(String targetBeanName) {
            this(targetBeanName, null);
        }
    
        public DelegatingFilterProxy(String targetBeanName, WebApplicationContext wac) {
            Assert.hasText(targetBeanName, "target Filter bean name must not be null or empty");
            this.setTargetBeanName(targetBeanName);
            this.webApplicationContext = wac;
            if (wac != null) {
                this.setEnvironment(wac.getEnvironment());
            }
        }
        
        protected boolean isTargetFilterLifecycle() {
            return this.targetFilterLifecycle;
        }
    
        @Override
        protected void initFilterBean() throws ServletException {
            //同步块,防止spring容器启动时委托的这些filter保证它们的执行顺序
            synchronized (this.delegateMonitor) {
                if (this.delegate == null) {
                    // If no target bean name specified, use filter name.
                    if (this.targetBeanName == null) {
                        this.targetBeanName = getFilterName();
                    }
                    // Fetch Spring root application context and initialize the delegate early,
                    // if possible. If the root application context will be started after this
                    // filter proxy, we'll have to resort to lazy initialization.
                    WebApplicationContext wac = findWebApplicationContext();
                    if (wac != null) {
                        this.delegate = initDelegate(wac);
                    }
                }
            }
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
    
            // Lazily initialize the delegate if necessary.
            Filter delegateToUse = this.delegate;
            if (delegateToUse == null) {
                synchronized (this.delegateMonitor) {
                    if (this.delegate == null) {
                        WebApplicationContext wac = findWebApplicationContext();
                        if (wac == null) {
                            throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?");
                        }
                        this.delegate = initDelegate(wac);
                    }
                    delegateToUse = this.delegate;
                }
            }
    
            // Let the delegate perform the actual doFilter operation.
            invokeDelegate(delegateToUse, request, response, filterChain);
        }
    
        @Override
        public void destroy() {
            Filter delegateToUse = this.delegate;
            if (delegateToUse != null) {
                destroyDelegate(delegateToUse);
            }
        }
    
        protected WebApplicationContext findWebApplicationContext() {
            if (this.webApplicationContext != null) {
                // the user has injected a context at construction time -> use it
                if (this.webApplicationContext instanceof ConfigurableApplicationContext) {
                    if (!((ConfigurableApplicationContext)this.webApplicationContext).isActive()) {
                        // the context has not yet been refreshed -> do so before returning it
                        ((ConfigurableApplicationContext)this.webApplicationContext).refresh();
                    }
                }
                return this.webApplicationContext;
            }
            String attrName = getContextAttribute();
            if (attrName != null) {
                return WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
            }
            else {
                return WebApplicationContextUtils.getWebApplicationContext(getServletContext());
            }
        }
    
        protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
            Filter delegate = wac.getBean(getTargetBeanName(), Filter.class);
            if (isTargetFilterLifecycle()) {
                delegate.init(getFilterConfig());
            }
            return delegate;
        }
    
        /**
         * Actually invoke the delegate Filter with the given request and response.
         * 调用了委托的doFilter方法
         */
        protected void invokeDelegate(
                Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
    
            delegate.doFilter(request, response, filterChain);
        }
    
        /**
         * Destroy the Filter delegate.
         * Default implementation simply calls {@code Filter.destroy} on it.
         */
        protected void destroyDelegate(Filter delegate) {
            if (isTargetFilterLifecycle()) {
                delegate.destroy();
            }
        }
    }
    
    

    查看doFilter真正实现类(session包下)
    每次只filter一次

    abstract class OncePerRequestFilter implements Filter {
        /**
         * Suffix that gets appended to the filter name for the "already filtered" request
         * attribute.
         */
        public static final String ALREADY_FILTERED_SUFFIX = ".FILTERED";
    
        private String alreadyFilteredAttributeName = getClass().getName()
                .concat(ALREADY_FILTERED_SUFFIX);
    
        /**
         * This {@code doFilter} implementation stores a request attribute for
         * "already filtered", proceeding without filtering again if the attribute is already
         * there.
         */
        public final void doFilter(ServletRequest request, ServletResponse response,
                FilterChain filterChain) throws ServletException, IOException {
    
            if (!(request instanceof HttpServletRequest)
                    || !(response instanceof HttpServletResponse)) {
                throw new ServletException(
                        "OncePerRequestFilter just supports HTTP requests");
            }
            HttpServletRequest httpRequest = (HttpServletRequest) request;
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            //判断是否已经被Filter
            boolean hasAlreadyFilteredAttribute = request
                    .getAttribute(this.alreadyFilteredAttributeName) != null;
    
            if (hasAlreadyFilteredAttribute) {
    
                // Proceed without invoking this filter...
                filterChain.doFilter(request, response);
            }
            else {
                
                // Do invoke this filter...
                //确实调用此filter
                request.setAttribute(this.alreadyFilteredAttributeName, Boolean.TRUE);
                try {
                    //跳至下面的抽象方法
                    doFilterInternal(httpRequest, httpResponse, filterChain);
                }
                finally {
                    // Remove the "already filtered" request attribute for this request.
                    request.removeAttribute(this.alreadyFilteredAttributeName);
                }
            }
        }
    
        /**
         * Same contract as for {@code doFilter}, but guaranteed to be just invoked once per
         * request within a single request thread.
         * <p>
         * Provides HttpServletRequest and HttpServletResponse arguments instead of the
         * default ServletRequest and ServletResponse ones.
         */
         //唯一实现子类:SessionRepositoryFilter!!!
        protected abstract void doFilterInternal(HttpServletRequest request,
                HttpServletResponse response, FilterChain filterChain)
                        throws ServletException, IOException;
    
        public void init(FilterConfig config) {
        }
    
        public void destroy() {
        }
    }
    
    

    这个类应该是Spring-session里最关键的Bean了,他是一个Filter,他的作用就是封装HttpServietRequest,HttpServletResponse,改变其获取Session的行为,原始的获取Session方式是从服务器容器内获取,而SessionRepositoryFilter将其改变为从其他地方获取,比如从整合的Redis内,当不存在Session时,创建一个封装过的Session,设置到Redis中,同时将此Session关联的Cookie注入到返回结果中,可看其内部的Request和Session的包装类:


    SessionRepositoryFilter.png
    @Order(SessionRepositoryFilter.DEFAULT_ORDER)
    public class SessionRepositoryFilter<S extends ExpiringSession>
            extends OncePerRequestFilter {
        private static final String SESSION_LOGGER_NAME = SessionRepositoryFilter.class
                .getName().concat(".SESSION_LOGGER");
    
        private static final Log SESSION_LOGGER = LogFactory.getLog(SESSION_LOGGER_NAME);
    
        /**
         * The session repository request attribute name.
         */
        public static final String SESSION_REPOSITORY_ATTR = SessionRepository.class
                .getName();
    
        /**
         * Invalid session id (not backed by the session repository) request attribute name.
         */
        public static final String INVALID_SESSION_ID_ATTR = SESSION_REPOSITORY_ATTR
                + ".invalidSessionId";
    
        /**
         * The default filter order.
         */
        public static final int DEFAULT_ORDER = Integer.MIN_VALUE + 50;
    
        private final SessionRepository<S> sessionRepository;
    
        private ServletContext servletContext;
    
        private MultiHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();
    
        /**
         * Creates a new instance.
         */
        public SessionRepositoryFilter(SessionRepository<S> sessionRepository) {
            if (sessionRepository == null) {
                throw new IllegalArgumentException("sessionRepository cannot be null");
            }
            this.sessionRepository = sessionRepository;
        }
    
        /**
         * Sets the {@link HttpSessionStrategy} to be used.
         */
        public void setHttpSessionStrategy(HttpSessionStrategy httpSessionStrategy) {
            if (httpSessionStrategy == null) {
                throw new IllegalArgumentException("httpSessionStrategy cannot be null");
            }
            this.httpSessionStrategy = new MultiHttpSessionStrategyAdapter(
                    httpSessionStrategy);
        }
    
        /**
         * Sets the {@link MultiHttpSessionStrategy} to be used.
         */
        public void setHttpSessionStrategy(MultiHttpSessionStrategy httpSessionStrategy) {
            if (httpSessionStrategy == null) {
                throw new IllegalArgumentException("httpSessionStrategy cannot be null");
            }
            this.httpSessionStrategy = httpSessionStrategy;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request,
                HttpServletResponse response, FilterChain filterChain)
                        throws ServletException, IOException {
            request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
    
            SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
                    request, response, this.servletContext);
            SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(
                    wrappedRequest, response);
    
            HttpServletRequest strategyRequest = this.httpSessionStrategy
                    .wrapRequest(wrappedRequest, wrappedResponse);
            HttpServletResponse strategyResponse = this.httpSessionStrategy
                    .wrapResponse(wrappedRequest, wrappedResponse);
    
            try {
                filterChain.doFilter(strategyRequest, strategyResponse);
            }
            finally {
                wrappedRequest.commitSession();
            }
        }
    
        public void setServletContext(ServletContext servletContext) {
            this.servletContext = servletContext;
        }
    
        /**
         * Allows ensuring that the session is saved if the response is committed.
         * 对现有Request的一个包装类
         */
        private final class SessionRepositoryResponseWrapper
                extends OnCommittedResponseWrapper {
    
            private final SessionRepositoryRequestWrapper request;
    
            SessionRepositoryResponseWrapper(SessionRepositoryRequestWrapper request,
                    HttpServletResponse response) {
                super(response);
                if (request == null) {
                    throw new IllegalArgumentException("request cannot be null");
                }
                this.request = request;
            }
    
            @Override
            protected void onResponseCommitted() {
                this.request.commitSession();
            }
        }
    
        private final class SessionRepositoryRequestWrapper
                extends HttpServletRequestWrapper {
            private final String CURRENT_SESSION_ATTR = HttpServletRequestWrapper.class
                    .getName();
            private Boolean requestedSessionIdValid;
            private boolean requestedSessionInvalidated;
            private final HttpServletResponse response;
            private final ServletContext servletContext;
    
            private SessionRepositoryRequestWrapper(HttpServletRequest request,
                    HttpServletResponse response, ServletContext servletContext) {
                super(request);
                this.response = response;
                this.servletContext = servletContext;
            }
    
            /**
                     * 更新Session内的数据及最近访问时间到Redis中,若session过期,则清除浏览器cookie的sessionId值
             * Uses the HttpSessionStrategy to write the session id to the response and
             * persist the Session.
             */
            private void commitSession() {
                HttpSessionWrapper wrappedSession = getCurrentSession();
                if (wrappedSession == null) {
                    if (isInvalidateClientSession()) {
                        SessionRepositoryFilter.this.httpSessionStrategy
                                .onInvalidateSession(this, this.response);
                    }
                }
                else {
                    S session = wrappedSession.getSession();
                    SessionRepositoryFilter.this.sessionRepository.save(session);
                    if (!isRequestedSessionIdValid()
                            || !session.getId().equals(getRequestedSessionId())) {
                        SessionRepositoryFilter.this.httpSessionStrategy.onNewSession(session,
                                this, this.response);
                    }
                }
            }
    
            @SuppressWarnings("unchecked")
            private HttpSessionWrapper getCurrentSession() {
                return (HttpSessionWrapper) getAttribute(this.CURRENT_SESSION_ATTR);
            }
    
            private void setCurrentSession(HttpSessionWrapper currentSession) {
                if (currentSession == null) {
                    removeAttribute(this.CURRENT_SESSION_ATTR);
                }
                else {
                    setAttribute(this.CURRENT_SESSION_ATTR, currentSession);
                }
            }
    
            @SuppressWarnings("unused")
            public String changeSessionId() {
                HttpSession session = getSession(false);
    
                if (session == null) {
                    throw new IllegalStateException(
                            "Cannot change session ID. There is no session associated with this request.");
                }
    
                // eagerly get session attributes in case implementation lazily loads them
                Map<String, Object> attrs = new HashMap<String, Object>();
                Enumeration<String> iAttrNames = session.getAttributeNames();
                while (iAttrNames.hasMoreElements()) {
                    String attrName = iAttrNames.nextElement();
                    Object value = session.getAttribute(attrName);
    
                    attrs.put(attrName, value);
                }
    
                SessionRepositoryFilter.this.sessionRepository.delete(session.getId());
                HttpSessionWrapper original = getCurrentSession();
                setCurrentSession(null);
    
                HttpSessionWrapper newSession = getSession();
                original.setSession(newSession.getSession());
    
                newSession.setMaxInactiveInterval(session.getMaxInactiveInterval());
                for (Map.Entry<String, Object> attr : attrs.entrySet()) {
                    String attrName = attr.getKey();
                    Object attrValue = attr.getValue();
                    newSession.setAttribute(attrName, attrValue);
                }
                return newSession.getId();
            }
    
            @Override
            public boolean isRequestedSessionIdValid() {
                if (this.requestedSessionIdValid == null) {
                    String sessionId = getRequestedSessionId();
                    S session = sessionId == null ? null : getSession(sessionId);
                    return isRequestedSessionIdValid(session);
                }
    
                return this.requestedSessionIdValid;
            }
    
            private boolean isRequestedSessionIdValid(S session) {
                if (this.requestedSessionIdValid == null) {
                    this.requestedSessionIdValid = session != null;
                }
                return this.requestedSessionIdValid;
            }
    
            private boolean isInvalidateClientSession() {
                return getCurrentSession() == null && this.requestedSessionInvalidated;
            }
    
            private S getSession(String sessionId) {
                S session = SessionRepositoryFilter.this.sessionRepository
                        .getSession(sessionId);
                if (session == null) {
                    return null;
                }
                session.setLastAccessedTime(System.currentTimeMillis());
                return session;
            }
    
            //重写的getSession方法
    
            //重写获取session的方法,服务区容器内不存在当前请求相关的session,但是请求内含有
            //session=***形式的Cookie时,尝试通过此sessionId从Redis内获取相关的Session信息
            //这就是实现SSO的关键之处
    
            @Override
            public HttpSessionWrapper getSession(boolean create) {
                HttpSessionWrapper currentSession = getCurrentSession();
                if (currentSession != null) {
                    return currentSession;
                }
                String requestedSessionId = getRequestedSessionId();
                if (requestedSessionId != null
                        && getAttribute(INVALID_SESSION_ID_ATTR) == null) {
                    S session = getSession(requestedSessionId);
                    if (session != null) {
                        this.requestedSessionIdValid = true;
                        currentSession = new HttpSessionWrapper(session, getServletContext());
                        currentSession.setNew(false);
                        setCurrentSession(currentSession);
                        return currentSession;
                    }
                    else {
                        // This is an invalid session id. No need to ask again if
                        // request.getSession is invoked for the duration of this request
                        if (SESSION_LOGGER.isDebugEnabled()) {
                            SESSION_LOGGER.debug(
                                    "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
                        }
                        setAttribute(INVALID_SESSION_ID_ATTR, "true");
                    }
                }
                if (!create) {
                    return null;
                }
                if (SESSION_LOGGER.isDebugEnabled()) {
                    SESSION_LOGGER.debug(
                            "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
                                    + SESSION_LOGGER_NAME,
                            new RuntimeException(
                                    "For debugging purposes only (not an error)"));
                }
                S session = SessionRepositoryFilter.this.sessionRepository.createSession();
                //会存到redis中
                session.setLastAccessedTime(System.currentTimeMillis());
                //对session也进行了包装(和request的包装同理)
                currentSession = new HttpSessionWrapper(session, getServletContext());
                setCurrentSession(currentSession);
                return currentSession;
            }
    
            @Override
            public ServletContext getServletContext() {
                if (this.servletContext != null) {
                    return this.servletContext;
                }
                // Servlet 3.0+
                return super.getServletContext();
            }
    
            @Override
            public HttpSessionWrapper getSession() {
                return getSession(true);
            }
    
            @Override
            public String getRequestedSessionId() {
                return SessionRepositoryFilter.this.httpSessionStrategy
                        .getRequestedSessionId(this);
            }
    
            /**
             * Allows creating an HttpSession from a Session instance.
             */
            private final class HttpSessionWrapper extends ExpiringSessionHttpSession<S> {
    
                HttpSessionWrapper(S session, ServletContext servletContext) {
                    super(session, servletContext);
                }
    
                           //重写session失效方法,在设置Session失效的同时删除Redis数据库内Session信息
                @Override
                public void invalidate() {
                    super.invalidate();
                    SessionRepositoryRequestWrapper.this.requestedSessionInvalidated = true;
                    setCurrentSession(null);
                    SessionRepositoryFilter.this.sessionRepository.delete(getId());
                }
            }
        }
    
        /**
         * A delegating implementation of {@link MultiHttpSessionStrategy}.
         */
        static class MultiHttpSessionStrategyAdapter implements MultiHttpSessionStrategy {
            private HttpSessionStrategy delegate;
    
            /**
             * Create a new {@link MultiHttpSessionStrategyAdapter} instance.
             * @param delegate the delegate HTTP session strategy
             */
            MultiHttpSessionStrategyAdapter(HttpSessionStrategy delegate) {
                this.delegate = delegate;
            }
    
            public String getRequestedSessionId(HttpServletRequest request) {
                return this.delegate.getRequestedSessionId(request);
            }
    
            public void onNewSession(Session session, HttpServletRequest request,
                    HttpServletResponse response) {
                this.delegate.onNewSession(session, request, response);
            }
    
            public void onInvalidateSession(HttpServletRequest request,
                    HttpServletResponse response) {
                this.delegate.onInvalidateSession(request, response);
            }
    
            public HttpServletRequest wrapRequest(HttpServletRequest request,
                    HttpServletResponse response) {
                return request;
            }
    
            public HttpServletResponse wrapResponse(HttpServletRequest request,
                    HttpServletResponse response) {
                return response;
            }
        }
    }
    
    

    对现有Request的一个包装类


    SessionRepositoryRequestWrapper.png

    2 创建spring session
    RedisSession在创建时设置3个变量creationTime,maxInactiveInterval,lastAccessedTime。maxInactiveInterval默认值为1800,表示1800s之内该session没有被再次使用,则表明该session已过期。每次session被访问都会更新lastAccessedTime的值,session的过期计算公式:当前时间-lastAccessedTime > maxInactiveInterval.

    /**

    • Creates a new instance ensuring to mark all of the new attributes to be
    • persisted in the next save operation.
      **/
      RedisSession() {
      this(new MapSession());
      this.delta.put(CREATION_TIME_ATTR, getCreationTime());
      this.delta.put(MAX_INACTIVE_ATTR, getMaxInactiveIntervalInSeconds());
      this.delta.put(LAST_ACCESSED_ATTR, getLastAccessedTime());
      this.isNew = true;
      this.flushImmediateIfNecessary();
      }
      public MapSession() {
      this(UUID.randomUUID().toString());
      }
      flushImmediateIfNecessary判断session是否需要立即写入后端存储。

    3 获取session
    spring session在redis里面保存的数据包括:

    SET类型的spring:session:expireations:[min]

    min表示从1970年1月1日0点0分经过的分钟数,SET集合的member为expires:[sessionId],表示members会在在min分钟过期。

    String类型的spring:session:sessions:expires:[sessionId]

    该数据的TTL表示sessionId过期的剩余时间,即maxInactiveInterval。

    Hash类型的spring:session:sessions:[sessionId]

    session保存的数据,记录了creationTime,maxInactiveInterval,lastAccessedTime,attribute。前两个数据是用于session过期管理的辅助数据结构。

    应用通过getSession(boolean create)方法来获取session数据,参数create表示session不存在时是否创建新的session。getSession方法首先从请求的“.CURRENT_SESSION”属性来获取currentSession,没有currentSession,则从request取出sessionId,然后读取spring:session:sessions:[sessionId]的值,同时根据lastAccessedTime和MaxInactiveIntervalInSeconds来判断这个session是否过期。如果request中没有sessionId,说明该用户是第一次访问,会根据不同的实现,如RedisSession,MongoExpiringSession,GemFireSession等来创建一个新的session。

    另, 从request取sessionId依赖具体的HttpSessionStrategy的实现,spring session给了两个默认的实现CookieHttpSessionStrategy和HeaderHttpSessionStrategy,即从cookie和header中取出sessionId。

    @Override
    public HttpSessionWrapper getSession(boolean create) {
    HttpSessionWrapper currentSession = getCurrentSession();
    if (currentSession != null) {
    return currentSession;
    }
    // 从request请求中得到sessionId
    String requestedSessionId = getRequestedSessionId();
    if (requestedSessionId != null
    && getAttribute(INVALID_SESSION_ID_ATTR) == null) {
    S session = getSession(requestedSessionId);
    if (session != null) {
    this.requestedSessionIdValid = true;
    currentSession = new HttpSessionWrapper(session, getServletContext());
    currentSession.setNew(false);
    setCurrentSession(currentSession);
    return currentSession;
    }
    else {
    // This is an invalid session id. No need to ask again if
    // request.getSession is invoked for the duration of this request
    setAttribute(INVALID_SESSION_ID_ATTR, "true");
    }
    }
    if (!create) {
    return null;
    }
    S session = SessionRepositoryFilter.this.sessionRepository.createSession();
    session.setLastAccessedTime(System.currentTimeMillis());
    currentSession = new HttpSessionWrapper(session, getServletContext());
    setCurrentSession(currentSession);
    return currentSession;
    }
    spring session为什么会使用3个key,而不是一个key?接下来回答。

    4 session有效期与删除
    spring session的有效期指的是访问有效期,每一次访问都会更新lastAccessedTime的值,过期时间为lastAccessedTime + maxInactiveInterval,也即在有效期内每访问一次,有效期就向后延长maxInactiveInterval。

    对于过期数据,一般有三种删除策略:

    1)定时删除,即在设置键的过期时间的同时,创建一个定时器, 当键的过期时间到来时,立即删除。

    2)惰性删除,即在访问键的时候,判断键是否过期,过期则删除,否则返回该键值。

    3)定期删除,即每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。

    redis删除过期数据采用的是懒性删除+定期删除组合策略,也就是数据过期了并不会及时被删除。为了实现session过期的及时性,spring session采用了定时删除的策略,但它并不是如上描述在设置键的同时设置定时器,而是采用固定频率(1分钟)轮询删除过期值,这里的删除是惰性删除。

    轮询操作并没有去扫描所有的spring:session:sessions:[sessionId]的过期时间,而是在当前分钟数检查前一分钟应该过期的数据,即spring:session:expirations:[min]的members,然后delete掉spring:session:expirations:[min],惰性删除spring:session:sessions:expires:[sessionId]。

    还有一点是,查看三个数据结构的TTL时间,spring:session:sessions:[sessionId]和spring:session:expirations:[min]比真正的有效期大5分钟,目的是确保当expire key数据过期后,监听事件还能获取到session保存的原始数据。

    @Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}")
    public void cleanupExpiredSessions() {
    this.expirationPolicy.cleanExpiredSessions();
    }
    public void cleanExpiredSessions() {
    long now = System.currentTimeMillis();
    long prevMin = roundDownMinute(now);
    // preMin时间到,将spring:session:expirations:[min], set集合中members包括了这一分钟之内需要过期的所有
    // expire key删掉, member元素为expires:[sessionId]
    String expirationKey = getExpirationKey(prevMin);
    Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
    this.redis.delete(expirationKey);
    for (Object session : sessionsToExpire) {
    // sessionKey为spring:session:sessions:expires:[sessionId]
    String sessionKey = getSessionKey((String) session);
    //利用redis的惰性删除策略
    touch(sessionKey);
    }
    }
    spring session在redis中保存了三个key,为什么? sessions key记录session本身的数据,expires key标记session的准确过期时间,expiration key保证session能够被及时删除,spring监听事件能够被及时处理。

    上面的代码展示了session expires key如何被删除,那session每次都是怎样更新过期时间的呢? 每一次http请求,在经过所有的filter处理过后,spring session都会通过onExpirationUpdated()方法来更新session的过期时间, 具体的操作看下面源码的注释。

    public void onExpirationUpdated(Long originalExpirationTimeInMilli,
    ExpiringSession session) {
    String keyToExpire = "expires:" + session.getId();
    long toExpire = roundUpToNextMinute(expiresInMillis(session));
    if (originalExpirationTimeInMilli != null) {
    long originalRoundedUp = roundUpToNextMinute(originalExpirationTimeInMilli);
    // 更新expirations:[min],两个分钟数之内都有这个session,将前一个set中的成员删除
    if (toExpire != originalRoundedUp) {
    String expireKey = getExpirationKey(originalRoundedUp);
    this.redis.boundSetOps(expireKey).remove(keyToExpire);
    }
    }
    long sessionExpireInSeconds = session.getMaxInactiveIntervalInSeconds();
    String sessionKey = getSessionKey(keyToExpire);
    if (sessionExpireInSeconds < 0) {
    this.redis.boundValueOps(sessionKey).append("");
    this.redis.boundValueOps(sessionKey).persist();
    this.redis.boundHashOps(getSessionKey(session.getId())).persist();
    return;
    }
    String expireKey = getExpirationKey(toExpire);
    BoundSetOperations<Object, Object> expireOperations = this.redis
    .boundSetOps(expireKey);
    expireOperations.add(keyToExpire);
    long fiveMinutesAfterExpires = sessionExpireInSeconds
    + TimeUnit.MINUTES.toSeconds(5);
    // expirations:[min] key的过期时间加5分钟
    expireOperations.expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
    if (sessionExpireInSeconds == 0) {
    this.redis.delete(sessionKey);
    }
    else {
    // expires:[sessionId] 值为“”,过期时间为MaxInactiveIntervalInSeconds
    this.redis.boundValueOps(sessionKey).append("");
    this.redis.boundValueOps(sessionKey).expire(sessionExpireInSeconds,
    TimeUnit.SECONDS);
    }
    // sessions:[sessionId]的过期时间 加5分钟
    this.redis.boundHashOps(getSessionKey(session.getId()))
    .expire(fiveMinutesAfterExpires, TimeUnit.SECONDS);
    }

    相关文章

      网友评论

        本文标题:2018-02-17

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