聊一聊session和cookie

原本是想寫aop設計機制的,可是最近被session這個東西搞得有點頭大,因此就抽點時間來整理下關於session的一些東西。html

目錄

從http協議的無狀態性提及

HTTP是一種無狀態協議。關於這個無狀態以前我也不太理解,由於HTTP底層是TCP,既然是TCP,就是長鏈接,這個過程是保持鏈接狀態的,又爲何說http是無狀態的呢?先來搞清楚這兩個概念:java

無鏈接和無狀態

  • 無鏈接nginx

    每次鏈接只處理一個請求,服務端處理完客戶端一次請求,等到客戶端做出迴應以後便斷開鏈接;web

  • 無狀態ajax

    是指服務端對於客戶端每次發送的請求都認爲它是一個新的請求,上一次會話和下一次會話沒有聯繫;redis

無鏈接的維度是鏈接,無狀態的維度是請求;http是基於tcp的,而從http1.1開始默認使用持久鏈接;在這個鏈接過程當中,客戶端能夠向服務端發送屢次請求,可是各個請求之間的並無什麼聯繫;這樣來考慮,就很好理解無狀態這個概念了。數據庫

持久鏈接

持久鏈接,本質上是客戶端與服務器通訊的時候,創建一個持久化的TCP鏈接,這個鏈接不會隨着請求結束而關閉,一般會保持鏈接一段時間。json

現有的持久鏈接類型有兩種:HTTP/1.0+的keep-alive和HTTP/1.1的persistent。api

  • HTTP/1.0+的keep-alive

先來開一張圖:跨域

這張圖是請求www.baidu.com時的請求頭信息。這裏面咱們須要注意的是:

connection: keep-alive
複製代碼

咱們每次發送一個HTTP請求,會附帶一個connection:keep-alive,這個參數就是聲明一個持久鏈接。

  • HTTP/1.1的persistent

HTTP/1.1的持久鏈接默認是開啓的,只有首部中包含connection:close,纔會事務結束以後關閉鏈接。固然服務器和客戶端仍能夠隨時關閉持久鏈接。

當發送了connection:close首部以後客戶端就沒有辦法在那條鏈接上發送更多的請求了。固然根據持久鏈接的特性,必定要傳輸正確的content-length。

還有根據HTTP/1.1的特性,是不該該和HTTP/1.0客戶端創建持久鏈接的。最後,必定要作好重發的準備。

http無狀態

OK,首先來明確下,這個狀態的主體指的是什麼?應該是信息,這些信息是由服務端所維護的與客戶端交互的信息(也稱爲狀態信息); 由於HTTP自己是不保存任何用戶的狀態信息的,因此HTTP是無狀態的協議。

如何保持狀態信息

在聊這個這個問題以前,咱們來考慮下爲何http本身不來作這個事情:也就是讓http變成有狀態的。

  • http自己來實現狀態維護

    從上面關於無狀態的理解,若是如今須要讓http本身變成有狀態的,就意味着http協議須要保存交互的狀態信息;暫且不說這種方式是否合適,但從維護狀態信息這一點來講,代價就很高,由於既然保存了狀態信息,那後續的一些行爲一定也會受到狀態信息的影響。

    從歷史角度來講,最初的http協議只是用來瀏覽靜態文件的,無狀態協議已經足夠,這樣實現的負擔也很輕。可是隨着web技術的不斷髮展,愈來愈多的場景須要狀態信息可以得以保存;一方面是http自己不會去改變它的這種無狀態的特性(至少目前是這樣的),另外一方面業務場景又迫切的須要保持狀態;那麼這個時候就須要來「裝飾」一下http,引入一些其餘機制來實現有狀態。

  • cookie和session體系

    經過引入cookie和session體系機制來維護狀態信息。即用戶第一次訪問服務器的時候,服務器響應報頭一般會出現一個Set-Cookie響應頭,這裏其實就是在本地設置一個cookie,當用戶再次訪問服務器的時候,http會附帶這個cookie過去,cookie中存有sessionId這樣的信息來到服務器這邊確認是否屬於同一次會話。

Cookie

cookie是由服務器發送給客戶端(瀏覽器)的小量信息,以{key:value}的形式存在。

Cookie機制原理

客戶端請求服務器時,若是服務器須要記錄該用戶狀態,就使用response向客戶端瀏覽器頒發一個Cookie。而客戶端瀏覽器會把Cookie保存起來。當瀏覽器再請求 服務器時,瀏覽器把請求的網址連同該Cookie一同提交給服務器。服務器經過檢查該Cookie來獲取用戶狀態。

咱們經過看下servlet-api中Cookie類的定義及屬性,來更加具體的瞭解Cookie。

Cookie在servlet-api中的定義

public class Cookie implements Cloneable, Serializable {
    private static final long serialVersionUID = -6454587001725327448L;
    private static final String TSPECIALS;
    private static final String LSTRING_FILE =
    "javax.servlet.http.LocalStrings";
    private static ResourceBundle lStrings =
    ResourceBundle.getBundle("javax.servlet.http.LocalStrings");
    private String name;
    private String value;
    private String comment;
    private String domain;
    private int maxAge = -1;
    private String path;
    private boolean secure;
    private int version = 0;
    private boolean isHttpOnly = false;
    //....省略其餘方法
}
複製代碼

Cookie屬性

  • name

    cookie的名字,Cookie一旦建立,名稱便不可更改

  • value

    cookie值

  • comment

    該Cookie的用處說明。瀏覽器顯示Cookie信息的時候顯示該說明

  • domain

    能夠訪問該Cookie的域名。若是設置爲「.baidu.com」,則全部以「baidu.com」結尾的域名均可以訪問該Cookie;第一個字符必須爲「.」

  • maxAge

    Cookie失效的時間,單位秒。

    • 正數,則超過maxAge秒以後失效。
    • 負數,該Cookie爲臨時Cookie,關閉瀏覽器即失效,瀏覽器也不會以任何形式保存該Cookie。
    • 爲0,表示刪除該Cookie。
  • path

    該Cookie的使用路徑。例如:

    • path=/,說明本域名下contextPath均可以訪問該Cookie。
    • path=/app/,則只有contextPath爲「/app」的程序能夠訪問該Cookie

    path設置時,其以「/」結尾.

  • secure

    該Cookie是否僅被使用安全協議傳輸。這裏的安全協議包括HTTPS,SSL等。默認爲false。

  • version

    該Cookie使用的版本號。

    • 0 表示遵循Netscape的Cookie規範,目前大多數用的都是這種規範;
    • 1 表示遵循W3C的RFC2109規範;規範過於嚴格,實施起來很難。

    在servlet規範中默認是0;

  • isHttpOnly

    HttpOnly屬性是用來限制非HTTP協議程序接口對客戶端Cookie進行訪問;也就是說若是想要在客戶端取到httponly的Cookie的惟一方法就是使用AJAX,將取Cookie的操做放到服務端,接收客戶端發送的ajax請求後將取值結果經過HTTP返回客戶端。這樣能有效的防止XSS攻擊。

上述的這些屬性,除了name與value屬性會被提交外,其餘的屬性對於客戶端來講都是不可讀的,也是不可被提交的。

建立Cookie

Cookie cookie = new Cookie("cookieSessionId","qwertyuiop");
cookie.setDomain(".baidu.com");             // 設置域名
cookie.setPath("/");                        // 設置路徑
cookie.setMaxAge(Integer.MAX_VALUE);        // 設置有效期爲永久
response.addCookie(cookie);                 // 回寫到客戶端
複製代碼

建立Cookie只能經過上述方式來建立,由於在Cookie類中只提供了這樣一個構造函數。

//Cookie的構造函數
public Cookie(String name, String value) {
    if (name != null && name.length() != 0) {
        //判斷下是否是token
        //判斷是否是和Cookie的屬性字段重複
        if (this.isToken(name) && !name.equalsIgnoreCase("Comment") &&
        !name.equalsIgnoreCase("Discard") &&
        !name.equalsIgnoreCase("Domain") &&
        !name.equalsIgnoreCase("Expires") &&
        !name.equalsIgnoreCase("Max-Age") &&
        !name.equalsIgnoreCase("Path") &&
        !name.equalsIgnoreCase("Secure") &&
        !name.equalsIgnoreCase("Version") && !name.startsWith("$")) {
            this.name = name;
            this.value = value;
        } else {
            String errMsg =
            lStrings.getString("err.cookie_name_is_token");
            Object[] errArgs = new Object[]{name};
            errMsg = MessageFormat.format(errMsg, errArgs);
            throw new IllegalArgumentException(errMsg);
        }
    } else {
        throw new IllegalArgumentException(lStrings.getString
        ("err.cookie_name_blank"));
    }
}
複製代碼

Cookie更新

在源碼中能夠知道,Cookie自己並無提供修改的方法;在實際應用中,通常經過使用相同name的Cookie來覆蓋原來的Cookie,以達到更新的目的。

可是這個修改的前提是須要具備相同domain,path的 Set-Cookie 消息頭

Cookie cookie = new Cookie("cookieSessionId","new-qwertyuiop");
response.addCookie(cookie);
複製代碼

Cookie刪除

與Cookie更新同樣,Cookie自己也沒有提供刪除的方法;可是從前面分析Cookie屬性時瞭解到,刪除Cookie能夠經過將maxAge設置爲0便可。

Cookie cookie = new Cookie("cookieSessionId","new-qwertyuiop");
cookie.setMaxAge(0);
response.addCookie(cookie);
複製代碼

上面的刪除是咱們本身可控的;可是也存在一些咱們不可控或者說無心識狀況下的刪除操做:

  • 若是maxAge是負值,則cookie在瀏覽器關閉時被刪除
  • 持久化cookie在到達失效日期時會被刪除
  • 瀏覽器中的 cookie 數量達到上限,那麼 cookie 會被刪除覺得新建的 cookie 建立空間。

其實不少狀況下,咱們關注的都是後者。關於數量上限後面會說到。

從請求中獲取Cookie

Cookie[] cookies = request.getCookies();
複製代碼

Cookie同源與跨域

咱們知道瀏覽器的同源策略:

URL由協議、域名、端口和路徑組成,若是兩個URL的協議、域名和端口相同,則表示他們同源。瀏覽器的同源策略,限制了來自不一樣源的"document"或腳本,對當前"document"讀取或設置某些屬性。

對於Cookie來講,Cookie的同源只關注域名,是忽略協議和端口的。因此通常狀況下,https://localhost:80/和http://localhost:8080/的Cookie是共享的。

Cookie是不可跨域的;在沒有通過任何處理的狀況下,二級域名不一樣也是不行的。(wenku.baidu.com和baike.baidu.com)。

Cookie數量&大小限制及處理策略

IE6.0 IE7.0/8.0 Opera FF Safari Chrome
個數/個 20/域 50/域 30/域 50/域 無限制 53/域
大小/Byte 4095 4095 4096 4097 4097 4097

注:數據來自網絡,僅供參考

由於瀏覽器對於Cookie在數量上是有限制的,若是超過了天然會有一些剔除策略。在這篇文章中Browser cookie restrictions提到的剔除策略以下:

The least recently used (LRU) approach automatically kicks out the oldest cookie when the cookie limit has been reached in order to allow the newest cookie some space. Internet Explorer and Opera use this approach.

最近最少使用(LRU)方法:在達到cookie限制時自動地剔除最老的cookie,以便騰出空間給許最新的cookie。Internet Explorer和Opera使用這種方法。

Firefox does something strange: it seems to randomly decide which cookies to keep although the last cookie set is always kept. There doesn’t seem to be any scheme it’s following at all. The takeaway? Don’t go above the cookie limit in Firefox.

Firefox決定隨機刪除Cookie集中的一個Cookie,並無什麼章法。因此最好不要超過Firefox中的Cookie限制。

超過大小長度的話就是直接被截取丟棄;

Session

Cookie機制彌補了HTTP協議無狀態的不足。在Session出現以前,基本上全部的網站都採用Cookie來跟蹤會話。

與Cookie不一樣的是,session是以服務端保存狀態的。

session機制原理

當客戶端請求建立一個session的時候,服務器會先檢查這個客戶端的請求裏是否已包含了一個session標識 - sessionId,

  • 若是已包含這個sessionId,則說明之前已經爲此客戶端建立過session,服務器就按照sessionId把這個session檢索出來使用(若是檢索不到,可能會新建一個)
  • 若是客戶端請求不包含sessionId,則爲此客戶端建立一個session而且生成一個與此session相關聯的sessionId

sessionId的值通常是一個既不會重複,又不容易被仿造的字符串,這個sessionId將被在本次響應中返回給客戶端保存。保存sessionId的方式大多狀況下用的是cookie。

HttpSession

HttpSession和Cookie同樣,都是javax.servlet.http下面的;Cookie是一個類,它描述了Cookie的不少內部細節。而HttpSession是一個接口,它爲session的實現提供了一些行爲約束。

public interface HttpSession {
    /** * 返回session的建立時間 */
    public long getCreationTime();
    
    /** * 返回一個sessionId,惟一標識 */
    public String getId();
    
    /** *返回客戶端最後一次發送與該 session 會話相關的請求的時間 *自格林尼治標準時間 1970 年 1 月 1 日午夜算起,以毫秒爲單位。 */
    public long getLastAccessedTime();
    
    /** * 返回當前session所在的ServletContext */
    public ServletContext getServletContext();

    public void setMaxInactiveInterval(int interval);

    /** * 返回 Servlet 容器在客戶端訪問時保持 session * 會話打開的最大時間間隔 */
    public int getMaxInactiveInterval();
    
    public HttpSessionContext getSessionContext();

    /** * 返回在該 session會話中具備指定名稱的對象, * 若是沒有指定名稱的對象,則返回 null。 */
    public Object getAttribute(String name);
    
    public Object getValue(String name);

    /** * 返回 String 對象的枚舉,String 對象包含全部綁定到該 session * 會話的對象的名稱。 */    
    public Enumeration<String> getAttributeNames();
    
    public String[] getValueNames();

    public void setAttribute(String name, Object value);

    public void putValue(String name, Object value);

    public void removeAttribute(String name);

    public void removeValue(String name);

    /** * 指示該 session 會話無效,並解除綁定到它上面的任何對象。 */
    public void invalidate();
    
    /** * 若是客戶端不知道該 session 會話,或者若是客戶選擇不參入該 * session 會話,則該方法返回 true。 */
    public boolean isNew();
}
複製代碼

建立session

建立session的方式是經過request來建立;

// 一、建立Session對象
HttpSession session = request.getSession(); 
// 二、建立Session對象
HttpSession session = request.getSession(true); 
複製代碼

這兩種是同樣的;若是session不存在,就新建一個;若是是false的話,標識若是不存在就返回null;

生命週期

session的生命週期指的是從Servlet容器建立session對象到銷燬的過程。Servlet容器會依據session對象設置的存活時間,在達到session時間後將session對象銷燬。session生成後,只要用戶繼續訪問,服務器就會更新session的最後訪問時間,並維護該session。

以前在單進程應用中,session我通常是存在內存中的,不會作持久化操做或者說使用三方的服務來存session信息,如redis。可是在分佈式場景下,這種存在本機內存中的方式顯然是不適用的,由於session沒法共享。這個後面說。

session的有效期

session通常在內存中存放,內存空間自己大小就有必定的侷限性,所以session須要採用一種過時刪除的機制來確保session信息不會一直累積,來防止內存溢出的發生。

session的超時時間能夠經過maxInactiveInterval屬性來設置。

若是咱們想讓session失效的話,也能夠當經過調用session的invalidate()來完成。

分佈式session

首先是爲何會有這樣的概念出現?

先考慮這樣一個問題,如今個人應用須要部署在3臺機器上。是否是出現這樣一種狀況,我第一次登錄,請求去了機器1,而後再機器1上建立了一個session;可是我第二次訪問時,請求被路由到機器2了,可是機器2上並無個人session信息,因此得從新登陸。固然這種能夠經過nginx的IP HASH負載策略來解決。對於同一個IP請求都會去同一個機器。

可是業務發展的愈來愈大,拆分的愈來愈多,機器數不斷增長;很顯然那種方案就不行了。那麼這個時候就須要考慮是否是應該將session信息放在一個獨立的機器上,因此分佈式session要解決的問題其實就是分佈式環境下的session共享的問題。

上圖中的關於session獨立部署的方式有不少種,能夠是一個獨立的數據庫服務,也能夠是一個緩存服務(redis,目前比較經常使用的一種方式,即便用Redis來做爲session緩存服務器)。

參考

  • https://www.cnblogs.com/icelin/p/3974935.html
  • https://www.nczonline.net/blog/2008/05/17/browser-cookie-restrictions/
  • https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE
相關文章
相關標籤/搜索