一文徹底搞懂Cookie、Session、Token到底是什麼
筆者文筆功力尚淺,如有不妥,請慷慨指出,必定感激不盡
Cookie
1洛:大爺,樓上322住的是馬冬梅家吧?
2
3大爺:馬都什麼?
4
5夏洛:馬冬梅。
6
7大爺:什麼都沒啊?
8
9夏洛:馬冬梅啊。
10
11大爺:馬什麼沒?
12
13夏洛:行,大爺你先涼快著吧。
複製程式碼
在瞭解這三個概念之前我們先要了解HTTP是無狀態的Web伺服器,什麼是無狀態呢?就像上面夏洛特煩惱中經典的一幕對話一樣,一次對話完成後下一次對話完全不知道上一次對話發生了什麼。如果在Web伺服器中只是用來管理靜態檔案還好說,對方是誰並不重要,把檔案從磁碟中讀取出來發出去即可。但是隨著網路的不斷髮展,比如電商中的購物車只有記住了使用者的身份才能夠執行接下來的一系列動作。所以此時就需要我們無狀態
那麼Web伺服器是如何記住一些事情呢?既然Web伺服器記不住東西,那麼我們就在外部想辦法記住,相當於伺服器給每個客戶端都貼上了一個小紙條。上面記錄了伺服器給我們返回的一些資訊。然後伺服器看到這張小紙條就知道我們是誰了。那麼Cookie
是誰產生的呢?Cookies是由伺服器產生的。接下來我們描述一下Cookie
產生的過程
- 瀏覽器第一次訪問服務端時,伺服器此時肯定不知道他的身份,所以建立一個獨特的身份標識資料,格式為
key=value
,放入到Set-Cookie
欄位裡,隨著響應報文發給瀏覽器。 - 瀏覽器看到有
Set-Cookie
欄位以後就知道這是伺服器給的身份標識,於是就儲存起來,下次請求時會自動將此key=value
Cookie
欄位中發給服務端。 - 服務端收到請求報文後,發現
Cookie
欄位中有值,就能根據此值識別使用者的身份然後提供個性化的服務。
接下來我們用程式碼演示一下伺服器是如何生成,我們自己搭建一個後臺伺服器,這裡我用的是SpringBoot搭建的,並且寫入SpringMVC的程式碼如下。
1@RequestMapping("/testCookies")
2public String cookies(HttpServletResponse response){
3 response.addCookie(new Cookie("testUser","xxxx"));
4 return "cookies" ;
5}
複製程式碼
專案啟動以後我們輸入路徑http://localhost:8005/testCookies
,然後檢視發的請求。可以看到下面那張圖使我們首次訪問伺服器時傳送的請求,可以看到伺服器返回的響應中有Set-Cookie
欄位。而裡面的key=value
值正是我們伺服器中設定的值。
接下來我們再次重新整理這個頁面可以看到在請求體中已經設定了Cookie
欄位,並且將我們的值也帶過去了。這樣伺服器就能夠根據Cookie
中的值記住我們的資訊了。
接下來我們換一個請求呢?是不是Cookie
也會帶過去呢?接下來我們輸入路徑http://localhost:8005
請求。我們可以看到Cookie
欄位還是被帶過去了。
那麼瀏覽器的Cookie
是存放在哪呢?如果是使用的是Chrome
瀏覽器的話,那麼可以按照下面步驟。
- 在計算機開啟
Chrome
- 在右上角,一次點選
更多
圖示->設定
- 在底部,點選
高階
- 在
隱私設定和安全性
下方,點選網站設定 - 依次點選
Cookie
->檢視所有Cookie和網站資料
然後可以根據域名進行搜尋所管理的Cookie
資料。所以是瀏覽器替你管理了Cookie
的資料,如果此時你換成了Firefox
等其他的瀏覽器,因為Cookie
剛才是儲存在Chrome
裡面的,所以伺服器又蒙圈了,不知道你是誰,就會給Firefox
再次貼上小紙條。
Cookie中的引數設定
說到這裡,應該知道了Cookie
就是伺服器委託瀏覽器儲存在客戶端裡的一些資料,而這些資料通常都會記錄使用者的關鍵識別資訊。所以Cookie
需要用一些其他的手段用來保護,防止外洩或者竊取,這些手段就是Cookie
的屬性。
引數名 | 作用 | 後端設定方法 |
---|---|---|
Max-Age | 設定cookie的過期時間,單位為秒 | cookie.setMaxAge(10) |
Domain | 指定了Cookie所屬的域名 | cookie.setDomain("") |
指定了Cookie所屬的路徑 | cookie.setPath(""); | |
告訴瀏覽器此Cookie只能靠瀏覽器Http協議傳輸,禁止其他方式訪問 | cookie.setHttpOnly(true) | |
告訴瀏覽器此Cookie只能在Https安全協議中傳輸,如果是Http則禁止傳輸 | cookie.setSecure(true) |
下面我就簡單演示一下這幾個引數的用法及現象。
Path
設定為cookie.setPath("/testCookies")
,接下來我們訪問http://localhost:8005/testCookies
,我們可以看到在左邊和我們指定的路徑是一樣的,所以Cookie
才在請求頭中出現,接下來我們訪問http://localhost:8005
,我們發現沒有Cookie
欄位了,這就是Path
控制的路徑。
Domain
設定為cookie.setDomain("localhost")
,接下來我們訪問http://localhost:8005/testCookies
我們發現下圖中左邊的是有Cookie
的欄位的,但是我們訪問http://172.16.42.81:8005/testCookies
,看下圖的右邊可以看到沒有Cookie
的欄位了。這就是Domain
控制的域名傳送Cookie
。
接下來的幾個引數就不一一演示了,相信到這裡大家應該對Cookie
有一些瞭解了。
Session
Cookie是儲存在客戶端方,Session是儲存在服務端方,客戶端只儲存
SessionId
在上面我們瞭解了什麼是Cookie
,既然瀏覽器已經通過Cookie
實現了有狀態這一需求,那麼為什麼又來了一個Session
呢?這裡我們想象一下,如果將賬戶的一些資訊都存入Cookie
中的話,一旦資訊被攔截,那麼我們所有的賬戶資訊都會丟失掉。所以就出現了Session
,在一次會話中將重要資訊儲存在Session
中,瀏覽器只記錄SessionId
一個SessionId
對應一次會話請求。
1@RequestMapping("/testSession")
2@ResponseBody
3public String testSession(HttpSession session){
4 session.setAttribute("testSession",38); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-string">"this is my session");
5 "testSession";
6}
7
9"/testGetSession")
1011public String testGetSession(HttpSession session){
12 Object testSession = session.getAttribute("testSession");
13 return String.valueOf(testSession);
14}
複製程式碼
這裡我們寫一個新的方法來測試Session
是如何產生的,我們在請求引數中加上HttpSession session
,然後再瀏覽器中輸入http://localhost:8005/testSession
進行訪問可以看到在伺服器的返回頭中在Cookie
中生成了一個SessionId
。然後瀏覽器記住此SessionId
下次訪問時可以帶著此Id,然後就能根據此Id找到儲存在服務端的資訊了。
此時我們訪問路徑http://localhost:8005/testGetSession
,發現得到了我們上面儲存在Session
中的資訊。那麼Session
什麼時候過期呢?
- 客戶端:和
Cookie
過期一致,如果沒設定,預設是關了瀏覽器就沒了,即再開啟瀏覽器的時候初次請求頭中是沒有SessionId
了。 - 服務端:服務端的過期是真的過期,即伺服器端的
Session
儲存的資料結構多久不可用了,預設是30分鐘。
既然我們知道了Session
是在服務端進行管理的,那麼或許你們看到這有幾個疑問,Session
是在在哪建立的?Session
是儲存在什麼資料結構中?接下來帶領大家一起看一下Session
是如何被管理的。
Session
的管理是在容器中被管理的,什麼是容器呢?Tomcat
、Jetty
等都是容器。接下來我們拿最常用的Tomcat
為例來看下Tomcat
是如何管理Session
的。在ManageBase
的createSession
是用來建立Session
的。
@Override
2public Session createSession(String sessionId) {
3 //首先判斷Session數量是不是到了最大值,最大Session數可以通過引數設定
4 if ((maxActiveSessions >= 0) &&
5 (getActiveSessions() >= maxActiveSessions)) {
6 rejectedSessions++;
7 throw new TooManyActiveSessionsException(
8 sm.getString("managerBase.createSession.ise"),
9 maxActiveSessions);
10 }
11
12 // 重用或者建立一個新的Session物件,請注意在Tomcat中就是StandardSession
13 // 它是HttpSession的具體實現類,而HttpSession是Servlet規範中定義的介面
14 Session session = createEmptySession();
15
16
17 // 初始化新Session的值
18 session.setNew(true);
19 session.setValid(20 session.setCreationTime(System.currentTimeMillis());
21 // 設定Session過期時間是30分鐘
22 session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
23 String id = sessionId;
24 if (id == null) {
25 id = generateSessionId();
26 }
27 session.setId(id);// 這裡會將Session新增到ConcurrentHashMap中
28 sessionCounter++;
29
30 //將建立時間新增到LinkedList中,並且把最先新增的時間移除
31 //主要還是方便清理過期Session
32 SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
33 synchronized (sessionCreationTiming) {
34 sessionCreationTiming.add(timing);
35 sessionCreationTiming.poll();
36 }
37 return session
38}
複製程式碼
到此我們明白了Session
是如何創建出來的,創建出來後Session
會被儲存到一個ConcurrentHashMap
中。可以看StandardSession
類。
1protected Map<String, Session> sessions = new ConcurrentHashMap<>();
複製程式碼
到這裡大家應該對Session
有簡單的瞭解了。
Session是儲存在Tomcat的容器中,所以如果後端機器是多臺的話,因此多個機器間是無法共享Session的,此時可以使用Spring提供的分散式Session的解決方案,是將Session放在了Redis中。
Token
Session
是將要驗證的資訊儲存在服務端,並以SessionId
和資料進行對應,SessionId
由客戶端儲存,在請求時將SessionId
也帶過去,因此實現了狀態的對應。而Token
是在服務端將使用者資訊經過Base64Url編碼過後傳給在客戶端,每次使用者請求的時候都會帶上這一段資訊,因此服務端拿到此資訊進行解密後就知道此使用者是誰了,這個方法叫做JWT(Json Web Token)。
Token
相比較於Session
的優點在於,當後端系統有多臺時,由於是客戶端訪問時直接帶著資料,因此無需做共享資料的操作。
Token的優點
- 簡潔:可以通過
URL
,POST
引數或者是在HTTP
頭引數傳送,因為資料量小,傳輸速度也很快 - 自包含:由於串包含了使用者所需要的資訊,避免了多次查詢資料庫
- 因為Token是以Json的形式儲存在客戶端的,所以JWT是跨語言的
- 不需要在服務端儲存會話資訊,特別適用於分散式微服務
JWT的結構
實際的JWT大概長下面的這樣,它是一個很長的字串,中間用.
分割成三部分
JWT是有三部分組成的
Header
是一個Json物件,描述JWT的元資料,通常是下面這樣子的
1{
2 "alg": "HS256",155); padding-right: 20px; word-spacing: 0px; word-wrap: inherit !important; word-break: inherit !important;" class="linenum hljs-number">3 "typ": "JWT"
4}
複製程式碼
上面程式碼中,alg屬性表示簽名的演演算法(algorithm),預設是 HMAC SHA256(寫成 HS256);typ屬性表示這個令牌(token)的型別(type),JWT 令牌統一寫為JWT。
最後,將上面的 JSON 物件使用 Base64URL 演演算法轉成字串。
JWT 作為一個令牌(token),有些場合可能會放到 URL(比如 api.example.com/?token=xxx)。Base64 有三個字元+、/和=,在 URL 裡面有特殊含義,所以要被替換掉:=被省略、+替換成-,/替換成_ 。這就是 Base64URL 演演算法。
Payload
Payload部分也是一個Json物件,用來存放實際需要傳輸的資料,JWT官方規定了下面幾個官方的欄位供選用。
- iss (issuer):簽發人
- exp (expiration time):過期時間
- sub (subject):主題
- aud (audience):受眾
- nbf (Not Before):生效時間
- iat (Issued At):簽發時間
- jti (JWT ID):編號
當然除了官方提供的這幾個欄位我們也能夠自己定義私有欄位,下面就是一個例子
"name": "xiaoMing",47); word-wrap: inherit !important; word-break: inherit !important;" class="hljs-attr">"age": 14
4}
複製程式碼
預設情況下JWT是不加密的,任何人只要在網上進行Base64解碼就可以讀到資訊,所以一般不要將祕密資訊放在這個部分。這個Json物件也要用Base64URL
演演算法轉成字串
Signature
Signature部分是對前面的兩部分的資料進行簽名,防止資料篡改。
首先需要定義一個祕鑰,這個祕鑰只有伺服器才知道,不能洩露給使用者,然後使用Header中指定的簽名演演算法(預設情況是HMAC SHA256),算出簽名以後將Header、Payload、Signature三部分拼成一個字串,每個部分用.
分割開來,就可以返給使用者了。
HS256可以使用單個金鑰為給定的資料樣本建立簽名。當訊息與簽名一起傳輸時,接收方可以使用相同的金鑰來驗證簽名是否與訊息匹配。