Spring Session Strategy 詳解

HttpSessionStrategy

HttpSessionStrategy 定了三個方法如下:

/**
*從提供的{@link javax.servlet.http.HttpServletRequest}獲取請求的會話ID。
* 例如,會話ID可能來自Cookie或請求標頭。
*/
String getRequestedSessionId(HttpServletRequest request);

/**
*此方法在創(chuàng)建新會話時調(diào)用,并應(yīng)通知客戶端新會話ID是什么。 
*例如,它可能創(chuàng)建一個帶有會話ID的新Cookie,或者設(shè)置一個帶有新會話ID值的HTTP響應(yīng)頭。
*注意這里的 session 是 org.springframework.session.Session
*/
void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response);

/**
 *當會話無效時調(diào)用此方法,并應(yīng)通知客戶端會話標識不再有效。 
 *例如,它可能會刪除其中包含會話ID的Cookie,或者設(shè)置一個帶有空值的HTTP響應(yīng)標頭,指示客戶端不再提交該會話ID。
*/
void onInvalidateSession(HttpServletRequest request, HttpServletResponse response);

HeaderHttpSessionStrategy

HeaderHttpSessionStrategy 實現(xiàn)了 HttpSessionStrategy 接口。 它主要的功能是在 HTTP 請請求頭中設(shè)置設(shè)置我們的 sessionId,已經(jīng)從請求頭中獲取我們的 sessionId 。默人的請求頭參數(shù)為 x-auth-token,同時提供了 setHeaderName(String headerName)方法一遍個性化設(shè)置存儲 sessionId 的請求頭參數(shù)。HeaderHttpSessionStrategy 通知客戶端使session 失效的方式值是將 session 在HTTP頭中的請求參數(shù)設(shè)置為空串("");

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

在某些業(yè)務(wù)場景下可能還需要進一步 HttpServletRequest、HttpServletResponse進行處理,自定義包裝。 例如,CookieHttpSessionStrategy 自定義了如何完成網(wǎng)址重寫,以選擇在多個會話處于活動狀態(tài)的情況下應(yīng)使用哪個會話。因此定義此MultiHttpSessionStrategy 接口,此接口繼承了HttpSessionStrategy接口、RequestResponsePostProcessor接口。其中在上文提到 HttpSessionStrategy 定義了 session 基本處理方法,而RequestResponsePostProcessor 定義 HttpServletRequest、HttpServletResponse 包裝方法。

public interface MultiHttpSessionStrategy extends HttpSessionStrategy, RequestResponsePostProcessor {
}

RequestResponsePostProcessor

RequestResponsePostProcessor 提供一下兩個方法便于開發(fā)人員去個性化定制我們的 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 參數(shù)名默認為 SESSION。CookieHttpSessionStrategy 實現(xiàn)了MultiHttpSessionStrategy 接口以及HttpSessionManager 接口。

當一個 session 創(chuàng)建后, HTTP 的響應(yīng)頭中將會以指定 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 過期以后,服務(wù)端將會給客戶端返回一個過期的 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

服務(wù)端返回的結(jié)果為:

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

要使用原始會話,您可以創(chuàng)建一個沒有HTTP參數(shù)的請求。 要使用新會話,可以使用HTTP參數(shù)_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 的參數(shù)名,可以自定義
    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 中獲取相應(yīng)的 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 的參數(shù)名,默認為 "_s".
     * 從當前的 request 獲取 sessionParam 的值,如果為空返回默認的 DEFAULT_ALIAS (0) ,否則使用聲明 ALIAS_PATTERN 進行正則匹配,驗證參數(shù)值是否合法。
     * 合法返回值,不能合法返回 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 同時也代表了最后的創(chuàng)建的 session 的 alias。
         * 最大的 alias + 1 變得到了新的 alias,同時將 long 轉(zhuǎn)化為 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進制字符串轉(zhuǎn)化為 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) {
        // 獲取客戶端當前已經(jīng)存在 session id 集合,如果已經(jīng)包含指定的 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 中。
         * 構(gòu)建新的 cookie 信息,寫入 response.
         * 
         * 從這里看出,調(diào)用此方法可以創(chuàng)建一個新的 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 不存在事,會主動設(shè)置一個新的 Set 到 request 中,
         * 因此在上一個 onNewSession() 中便將參數(shù)中傳遞的 session id 已保存在里面。所以,在
         * 我個人看來,此方法名應(yīng)該改為 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;
    }

    /**
     * 根據(jù)指定的 session ids 給指定的請求創(chuàng)建 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;
    }

     /**
      * 當會話無效時調(diào)用此方法,并應(yīng)通知客戶端會話標識不再有效。
      * 從 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 字符串分隔解析類型, 用于根據(jù)指定的模式分割字符串。
         * 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 的個數(shù)一點是偶數(shù)。
         * 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 中設(shè)置 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());
        
        // 這里主要的是為了替換查詢參數(shù)里面的 sessionParim 的值。
        String replacement = isDefaultAlias ? "" : "$1"+encodedSessionAlias;
        query = query.replaceFirst( "((^|&)" + sessionParam + "=)([^&]+)?", replacement);
        
        /*
         * 需要這一步是為了防止上在上述的 query.replaceFirst( "((^|&)" + sessionParam + "=)([^&]+)?", replacement)
         * 中沒有找到  sessionParam 對應(yīng)的請求參數(shù),這需要在 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);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容