筆者文筆功力尚淺被盈,如有不妥析孽,請慷慨指出搭伤,必定感激不盡
Cookie
洛:大爺只怎,樓上322住的是馬冬梅家吧?
大爺:馬都什么怜俐?
夏洛:馬冬梅身堡。
大爺:什么都沒啊拍鲤?
夏洛:馬冬梅啊贴谎。
大爺:馬什么沒?
夏洛:行季稳,大爺你先涼快著吧擅这。
在了解這三個概念之前我們先要了解HTTP是無狀態(tài)的Web服務(wù)器,什么是無狀態(tài)呢景鼠?就像上面夏洛特?zé)乐薪?jīng)典的一幕對話一樣仲翎,一次對話完成后下一次對話完全不知道上一次對話發(fā)生了什么。如果在Web服務(wù)器中只是用來管理靜態(tài)文件還好說,對方是誰并不重要溯香,把文件從磁盤中讀取出來發(fā)出去即可鲫构。但是隨著網(wǎng)絡(luò)的不斷發(fā)展,比如電商中的購物車只有記住了用戶的身份才能夠執(zhí)行接下來的一系列動作玫坛。所以此時就需要我們無狀態(tài)的服務(wù)器記住一些事情结笨。
那么Web服務(wù)器是如何記住一些事情呢?既然Web服務(wù)器記不住東西湿镀,那么我們就在外部想辦法記住炕吸,相當(dāng)于服務(wù)器給每個客戶端都貼上了一個小紙條。上面記錄了服務(wù)器給我們返回的一些信息勉痴。然后服務(wù)器看到這張小紙條就知道我們是誰了算途。那么Cookie
是誰產(chǎn)生的呢?Cookies是由服務(wù)器產(chǎn)生的蚀腿。接下來我們描述一下Cookie
產(chǎn)生的過程
- 瀏覽器第一次訪問服務(wù)端時,服務(wù)器此時肯定不知道他的身份莉钙,所以創(chuàng)建一個獨特的身份標(biāo)識數(shù)據(jù),格式為
key=value
磁玉,放入到Set-Cookie
字段里时迫,隨著響應(yīng)報文發(fā)給瀏覽器。 - 瀏覽器看到有
Set-Cookie
字段以后就知道這是服務(wù)器給的身份標(biāo)識掠拳,于是就保存起來溺欧,下次請求時會自動將此key=value
值放入到Cookie
字段中發(fā)給服務(wù)端喊熟。 - 服務(wù)端收到請求報文后,發(fā)現(xiàn)
Cookie
字段中有值姐刁,就能根據(jù)此值識別用戶的身份然后提供個性化的服務(wù)芥牌。
接下來我們用代碼演示一下服務(wù)器是如何生成,我們自己搭建一個后臺服務(wù)器聂使,這里我用的是SpringBoot搭建的壁拉,并且寫入SpringMVC的代碼如下拐叉。
@RequestMapping("/testCookies")
public String cookies(HttpServletResponse response){
response.addCookie(new Cookie("testUser","xxxx"));
return "cookies";
}
項目啟動以后我們輸入路徑http://localhost:8005/testCookies
,然后查看發(fā)的請求扇商》锸荩可以看到下面那張圖使我們首次訪問服務(wù)器時發(fā)送的請求,可以看到服務(wù)器返回的響應(yīng)中有Set-Cookie
字段案铺。而里面的key=value
值正是我們服務(wù)器中設(shè)置的值蔬芥。
接下來我們再次刷新這個頁面可以看到在請求體中已經(jīng)設(shè)置了Cookie
字段,并且將我們的值也帶過去了控汉。這樣服務(wù)器就能夠根據(jù)Cookie
中的值記住我們的信息了笔诵。
接下來我們換一個請求呢?是不是Cookie
也會帶過去呢姑子?接下來我們輸入路徑http://localhost:8005
請求乎婿。我們可以看到Cookie
字段還是被帶過去了。
那么瀏覽器的Cookie
是存放在哪呢街佑?如果是使用的是Chrome
瀏覽器的話谢翎,那么可以按照下面步驟。
- 在計算機打開
Chrome
- 在右上角沐旨,一次點擊
更多
圖標(biāo)->設(shè)置
- 在底部森逮,點擊
高級
- 在
隱私設(shè)置和安全性
下方,點擊網(wǎng)站設(shè)置 - 依次點擊
Cookie
->查看所有Cookie和網(wǎng)站數(shù)據(jù)
然后可以根據(jù)域名進行搜索所管理的Cookie
數(shù)據(jù)磁携。所以是瀏覽器替你管理了Cookie
的數(shù)據(jù)褒侧,如果此時你換成了Firefox
等其他的瀏覽器,因為Cookie
剛才是存儲在Chrome
里面的谊迄,所以服務(wù)器又蒙圈了闷供,不知道你是誰,就會給Firefox
再次貼上小紙條统诺。
Cookie中的參數(shù)設(shè)置
說到這里歪脏,應(yīng)該知道了Cookie
就是服務(wù)器委托瀏覽器存儲在客戶端里的一些數(shù)據(jù),而這些數(shù)據(jù)通常都會記錄用戶的關(guān)鍵識別信息篙议。所以Cookie
需要用一些其他的手段用來保護唾糯,防止外泄或者竊取怠硼,這些手段就是Cookie
的屬性鬼贱。
參數(shù)名 | 作用 | 后端設(shè)置方法 |
---|---|---|
Max-Age | 設(shè)置cookie的過期時間,單位為秒 | cookie.setMaxAge(10) |
Domain | 指定了Cookie所屬的域名 | cookie.setDomain("") |
Path | 指定了Cookie所屬的路徑 | cookie.setPath(""); |
HttpOnly | 告訴瀏覽器此Cookie只能靠瀏覽器Http協(xié)議傳輸,禁止其他方式訪問 | cookie.setHttpOnly(true) |
Secure | 告訴瀏覽器此Cookie只能在Https安全協(xié)議中傳輸,如果是Http則禁止傳輸 | cookie.setSecure(true) |
下面我就簡單演示一下這幾個參數(shù)的用法及現(xiàn)象香璃。
Path
設(shè)置為cookie.setPath("/testCookies")
这难,接下來我們訪問http://localhost:8005/testCookies
,我們可以看到在左邊和我們指定的路徑是一樣的葡秒,所以Cookie
才在請求頭中出現(xiàn)姻乓,接下來我們訪問http://localhost:8005
嵌溢,我們發(fā)現(xiàn)沒有Cookie
字段了,這就是Path
控制的路徑蹋岩。
Domain
設(shè)置為cookie.setDomain("localhost")
赖草,接下來我們訪問http://localhost:8005/testCookies
我們發(fā)現(xiàn)下圖中左邊的是有Cookie
的字段的,但是我們訪問http://172.16.42.81:8005/testCookies
剪个,看下圖的右邊可以看到?jīng)]有Cookie
的字段了秧骑。這就是Domain
控制的域名發(fā)送Cookie
。
接下來的幾個參數(shù)就不一一演示了扣囊,相信到這里大家應(yīng)該對Cookie
有一些了解了乎折。
Session
Cookie是存儲在客戶端方,Session是存儲在服務(wù)端方侵歇,客戶端只存儲
SessionId
在上面我們了解了什么是Cookie
骂澄,既然瀏覽器已經(jīng)通過Cookie
實現(xiàn)了有狀態(tài)這一需求,那么為什么又來了一個Session
呢惕虑?這里我們想象一下坟冲,如果將賬戶的一些信息都存入Cookie
中的話,一旦信息被攔截溃蔫,那么我們所有的賬戶信息都會丟失掉樱衷。所以就出現(xiàn)了Session
,在一次會話中將重要信息保存在Session
中酒唉,瀏覽器只記錄SessionId
一個SessionId
對應(yī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);
}
這里我們寫一個新的方法來測試Session
是如何產(chǎn)生的,我們在請求參數(shù)中加上HttpSession session
痪伦,然后再瀏覽器中輸入http://localhost:8005/testSession
進行訪問可以看到在服務(wù)器的返回頭中在Cookie
中生成了一個SessionId
侄榴。然后瀏覽器記住此SessionId
下次訪問時可以帶著此Id,然后就能根據(jù)此Id找到存儲在服務(wù)端的信息了网沾。
此時我們訪問路徑http://localhost:8005/testGetSession
癞蚕,發(fā)現(xiàn)得到了我們上面存儲在Session
中的信息。那么Session
什么時候過期呢辉哥?
- 客戶端:和
Cookie
過期一致桦山,如果沒設(shè)置,默認(rèn)是關(guān)了瀏覽器就沒了醋旦,即再打開瀏覽器的時候初次請求頭中是沒有SessionId
了恒水。 - 服務(wù)端:服務(wù)端的過期是真的過期,即服務(wù)器端的
Session
存儲的數(shù)據(jù)結(jié)構(gòu)多久不可用了饲齐,默認(rèn)是30分鐘钉凌。
既然我們知道了Session
是在服務(wù)端進行管理的,那么或許你們看到這有幾個疑問捂人,Session
是在在哪創(chuàng)建的御雕?Session
是存儲在什么數(shù)據(jù)結(jié)構(gòu)中矢沿?接下來帶領(lǐng)大家一起看一下Session
是如何被管理的。
Session
的管理是在容器中被管理的酸纲,什么是容器呢捣鲸?Tomcat
、Jetty
等都是容器闽坡。接下來我們拿最常用的Tomcat
為例來看下Tomcat
是如何管理Session
的淤毛。在ManageBase
的createSession
是用來創(chuàng)建Session
的颊乘。
@Override
public Session createSession(String sessionId) {
//首先判斷Session數(shù)量是不是到了最大值烟零,最大Session數(shù)可以通過參數(shù)設(shè)置
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString("managerBase.createSession.ise"),
maxActiveSessions);
}
// 重用或者創(chuàng)建一個新的Session對象仪际,請注意在Tomcat中就是StandardSession
// 它是HttpSession的具體實現(xiàn)類,而HttpSession是Servlet規(guī)范中定義的接口
Session session = createEmptySession();
// 初始化新Session的值
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
// 設(shè)置Session過期時間是30分鐘
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id);// 這里會將Session添加到ConcurrentHashMap中
sessionCounter++;
//將創(chuàng)建時間添加到LinkedList中宪迟,并且把最先添加的時間移除
//主要還是方便清理過期Session
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return session
}
到此我們明白了Session
是如何創(chuàng)建出來的酣衷,創(chuàng)建出來后Session
會被保存到一個ConcurrentHashMap
中〈卧螅可以看StandardSession
類穿仪。
protected Map<String, Session> sessions = new ConcurrentHashMap<>();
到這里大家應(yīng)該對Session
有簡單的了解了。
Session是存儲在Tomcat的容器中意荤,所以如果后端機器是多臺的話啊片,因此多個機器間是無法共享Session的,此時可以使用Spring提供的分布式Session的解決方案玖像,是將Session放在了Redis中紫谷。
Token
Session
是將要驗證的信息存儲在服務(wù)端,并以SessionId
和數(shù)據(jù)進行對應(yīng)捐寥,SessionId
由客戶端存儲笤昨,在請求時將SessionId
也帶過去,因此實現(xiàn)了狀態(tài)的對應(yīng)握恳。而Token
是在服務(wù)端將用戶信息經(jīng)過Base64Url編碼過后傳給在客戶端瞒窒,每次用戶請求的時候都會帶上這一段信息,因此服務(wù)端拿到此信息進行解密后就知道此用戶是誰了乡洼,這個方法叫做JWT(Json Web Token)崇裁。
Token
相比較于Session
的優(yōu)點在于,當(dāng)后端系統(tǒng)有多臺時束昵,由于是客戶端訪問時直接帶著數(shù)據(jù)拔稳,因此無需做共享數(shù)據(jù)的操作。
Token的優(yōu)點
- 簡潔:可以通過
URL
,POST
參數(shù)或者是在HTTP
頭參數(shù)發(fā)送妻怎,因為數(shù)據(jù)量小壳炎,傳輸速度也很快 - 自包含:由于串包含了用戶所需要的信息,避免了多次查詢數(shù)據(jù)庫
- 因為Token是以Json的形式保存在客戶端的逼侦,所以JWT是跨語言的
- 不需要在服務(wù)端保存會話信息匿辩,特別適用于分布式微服務(wù)
JWT的結(jié)構(gòu)
實際的JWT大概長下面的這樣,它是一個很長的字符串榛丢,中間用.
分割成三部分
JWT是有三部分組成的
Header
是一個Json對象铲球,描述JWT的元數(shù)據(jù),通常是下面這樣子的
{
"alg": "HS256",
"typ": "JWT"
}
上面代碼中晰赞,alg屬性表示簽名的算法(algorithm)稼病,默認(rèn)是 HMAC SHA256(寫成 HS256);typ屬性表示這個令牌(token)的類型(type)掖鱼,JWT 令牌統(tǒng)一寫為JWT然走。
最后,將上面的 JSON 對象使用 Base64URL 算法轉(zhuǎn)成字符串戏挡。
JWT 作為一個令牌(token)芍瑞,有些場合可能會放到 URL(比如 api.example.com/?token=xxx)。Base64 有三個字符+褐墅、/和=拆檬,在 URL 里面有特殊含義,所以要被替換掉:=被省略妥凳、+替換成-竟贯,/替換成_ 。這就是 Base64URL 算法逝钥。
Payload
Payload部分也是一個Json對象屑那,用來存放實際需要傳輸?shù)臄?shù)據(jù),JWT官方規(guī)定了下面幾個官方的字段供選用艘款。
- iss (issuer):簽發(fā)人
- exp (expiration time):過期時間
- sub (subject):主題
- aud (audience):受眾
- nbf (Not Before):生效時間
- iat (Issued At):簽發(fā)時間
- jti (JWT ID):編號
當(dāng)然除了官方提供的這幾個字段我們也能夠自己定義私有字段齐莲,下面就是一個例子
{
"name": "xiaoMing",
"age": 14
}
默認(rèn)情況下JWT是不加密的,任何人只要在網(wǎng)上進行Base64解碼就可以讀到信息磷箕,所以一般不要將秘密信息放在這個部分选酗。這個Json對象也要用Base64URL
算法轉(zhuǎn)成字符串
Signature
Signature部分是對前面的兩部分的數(shù)據(jù)進行簽名,防止數(shù)據(jù)篡改岳枷。
首先需要定義一個秘鑰芒填,這個秘鑰只有服務(wù)器才知道,不能泄露給用戶空繁,然后使用Header中指定的簽名算法(默認(rèn)情況是HMAC SHA256)殿衰,算出簽名以后將Header、Payload盛泡、Signature三部分拼成一個字符串闷祥,每個部分用.
分割開來,就可以返給用戶了。
HS256可以使用單個密鑰為給定的數(shù)據(jù)樣本創(chuàng)建簽名凯砍。當(dāng)消息與簽名一起傳輸時箱硕,接收方可以使用相同的密鑰來驗證簽名是否與消息匹配。
Java中如何使用Token
上面我們介紹了關(guān)于JWT的一些概念悟衩,接下來如何使用呢剧罩?首先在項目中引入Jar包
compile('io.jsonwebtoken:jjwt:0.9.0')
然后編碼如下
// 簽名算法 ,將對token進行簽名
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 通過秘鑰簽名JWT
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary("SECRET");
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
Map<String,Object> 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
此時在網(wǎng)上隨便找個Base64解碼的網(wǎng)站就能將信息解碼出來
總結(jié)
相信大家看到這應(yīng)該對Cookie
座泳、Session
惠昔、Token
有一定的了解了,接下來再回顧一下重要的知識點
- Cookie是存儲在客戶端的
- Session是存儲在服務(wù)端的挑势,可以理解為一個狀態(tài)列表镇防。擁有一個唯一會話標(biāo)識
SessionId
〕北ィ可以根據(jù)SessionId
在服務(wù)端查詢到存儲的信息来氧。 - Session會引發(fā)一個問題,即后端多臺機器時Session共享的問題饼齿,解決方案可以使用Spring提供的框架饲漾。
- Token類似一個令牌,無狀態(tài)的缕溉,服務(wù)端所需的信息被Base64編碼后放到Token中考传,服務(wù)器可以直接解碼出其中的數(shù)據(jù)。
GitHub代碼地址
猜你想讀
- 學(xué)會這幾道鏈表算法題证鸥,面試再也不怕手寫鏈表了
- Spring事務(wù)傳播屬性有那么難嗎僚楞?看這一篇就夠了
- 后端框架開發(fā)需要注意的幾點
- 壓縮20M文件從30秒到1秒的優(yōu)化過程
- 為什么重寫了equals()也要重寫hashCode()