Cookie
洛:大爺,樓上322住的是馬冬梅家吧?
大爺:馬都什么?
夏洛:馬冬梅。
大爺:什么都沒(méi)???
夏洛:馬冬梅啊。
大爺:馬什么沒(méi)?
夏洛:行,大爺你先涼快著吧。
在了解這三個(gè)概念之前我們先要了解HTTP是無(wú)狀態(tài)的Web服務(wù)器,什么是無(wú)狀態(tài)呢?就像上面夏洛特?zé)乐薪?jīng)典的一幕對(duì)話一樣,一次對(duì)話完成后下一次對(duì)話完全不知道上一次對(duì)話發(fā)生了什么。如果在Web服務(wù)器中只是用來(lái)管理靜態(tài)文件還好說(shuō),對(duì)方是誰(shuí)并不重要,把文件從磁盤中讀取出來(lái)發(fā)出去即可。但是隨著網(wǎng)絡(luò)的不斷發(fā)展,比如電商中的購(gòu)物車只有記住了用戶的身份才能夠執(zhí)行接下來(lái)的一系列動(dòng)作。所以此時(shí)就需要我們無(wú)狀態(tài)的服務(wù)器記住一些事情。
那么Web服務(wù)器是如何記住一些事情呢?既然Web服務(wù)器記不住東西,那么我們就在外部想辦法記住,相當(dāng)于服務(wù)器給每個(gè)客戶端都貼上了一個(gè)小紙條。上面記錄了服務(wù)器給我們返回的一些信息。然后服務(wù)器看到這張小紙條就知道我們是誰(shuí)了。那么Cookie是誰(shuí)產(chǎn)生的呢?Cookies是由服務(wù)器產(chǎn)生的。接下來(lái)我們描述一下Cookie產(chǎn)生的過(guò)程
瀏覽器第一次訪問(wèn)服務(wù)端時(shí),服務(wù)器此時(shí)肯定不知道他的身份,所以創(chuàng)建一個(gè)獨(dú)特的身份標(biāo)識(shí)數(shù)據(jù),格式為key=value,放入到Set-Cookie字段里,隨著響應(yīng)報(bào)文發(fā)給瀏覽器。
瀏覽器看到有Set-Cookie字段以后就知道這是服務(wù)器給的身份標(biāo)識(shí),于是就保存起來(lái),下次請(qǐng)求時(shí)會(huì)自動(dòng)將此key=value值放入到Cookie字段中發(fā)給服務(wù)端。
服務(wù)端收到請(qǐng)求報(bào)文后,發(fā)現(xiàn)Cookie字段中有值,就能根據(jù)此值識(shí)別用戶的身份然后提供個(gè)性化的服務(wù)。

接下來(lái)我們用代碼演示一下服務(wù)器是如何生成,我們自己搭建一個(gè)后臺(tái)服務(wù)器,這里我用的是SpringBoot搭建的,并且寫(xiě)入SpringMVC的代碼如下。
@RequestMapping("/testCookies")
public String cookies(HttpServletResponse response){
response.addCookie(new Cookie("testUser","xxxx"));
return "cookies";
}
項(xiàng)目啟動(dòng)以后我們輸入路徑http://localhost:8005/testCookies,然后查看發(fā)的請(qǐng)求。可以看到下面那張圖使我們首次訪問(wèn)服務(wù)器時(shí)發(fā)送的請(qǐng)求,可以看到服務(wù)器返回的響應(yīng)中有Set-Cookie字段。而里面的key=value值正是我們服務(wù)器中設(shè)置的值。

接下來(lái)我們?cè)俅嗡⑿逻@個(gè)頁(yè)面可以看到在請(qǐng)求體中已經(jīng)設(shè)置了Cookie字段,并且將我們的值也帶過(guò)去了。這樣服務(wù)器就能夠根據(jù)Cookie中的值記住我們的信息了。

接下來(lái)我們換一個(gè)請(qǐng)求呢?是不是Cookie也會(huì)帶過(guò)去呢?接下來(lái)我們輸入路徑http://localhost:8005請(qǐng)求。我們可以看到Cookie字段還是被帶過(guò)去了。

那么瀏覽器的Cookie是存放在哪呢?如果是使用的是Chrome瀏覽器的話,那么可以按照下面步驟。
在計(jì)算機(jī)打開(kāi)Chrome
在右上角,一次點(diǎn)擊更多圖標(biāo)->設(shè)置
在底部,點(diǎn)擊高級(jí)
在隱私設(shè)置和安全性下方,點(diǎn)擊網(wǎng)站設(shè)置
依次點(diǎn)擊Cookie->查看所有Cookie和網(wǎng)站數(shù)據(jù)
然后可以根據(jù)域名進(jìn)行搜索所管理的Cookie數(shù)據(jù)。所以是瀏覽器替你管理了Cookie的數(shù)據(jù),如果此時(shí)你換成了Firefox等其他的瀏覽器,因?yàn)镃ookie剛才是存儲(chǔ)在Chrome里面的,所以服務(wù)器又蒙圈了,不知道你是誰(shuí),就會(huì)給Firefox再次貼上小紙條。

Cookie中的參數(shù)設(shè)置
說(shuō)到這里,應(yīng)該知道了Cookie就是服務(wù)器委托瀏覽器存儲(chǔ)在客戶端里的一些數(shù)據(jù),而這些數(shù)據(jù)通常都會(huì)記錄用戶的關(guān)鍵識(shí)別信息。所以Cookie需要用一些其他的手段用來(lái)保護(hù),防止外泄或者竊取,這些手段就是Cookie的屬性。
參數(shù)名作用后端設(shè)置方法
Max-Age設(shè)置cookie的過(guò)期時(shí)間,單位為秒cookie.setMaxAge(10)
Domain指定了Cookie所屬的域名cookie.setDomain("")
Path指定了Cookie所屬的路徑cookie.setPath("");
HttpOnly告訴瀏覽器此Cookie只能靠瀏覽器Http協(xié)議傳輸,禁止其他方式訪問(wèn)cookie.setHttpOnly(true)
Secure告訴瀏覽器此Cookie只能在Https安全協(xié)議中傳輸,如果是Http則禁止傳輸cookie.setSecure(true)
下面我就簡(jiǎn)單演示一下這幾個(gè)參數(shù)的用法及現(xiàn)象。
Path
設(shè)置為cookie.setPath("/testCookies"),接下來(lái)我們?cè)L問(wèn)http://localhost:8005/testCookies,我們可以看到在左邊和我們指定的路徑是一樣的,所以Cookie才在請(qǐng)求頭中出現(xiàn),接下來(lái)我們?cè)L問(wèn)http://localhost:8005,我們發(fā)現(xiàn)沒(méi)有Cookie字段了,這就是Path控制的路徑。

Domain
設(shè)置為cookie.setDomain("localhost"),接下來(lái)我們?cè)L問(wèn)http://localhost:8005/testCookies我們發(fā)現(xiàn)下圖中左邊的是有Cookie的字段的,但是我們?cè)L問(wèn)http://172.16.42.81:8005/testCookies,看下圖的右邊可以看到?jīng)]有Cookie的字段了。這就是Domain控制的域名發(fā)送Cookie。

接下來(lái)的幾個(gè)參數(shù)就不一一演示了,相信到這里大家應(yīng)該對(duì)Cookie有一些了解了。
Session
> Cookie是存儲(chǔ)在客戶端方,Session是存儲(chǔ)在服務(wù)端方,客戶端只存儲(chǔ)SessionId
在上面我們了解了什么是Cookie,既然瀏覽器已經(jīng)通過(guò)Cookie實(shí)現(xiàn)了有狀態(tài)這一需求,那么為什么又來(lái)了一個(gè)Session呢?這里我們想象一下,如果將賬戶的一些信息都存入Cookie中的話,一旦信息被攔截,那么我們所有的賬戶信息都會(huì)丟失掉。所以就出現(xiàn)了Session,在一次會(huì)話中將重要信息保存在Session中,瀏覽器只記錄SessionId一個(gè)SessionId對(duì)應(yīng)一次會(huì)話請(qǐng)求。

@RequestMapping("/testSession")
@ResponseBody
public String testSession(HttpSession session){
session.setAttribute("testSession","this is my session");
return "testSession";
}
@RequestMapping("/testGetSession")
@ResponseBody
public String testGetSession(HttpSession session){
Object testSession = session.getAttribute("testSession");
return String.valueOf(testSession);
}
這里我們寫(xiě)一個(gè)新的方法來(lái)測(cè)試Session是如何產(chǎn)生的,我們?cè)谡?qǐng)求參數(shù)中加上HttpSession session,然后再瀏覽器中輸入http://localhost:8005/testSession進(jìn)行訪問(wèn)可以看到在服務(wù)器的返回頭中在Cookie中生成了一個(gè)SessionId。然后瀏覽器記住此SessionId下次訪問(wèn)時(shí)可以帶著此Id,然后就能根據(jù)此Id找到存儲(chǔ)在服務(wù)端的信息了。

此時(shí)我們?cè)L問(wèn)路徑http://localhost:8005/testGetSession,發(fā)現(xiàn)得到了我們上面存儲(chǔ)在Session中的信息。那么Session什么時(shí)候過(guò)期呢?
客戶端:和Cookie過(guò)期一致,如果沒(méi)設(shè)置,默認(rèn)是關(guān)了瀏覽器就沒(méi)了,即再打開(kāi)瀏覽器的時(shí)候初次請(qǐng)求頭中是沒(méi)有SessionId了。
服務(wù)端:服務(wù)端的過(guò)期是真的過(guò)期,即服務(wù)器端的Session存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)多久不可用了,默認(rèn)是30分鐘。

既然我們知道了Session是在服務(wù)端進(jìn)行管理的,那么或許你們看到這有幾個(gè)疑問(wèn),Session是在在哪創(chuàng)建的?Session是存儲(chǔ)在什么數(shù)據(jù)結(jié)構(gòu)中?接下來(lái)帶領(lǐng)大家一起看一下Session是如何被管理的。
Session的管理是在容器中被管理的,什么是容器呢?Tomcat、Jetty等都是容器。接下來(lái)我們拿最常用的Tomcat為例來(lái)看下Tomcat是如何管理Session的。在ManageBase的createSession是用來(lái)創(chuàng)建Session的。
@Override
public Session createSession(String sessionId) {
//首先判斷Session數(shù)量是不是到了最大值,最大Session數(shù)可以通過(guò)參數(shù)設(shè)置
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString("managerBase.createSession.ise"),
maxActiveSessions);
}
// 重用或者創(chuàng)建一個(gè)新的Session對(duì)象,請(qǐng)注意在Tomcat中就是StandardSession
// 它是HttpSession的具體實(shí)現(xiàn)類,而HttpSession是Servlet規(guī)范中定義的接口
Session session = createEmptySession();
// 初始化新Session的值
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
// 設(shè)置Session過(guò)期時(shí)間是30分鐘
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id);// 這里會(huì)將Session添加到ConcurrentHashMap中
sessionCounter++;
//將創(chuàng)建時(shí)間添加到LinkedList中,并且把最先添加的時(shí)間移除
//主要還是方便清理過(guò)期Session
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return session
}
到此我們明白了Session是如何創(chuàng)建出來(lái)的,創(chuàng)建出來(lái)后Session會(huì)被保存到一個(gè)ConcurrentHashMap中??梢钥碨tandardSession類。
protected Map sessions = new ConcurrentHashMap<>();
到這里大家應(yīng)該對(duì)Session有簡(jiǎn)單的了解了。
> Session是存儲(chǔ)在Tomcat的容器中,所以如果后端機(jī)器是多臺(tái)的話,因此多個(gè)機(jī)器間是無(wú)法共享Session的,此時(shí)可以使用Spring提供的分布式Session的解決方案,是將Session放在了Redis中。
Token
Session是將要驗(yàn)證的信息存儲(chǔ)在服務(wù)端,并以SessionId和數(shù)據(jù)進(jìn)行對(duì)應(yīng),SessionId由客戶端存儲(chǔ),在請(qǐng)求時(shí)將SessionId也帶過(guò)去,因此實(shí)現(xiàn)了狀態(tài)的對(duì)應(yīng)。而Token是在服務(wù)端將用戶信息經(jīng)過(guò)Base64Url編碼過(guò)后傳給在客戶端,每次用戶請(qǐng)求的時(shí)候都會(huì)帶上這一段信息,因此服務(wù)端拿到此信息進(jìn)行解密后就知道此用戶是誰(shuí)了,這個(gè)方法叫做JWT(Json Web Token)。

>?Token相比較于Session的優(yōu)點(diǎn)在于,當(dāng)后端系統(tǒng)有多臺(tái)時(shí),由于是客戶端訪問(wèn)時(shí)直接帶著數(shù)據(jù),因此無(wú)需做共享數(shù)據(jù)的操作。
Token的優(yōu)點(diǎn)
簡(jiǎn)潔:可以通過(guò)URL,POST參數(shù)或者是在HTTP頭參數(shù)發(fā)送,因?yàn)閿?shù)據(jù)量小,傳輸速度也很快
自包含:由于串包含了用戶所需要的信息,避免了多次查詢數(shù)據(jù)庫(kù)
因?yàn)門oken是以Json的形式保存在客戶端的,所以JWT是跨語(yǔ)言的
不需要在服務(wù)端保存會(huì)話信息,特別適用于分布式微服務(wù)
JWT的結(jié)構(gòu)
實(shí)際的JWT大概長(zhǎng)下面的這樣,它是一個(gè)很長(zhǎng)的字符串,中間用.分割成三部分

JWT是有三部分組成的
Header
是一個(gè)Json對(duì)象,描述JWT的元數(shù)據(jù),通常是下面這樣子的
{
"alg": "HS256",
"typ": "JWT"
}
上面代碼中,alg屬性表示簽名的算法(algorithm),默認(rèn)是 HMAC SHA256(寫(xiě)成 HS256);typ屬性表示這個(gè)令牌(token)的類型(type),JWT 令牌統(tǒng)一寫(xiě)為JWT。 最后,將上面的 JSON 對(duì)象使用 Base64URL 算法轉(zhuǎn)成字符串。
> JWT 作為一個(gè)令牌(token),有些場(chǎng)合可能會(huì)放到 URL(比如 api.example.com/?token=xxx)。Base64 有三個(gè)字符+、/和=,在 URL 里面有特殊含義,所以要被替換掉:=被省略、+替換成-,/替換成_ 。這就是 Base64URL 算法。
Payload
Payload部分也是一個(gè)Json對(duì)象,用來(lái)存放實(shí)際需要傳輸?shù)臄?shù)據(jù),JWT官方規(guī)定了下面幾個(gè)官方的字段供選用。
iss (issuer):簽發(fā)人
exp (expiration time):過(guò)期時(shí)間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時(shí)間
iat (Issued At):簽發(fā)時(shí)間
jti (JWT ID):編號(hào)
當(dāng)然除了官方提供的這幾個(gè)字段我們也能夠自己定義私有字段,下面就是一個(gè)例子
{
"name": "xiaoMing",
"age": 14
}
默認(rèn)情況下JWT是不加密的,任何人只要在網(wǎng)上進(jìn)行Base64解碼就可以讀到信息,所以一般不要將秘密信息放在這個(gè)部分。這個(gè)Json對(duì)象也要用Base64URL?算法轉(zhuǎn)成字符串
Signature
Signature部分是對(duì)前面的兩部分的數(shù)據(jù)進(jìn)行簽名,防止數(shù)據(jù)篡改。
首先需要定義一個(gè)秘鑰,這個(gè)秘鑰只有服務(wù)器才知道,不能泄露給用戶,然后使用Header中指定的簽名算法(默認(rèn)情況是HMAC SHA256),算出簽名以后將Header、Payload、Signature三部分拼成一個(gè)字符串,每個(gè)部分用.分割開(kāi)來(lái),就可以返給用戶了。
> HS256可以使用單個(gè)密鑰為給定的數(shù)據(jù)樣本創(chuàng)建簽名。當(dāng)消息與簽名一起傳輸時(shí),接收方可以使用相同的密鑰來(lái)驗(yàn)證簽名是否與消息匹配。

Java中如何使用Token
上面我們介紹了關(guān)于JWT的一些概念,接下來(lái)如何使用呢?首先在項(xiàng)目中引入Jar包
compile('io.jsonwebtoken:jjwt:0.9.0')
然后編碼如下
// 簽名算法 ,將對(duì)token進(jìn)行簽名
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 通過(guò)秘鑰簽名JWT
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary("SECRET");
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
Map claimsMap = new HashMap<>();
claimsMap.put("name","xiaoMing");
claimsMap.put("age",14);
JwtBuilder builderWithSercet = Jwts.builder()
.setSubject("subject")
.setIssuer("issuer")
.addClaims(claimsMap)
.signWith(signatureAlgorithm, signingKey);
System.out.printf(builderWithSercet.compact());
發(fā)現(xiàn)輸出的Token如下
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0IiwiaXNzIjoiaXNzdWVyIiwibmFtZSI6InhpYW9NaW5nIiwiYWdlIjoxNH0.3KOWQ-oYvBSzslW5vgB1D-JpCwS-HkWGyWdXCP5l3Ko
此時(shí)在網(wǎng)上隨便找個(gè)Base64解碼的網(wǎng)站就能將信息解碼出來(lái)

總結(jié)
相信大家看到這應(yīng)該對(duì)Cookie、Session、Token有一定的了解了,接下來(lái)再回顧一下重要的知識(shí)點(diǎn)
Cookie是存儲(chǔ)在客戶端的
Session是存儲(chǔ)在服務(wù)端的,可以理解為一個(gè)狀態(tài)列表。擁有一個(gè)唯一會(huì)話標(biāo)識(shí)SessionId??梢愿鶕?jù)SessionId在服務(wù)端查詢到存儲(chǔ)的信息。
Session會(huì)引發(fā)一個(gè)問(wèn)題,即后端多臺(tái)機(jī)器時(shí)Session共享的問(wèn)題,解決方案可以使用Spring提供的框架。
Token類似一個(gè)令牌,無(wú)狀態(tài)的,服務(wù)端所需的信息被Base64編碼后放到Token中,服務(wù)器可以直接解碼出其中的數(shù)據(jù)。
GitHub代碼地址
參考文章
Cookies vs. Tokens: The Definitive Guide
徹底弄懂session,cookie,token
透視HTTP協(xié)議
Manager組件:Tomcat的Session管理機(jī)制解析
JSON Web Token 入門教程
SpringBoot集成JWT實(shí)現(xiàn)token驗(yàn)證
JSON Web Token - 在Web應(yīng)用間安全地傳遞信息