session和cookie

山有木xi發表於2020-02-25

轉載自: 聊一聊session和cookie

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

從http協議的無狀態性說起

HTTP是一種無狀態協議。關於這個無狀態之前我也不太理解,因為HTTP底層是TCP,既然是TCP,就是長連線,這個過程是保持連線狀態的,又為什麼說http是無狀態的呢?先來搞清楚這兩個概念:

無連線和無狀態

無連線

每次連線只處理一個請求,服務端處理完客戶端一次請求,等到客戶端作出迴應之後便斷開連線;

無狀態

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

無連線的維度是連線,無狀態的維度是請求;http是基於tcp的,而從http1.1開始預設使用持久連線;在這個連線過程中,客戶端可以向服務端傳送多次請求,但是各個請求之間的並沒有什麼聯絡;這樣來考慮,就很好理解無狀態這個概念了。

持久連線

持久連線,本質上是客戶端與伺服器通訊的時候,建立一個持久化的TCP連線,這個連線不會隨著請求結束而關閉,通常會保持連線一段時間。

現有的持久連線型別有兩種:HTTP/1.0+的keep-alive和HTTP/1.1的persistent。

  • HTTP/1.0+的keep-alive

先來開一張圖: ![](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1280" height="150"></svg>) 這張圖是請求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中的定義

publicclassCookieimplementsCloneable, Serializable{
    privatestaticfinallong serialVersionUID = -6454587001725327448L;
    privatestaticfinal String TSPECIALS;
    privatestaticfinal String LSTRING_FILE =
    "javax.servlet.http.LocalStrings";
    privatestatic ResourceBundle lStrings =
    ResourceBundle.getBundle("javax.servlet.http.LocalStrings");
    private String name;
    private String value;
    private String comment;
    private String domain;
    privateint maxAge = -1;
    private String path;
    privateboolean secure;
    privateint version = 0;
    privateboolean 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的建構函式publicCookie(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);
            thrownew IllegalArgumentException(errMsg);
        }
    } else {
        thrownew 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.0IE7.0/8.0OperaFFSafariChrome個數/個20/域50/域30/域50/域無限制53/域大小/Byte409540954096409740974097 注:資料來自網路,僅供參考

因為瀏覽器對於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的實現提供了一些行為約束。

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

    publicvoidsetMaxInactiveInterval(int interval);

    /**
     * 返回 Servlet 容器在客戶端訪問時保持 session
     * 會話開啟的最大時間間隔
     */publicintgetMaxInactiveInterval();
    
    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();

    publicvoidsetAttribute(String name, Object value);

    publicvoidputValue(String name, Object value);

    publicvoidremoveAttribute(String name);

    publicvoidremoveValue(String name);

    /**
     * 指示該 session 會話無效,並解除繫結到它上面的任何物件。
     */publicvoidinvalidate();
    
    /**
     * 如果客戶端不知道該 session 會話,或者如果客戶選擇不參入該
     * session 會話,則該方法返回 true。
     */publicbooleanisNew();
}
複製程式碼

建立session

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

// 1、建立Session物件
HttpSession session = request.getSession(); 
// 2、建立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和cookie
上圖中的關於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

相關文章