美文网首页SpringFramework程序员
Spring Session Strategy 详解

Spring Session Strategy 详解

作者: 非典型程序员 | 来源:发表于2016-12-25 16:38 被阅读6869次

    HttpSessionStrategy

    HttpSessionStrategy 定了三个方法如下:

    /**
    *从提供的{@link javax.servlet.http.HttpServletRequest}获取请求的会话ID。
    * 例如,会话ID可能来自Cookie或请求标头。
    */
    String getRequestedSessionId(HttpServletRequest request);
    
    /**
    *此方法在创建新会话时调用,并应通知客户端新会话ID是什么。 
    *例如,它可能创建一个带有会话ID的新Cookie,或者设置一个带有新会话ID值的HTTP响应头。
    *注意这里的 session 是 org.springframework.session.Session
    */
    void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response);
    
    /**
     *当会话无效时调用此方法,并应通知客户端会话标识不再有效。 
     *例如,它可能会删除其中包含会话ID的Cookie,或者设置一个带有空值的HTTP响应标头,指示客户端不再提交该会话ID。
    */
    void onInvalidateSession(HttpServletRequest request, HttpServletResponse response);
    
    

    HeaderHttpSessionStrategy

    HeaderHttpSessionStrategy 实现了 HttpSessionStrategy 接口。 它主要的功能是在 HTTP 请请求头中设置设置我们的 sessionId,已经从请求头中获取我们的 sessionId 。默人的请求头参数为 x-auth-token,同时提供了 setHeaderName(String headerName)方法一遍个性化设置存储 sessionId 的请求头参数。HeaderHttpSessionStrategy 通知客户端使session 失效的方式值是将 session 在HTTP头中的请求参数设置为空串("");

    private String headerName = "x-auth-token";
    
    public String getRequestedSessionId(HttpServletRequest request) {
        return request.getHeader(headerName);
    }
    
    public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
        response.setHeader(headerName, session.getId());
    }
    
    public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
        response.setHeader(headerName, "");
    }
    
    /**
     * The name of the header to obtain the session id from. Default is "x-auth-token".
     * @param headerName the name of the header to obtain the session id from.
    */
    public void setHeaderName(String headerName) {
        Assert.notNull(headerName, "headerName cannot be null");
        this.headerName = headerName;
    }
    

    MultiHttpSessionStrategy

    在某些业务场景下可能还需要进一步 HttpServletRequest、HttpServletResponse进行处理,自定义包装。 例如,CookieHttpSessionStrategy 自定义了如何完成网址重写,以选择在多个会话处于活动状态的情况下应使用哪个会话。因此定义此MultiHttpSessionStrategy 接口,此接口继承了HttpSessionStrategy接口、RequestResponsePostProcessor接口。其中在上文提到 HttpSessionStrategy 定义了 session 基本处理方法,而RequestResponsePostProcessor 定义 HttpServletRequest、HttpServletResponse 包装方法。

    public interface MultiHttpSessionStrategy extends HttpSessionStrategy, RequestResponsePostProcessor {
    }
    

    RequestResponsePostProcessor

    RequestResponsePostProcessor 提供一下两个方法便于开发人员去个性化定制我们的 HttpServletRequest、HttpServletResponse 对象。

    /**包装HttpServletRequest */
    HttpServletRequest wrapRequest(HttpServletRequest request, HttpServletResponse response);
    
    /**包装HttpServletResponse */
    HttpServletResponse wrapResponse(HttpServletRequest request, HttpServletResponse response);
    

    HttpSessionManager

    HttpSessionManager 维护着一组 session Id 别名映射,以支持多个同时会话管理。

    /** 从 HttpServletRequest 获取当前回话的 session id 别名*/
    String getCurrentSessionAlias(HttpServletRequest request);
    
    /** 从{@link HttpServletRequest}中获取session Alias 到session id的映射,*/
    Map<String, String> getSessionIds(HttpServletRequest request);
    
    /** 通过指定的 sessionAlias 加密 url */
    String encodeURL(String url, String sessionAlias);
    
    /**
     * 通过 HttpServletRequest 获取一个新的,唯一的 session Alias。
     * <code>
     * String newAlias = httpSessionManager.getNewSessionAlias(request);
     * String addAccountUrl = httpSessionManager.encodeURL("./", newAlias);
    * </code>
    */
    String getNewSessionAlias(HttpServletRequest request);
    

    CookieHttpSessionStrategy

    提供通过 cookie 的方式来传递 sessionId。cookie 参数名默认为 SESSION。CookieHttpSessionStrategy 实现了MultiHttpSessionStrategy 接口以及HttpSessionManager 接口。

    当一个 session 创建后, HTTP 的响应头中将会以指定 cookie名称来存储 sessionID,此cookie 将会被标识为会话cookie,同时以 context path 作为该 cookie 的 path,并被标识了HTTPOnly。此外如果 javax.servlet.http.HttpServletRequest#isSecure()返回true,则cookie将被标记为Secure。如下:

    HTTP/1.1 200 OK
    Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly
    

    客户端在访问的时候必须带上此cookie:

    GET /messages/ HTTP/1.1
    Host: example.com
    Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
    

    当 session 过期以后,服务端将会给客户端返回一个过期的 cookie,如下:

    HTTP/1.1 200 OK
    Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly
    

    此外 CookieHttpSessionStrategy 默认支持多个同时会话。 一旦与浏览器建立会话,可以通过为{@link #setSessionAliasParamName(String)}指定唯一值来启动另一个会话。 请求如下:

    GET /messages/?_s=1416195761178 HTTP/1.1
    Host: example.com
    Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
    

    服务端返回的结果为:

    HTTP/1.1 200 OK
    Set-Cookie: SESSION="0 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 1416195761178 8a929cde-2218-4557-8d4e-82a79a37876d"; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly
    

    要使用原始会话,您可以创建一个没有HTTP参数的请求。 要使用新会话,可以使用HTTP参数_s = 1416195761178的请求。默认情况将会重写urls以包含当前使用的session。

    CookieHttpSessionStrategy 源码解析

    public final class CookieHttpSessionStrategy implements MultiHttpSessionStrategy, HttpSessionManager {
        private static final String SESSION_IDS_WRITTEN_ATTR = CookieHttpSessionStrategy.class.getName().concat(".SESSIONS_WRITTEN_ATTR");
    
        static final String DEFAULT_ALIAS = "0";
    
        static final String DEFAULT_SESSION_ALIAS_PARAM_NAME = "_s";
       
        // session alias 的正则表达式
        private Pattern ALIAS_PATTERN = Pattern.compile("^[\\w-]{1,50}$");
    
        // 用于存储 session 信息的 cookie 名,可以地址定义。
        private String cookieName = "SESSION";
        
        // session alias 的参数名,可以自定义
        private String sessionParam = DEFAULT_SESSION_ALIAS_PARAM_NAME;
    
        private boolean isServlet3Plus = isServlet3();
    
        /** 
         * 获取当前回话的 session id. 
         * 1.首先获取当前客户端所有的session alias 到 session id 的映射 sessionIds (Map).
         * 2.获取当前回话的 session alias 
         * 3.使用获取到的 session alias 从  sessionIds 中获取相应的 session id
         */
        public String getRequestedSessionId(HttpServletRequest request) {
            Map<String,String> sessionIds = getSessionIds(request);
            String sessionAlias = getCurrentSessionAlias(request);
            
            //通过sessionAlias 获取 session id
            return sessionIds.get(sessionAlias);
        }
    
        /** 
         * 获取当前 session 的 alias。
         * 当前 sessionParam 不存在时返回默认的 DEFAULT_ALIAS (0) 。 sessionParam 用于在 http 请求中保存当前 session 的 alias 的参数名,默认为 "_s".
         * 从当前的 request 获取 sessionParam 的值,如果为空返回默认的 DEFAULT_ALIAS (0) ,否则使用声明 ALIAS_PATTERN 进行正则匹配,验证参数值是否合法。
         * 合法返回值,不能合法返回 DEFAULT_ALIAS。
         * 
         */
        public String getCurrentSessionAlias(HttpServletRequest request) {
            if(sessionParam == null) {
                return DEFAULT_ALIAS;
            }
            String u = request.getParameter(sessionParam);
            if(u == null) {
                return DEFAULT_ALIAS;
            }
            if(!ALIAS_PATTERN.matcher(u).matches()) {
                return DEFAULT_ALIAS;
            }
            return u;
        }
    
        
        /**
         * 给当前客户端请求获取新的 session alias。
         */
        public String getNewSessionAlias(HttpServletRequest request) {
            // 获取当前请求客户端所有的 session alias 到 session ids 的映射中所有的 alias,如果为空返回 DEFAULT_ALIAS
            Set<String> sessionAliases = getSessionIds(request).keySet();
            if(sessionAliases.isEmpty()) {
                return DEFAULT_ALIAS;
            }
            
            /*
             * 遍历所有的 alias,选取值最大的 alias。这个最大的 alias 同时也代表了最后的创建的 session 的 alias。
             * 最大的 alias + 1 变得到了新的 alias,同时将 long 转化为 16 进制字符串
             */
            
            long lastAlias = Long.decode(DEFAULT_ALIAS);
            for(String alias : sessionAliases) {
                long selectedAlias = safeParse(alias);
                if(selectedAlias > lastAlias) {
                    lastAlias = selectedAlias;
                }
            }
            return Long.toHexString(lastAlias + 1);
        }
    
        /**
         * 将16进制字符串转化为 long 型。
         * @param hex
         * @return
         */
        private long safeParse(String hex) {
            try {
                return Long.decode("0x" + hex);
            } catch(NumberFormatException notNumber) {
                return 0;
            }
        }
    
        /**
         * 向客户端重写新的session cookie 信息。
         * 
         */
        public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
            // 获取客户端当前已经存在 session id 集合,如果已经包含指定的 session id 直接返回。
            Set<String> sessionIdsWritten = getSessionIdsWritten(request);
            if(sessionIdsWritten.contains(session.getId())) {
                return;
            }
            
            /*
             * 将session id 加入 sessionIdsWritten 集合中,获取请求中的 session alias 和 session id 的映射信息
             * 获取当前 session 的 session alias ,并将其添加到 session alias 和 session id 的映射 map 中。
             * 构建新的 cookie 信息,写入 response.
             * 
             * 从这里看出,调用此方法可以创建一个新的 session 或者改变当前请求的 session 。因为 getCurrentSessionAlias(request) 获取的当前的 session alias。
             */
            
            sessionIdsWritten.add(session.getId());
    
            Map<String,String> sessionIds = getSessionIds(request);
            String sessionAlias = getCurrentSessionAlias(request);
            sessionIds.put(sessionAlias, session.getId());
            Cookie sessionCookie = createSessionCookie(request, sessionIds);
            response.addCookie(sessionCookie);
        }
    
        @SuppressWarnings("unchecked")
        private Set<String> getSessionIdsWritten(HttpServletRequest request) {
            
            /*
             * 这里要特别注意,改方法不仅仅是获取 request 中 SESSION_IDS_WRITTEN_ATTR 属性值 。
             * 当 SESSION_IDS_WRITTEN_ATTR 不存在事,会主动设置一个新的 Set 到 request 中,
             * 因此在上一个 onNewSession() 中便将参数中传递的 session id 已保存在里面。所以,在
             * 我个人看来,此方法名应该改为 getSessionIdsWrittenOrNew() 之类的。
             */
            Set<String> sessionsWritten = (Set<String>) request.getAttribute(SESSION_IDS_WRITTEN_ATTR);
            if(sessionsWritten == null) {
                sessionsWritten = new HashSet<String>();
                request.setAttribute(SESSION_IDS_WRITTEN_ATTR, sessionsWritten);
            }
            return sessionsWritten;
        }
    
        /**
         * 根据指定的 session ids 给指定的请求创建 session cookie. 特别注意,当sessionIds为空时,相当于使当前 session 失效
         * @param request
         * @param sessionIds
         * @return
         */
        private Cookie createSessionCookie(HttpServletRequest request,
                Map<String, String> sessionIds) {
            Cookie sessionCookie = new Cookie(cookieName,"");
            if(isServlet3Plus) {
                sessionCookie.setHttpOnly(true);
            }
            sessionCookie.setSecure(request.isSecure());
            sessionCookie.setPath(cookiePath(request));
            // TODO set domain?
    
            // 使 session  失效
            if(sessionIds.isEmpty()) {
                sessionCookie.setMaxAge(0);
                return sessionCookie;
            }
            
            // 当客户端只有一个请求回话时,我们的 session cookie 里面并没有维护 session alias
            if(sessionIds.size() == 1) {
                String cookieValue = sessionIds.values().iterator().next();
                sessionCookie.setValue(cookieValue);
                return sessionCookie;
            }
            
            /*
             * 当客户端维护多个请求回话时
             * 我们的 session cookie 中 session id 和 session alias 的组合模式如下:SESSION="alias1 sessionId1 alias2 sessionId2 ......";
             * 例如:SESSION="0 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 1416195761178 8a929cde-2218-4557-8d4e-82a79a37876d";
             */
            
            StringBuffer buffer = new StringBuffer();
            for(Map.Entry<String,String> entry : sessionIds.entrySet()) {
                String alias = entry.getKey();
                String id = entry.getValue();
    
                buffer.append(alias);
                buffer.append(" ");
                buffer.append(id);
                buffer.append(" ");
            }
            buffer.deleteCharAt(buffer.length()-1);
    
            sessionCookie.setValue(buffer.toString());
            return sessionCookie;
        }
    
         /**
          * 当会话无效时调用此方法,并应通知客户端会话标识不再有效。
          * 从 sessionId 中删除当前回话,并重写 cookie。
          **/
        public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
            Map<String,String> sessionIds = getSessionIds(request);
            String requestedAlias = getCurrentSessionAlias(request);
            
            // 这里移除了当前 session alias 和 session id的映射,并重写了cookie,但是没有移除 request 中 SESSION_IDS_WRITTEN_ATTR 属性中的 session id
            sessionIds.remove(requestedAlias);
    
            Cookie sessionCookie = createSessionCookie(request, sessionIds);
            response.addCookie(sessionCookie);
        }
    
        /**
         * Sets the name of the HTTP parameter that is used to specify the session
         * alias. If the value is null, then only a single session is supported per
         * browser.
         *
         * @param sessionAliasParamName
         *            the name of the HTTP parameter used to specify the session
         *            alias. If null, then ony a single session is supported per
         *            browser.
         */
        public void setSessionAliasParamName(String sessionAliasParamName) {
            this.sessionParam = sessionAliasParamName;
        }
    
        /**
         * Sets the name of the cookie to be used
         * @param cookieName the name of the cookie to be used
         */
        public void setCookieName(String cookieName) {
            if(cookieName == null) {
                throw new IllegalArgumentException("cookieName cannot be null");
            }
            this.cookieName = cookieName;
        }
    
        /**
         * Retrieve the first cookie with the given name. Note that multiple
         * cookies can have the same name but different paths or domains.
         * @param request current servlet request
         * @param name cookie name
         * @return the first cookie with the given name, or {@code null} if none is found
         */
        private static Cookie getCookie(HttpServletRequest request, String name) {
            if(request == null) {
                throw new IllegalArgumentException("request cannot be null");
            }
            Cookie cookies[] = request.getCookies();
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (name.equals(cookie.getName())) {
                        return cookie;
                    }
                }
            }
            return null;
        }
    
        private static String cookiePath(HttpServletRequest request) {
            return request.getContextPath() + "/";
        }
    
        /** 
         * 获取当前请求客户端所有的 session alias 到 session ids 的映射。session alias 和 sessoin ids 保存在名为“SESSION”的 cookie 中, 如下:
         * <pre>
         *  HTTP/1.1 200 OK
         *  Set-Cookie: SESSION="0 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 1416195761178 8a929cde-2218-4557-8d4e-82a79a37876d"; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly
         * </pre>
         */
        public Map<String,String> getSessionIds(HttpServletRequest request) {
            // 获取 session cookie 字符串
            Cookie session = getCookie(request, cookieName);
            // 如果不存在 session 返回空串,如果存在获取session cookie 的值。
            String sessionCookieValue = session == null ? "" : session.getValue();
            // 申明一个 map 集合用于保存 session alias 到 session id 的映射
            Map<String,String> result = new LinkedHashMap<String,String>();
            
            /*
             * StringTokenizer 字符串分隔解析类型, 用于根据指定的模式分割字符串。
             * session cookie 的值中的分割符为“空格字符串”,因此这里使用它解析。
             */
            StringTokenizer tokens = new StringTokenizer(sessionCookieValue, " ");
            
            /*
             * 如果分割后的值只有一个,那么说明当前客户端只开启一个回话,tokens里面的唯一的元素便是当前的 session id。
             * 当只有一个会话是,使用 DEFAULT_ALIAS(0)作为当前 session ids 的别名。
             */
            if(tokens.countTokens() == 1) {
                result.put(DEFAULT_ALIAS, tokens.nextToken());
                return result;
            }
            
            /*
             * 当tokens 大于1时,说明当前客户端开启了多个回话,此时 tokens 的个数一点是偶数。
             * session cookie 中的session id 和 session alias 的组合模式如下:SESSION="alias1 sessionId1 alias2 sessionId2 ......";
             * 例如:SESSION="0 f81d4fae-7dec-11d0-a765-00a0c91e6bf6 1416195761178 8a929cde-2218-4557-8d4e-82a79a37876d";
             */
            while(tokens.hasMoreTokens()) {
                String alias = tokens.nextToken();
                if(!tokens.hasMoreTokens()) {
                    break;
                }
                String id = tokens.nextToken();
                result.put(alias, id);
            }
            return result;
        }
    
        /**
         * 在 request 中设置 session 管理策略
         */
        public HttpServletRequest wrapRequest(HttpServletRequest request, HttpServletResponse response) {
            request.setAttribute(HttpSessionManager.class.getName(), this);
            return request;
        }
    
        /**
         * 封装 HttpServletResponse 
         */
        public HttpServletResponse wrapResponse(HttpServletRequest request, HttpServletResponse response) {
            return new MultiSessionHttpServletResponse(response, request);
        }
    
        class MultiSessionHttpServletResponse extends HttpServletResponseWrapper {
            private final HttpServletRequest request;
    
            public MultiSessionHttpServletResponse(HttpServletResponse response, HttpServletRequest request) {
                super(response);
                this.request = request;
            }
    
            @Override
            public String encodeRedirectURL(String url) {
                url = super.encodeRedirectURL(url);
                return CookieHttpSessionStrategy.this.encodeURL(url, getCurrentSessionAlias(request));
            }
    
            @Override
            public String encodeURL(String url) {
                url = super.encodeURL(url);
    
                String alias = getCurrentSessionAlias(request);
                return CookieHttpSessionStrategy.this.encodeURL(url, alias);
            }
        }
    
        public String encodeURL(String url, String sessionAlias) {
            String encodedSessionAlias = urlEncode(sessionAlias);
            int queryStart = url.indexOf("?");
            boolean isDefaultAlias = DEFAULT_ALIAS.equals(encodedSessionAlias);
            if(queryStart < 0) {
                return isDefaultAlias ? url : url + "?" + sessionParam + "=" + encodedSessionAlias;
            }
            String path = url.substring(0, queryStart);
            String query = url.substring(queryStart + 1, url.length());
            
            // 这里主要的是为了替换查询参数里面的 sessionParim 的值。
            String replacement = isDefaultAlias ? "" : "$1"+encodedSessionAlias;
            query = query.replaceFirst( "((^|&)" + sessionParam + "=)([^&]+)?", replacement);
            
            /*
             * 需要这一步是为了防止上在上述的 query.replaceFirst( "((^|&)" + sessionParam + "=)([^&]+)?", replacement)
             * 中没有找到  sessionParam 对应的请求参数,这需要在 query 后面拼接 sessionParam。
             * 怎么造成这种情景的呢,大概是客户端开启一个新的 session。
             */
            if(!isDefaultAlias && url.endsWith(query)) {
                // no existing alias
                if(!(query.endsWith("&") || query.length() == 0)) {
                    query += "&";
                }
                query += sessionParam + "=" + encodedSessionAlias;
            }
    
            return path + "?" + query;
        }
    
        private String urlEncode(String value) {
            try {
                return URLEncoder.encode(value, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        }
    
        /**
         * Returns true if the Servlet 3 APIs are detected.
         * @return
         */
        private boolean isServlet3() {
            try {
                ServletRequest.class.getMethod("startAsync");
                return true;
            } catch(NoSuchMethodException e) {}
            return false;
        }
        
        public static void main(String[] args) {
            String query = "&_s=ygkhldjg&123=jkljkj";
            query = query.replaceFirst( "((^|&)_s=)([^&]+)?", "$1ranqi");
            System.out.println(query);
        }
    }
    

    相关文章

      网友评论

        本文标题:Spring Session Strategy 详解

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