一文理解Token、Session和Cookie
Web發展史
線上購物,部落格,視訊等網站都需要管理會話,需要記錄儲存使用者的狀態和資訊,然而HTTP請求是無狀態的,如果每次請求都是一個新的HTTP協議,那麼使用者第一次發起請求,登入成功後,每次開啟一個頁面都需要重新登入。服務端無法知道客戶之前的狀態,對於互動式的web應用,使用一種技術儲存使用者資訊是有必要的。為了解決這個問題,演進出了Cookie,Session。
其中的一個解決方案是使用會話標識(session id)。然而Session id只在保留在了一個節點上,如果由於負載均衡轉發到另一個節點上,就會產生Session丟失的問題
Session Sticky原理:負載均衡會將具有相同的ip的請求轉發到同一個節點上去,這樣就可以在小規模的場景下解決Session丟失的問題,比如修改Nginx負載均衡路由策略
upstream example{
server ip:port;
server ip:port;
ip_hash;
}
Cookie
cookie是瀏覽器實現的一種資料儲存功能。cookie以k,v形式儲存到某個目錄下的文字檔案內,下一次請求同一網站時會把該cookie傳送給伺服器。由於cookie是存在客戶端上的,所以瀏覽器加入了一些限制確保cookie不會被惡意使用,同時每個域的cookie數量是有限的,一般不超過4KB。使用Cookie實際上只能儲存一小段的文字資訊,另外Cookie中的資料只能以字串的形式儲存一小段的文字資訊。
Session
伺服器使用session把使用者的資訊臨時儲存在了伺服器上,使用者離開網站後session會被銷燬。這種使用者資訊儲存方式相對cookie來說更安全,可是session有一個缺陷:如果web伺服器做了負載均衡,那麼下一個操作請求到了另一臺伺服器的時候session會丟失。對於大型專案而言,不推薦使用Session Sticky,基於分散式Memcache的Session共享方案比較合適。為什麼使用Memcache
另外,Session可以用於共享同一使用者不同請求間的資料,服務端以ConcurrentMap<String, Object>
的形式儲存在StandardHttpSession
中。相比Cookie而言,儲存在服務端的技術更加安全,但是對伺服器產生了額外的負擔。
在專案中,通常是Cookie與Session結合使用,通過Cookie儲存對安全性要求不高的內容,比如使用者最近瀏覽的文章,商品,token。而Session用於記錄使用者登入資訊。
為什麼使用token進行身份驗證
無狀態、可擴充套件、支援移動裝置、跨程式呼叫、安全
token的生成
當用戶第一次登入時,服務端驗證使用者密碼並生成一串字串,這個字串將被作為token儲存到HttpServletResponse
當用戶再次訪問時,服務端通過HttpServletRequest.getCookies()
查詢使用者請求中攜帶的cookie資料,通過和快取中的token比較並驗證有效期,如果驗證成功執行對應的業務。
服務端token儲存形式
瀏覽器可以利用Cookie以鍵值對的方式儲存token,比如token-name:token的形式。比如,可以將Session id作為token寫入到Cookie,並新增到HttpServletResponse
。
public void writeLoginToken(HttpServletResponse response, String token){
Cookie ck = new Cookie(COOKIE_NAME,token);
ck.setDomain(COOKIE_DOMAIN);
ck.setPath("/");//代表設定在根目錄
ck.setHttpOnly(true);
ck.setMaxAge(60 * 60 * 24 * 365);//如果是-1,代表永久
response.addCookie(ck);
}
對於伺服器端,可以利用快取儲存token與字串形式的使用者物件,這時token也就是Session id作為鍵,而將存有許可權使用者名稱等資訊的User物件轉化為字串並存儲在快取中。
FASTjson能夠很好地支援物件與String之間的轉換,Memcached支援快取自動失效。同時當用戶請求通過負載均衡轉發到任一節點,該節點可以通過查詢快取的資料驗證token的有效性,並獲取到對應的使用者的id、許可權等資訊。
public <T> boolean set(KeyPrefix prefix, String key, T value ,int exTime) {
Jedis jedis = jedisPool.getResource();
String str = JSON.toJSONString(value);//FastJson
if (str == null || str.length() <= 0) {
return false;
}
//為了演示,省略了一些邏輯判斷
String realKey = prefix.getPrefix() + key;
if (exTime == 0) {
jedis.set(realKey, str);
} else {
//設定過期時間
jedis.setex(realKey, exTime, str);
}
return true;
}
伺服器端不儲存token的解決方案:
不儲存token方案的關鍵點在於需要驗證token來自於伺服器生成,還是來自惡意偽造。相比之下,快取token的方案不需要考慮這點,在快取中直接查詢即可。
此時,token必須能夠被驗證,下面貼一個知乎上的token生成方案【在該方案的基礎上補上時間戳】:金鑰只有伺服器知道,那麼通過HMAC-SHA256和金鑰對資料和時間戳簽名,如此伺服器在驗證時可以確保該token來自伺服器,並且通過時間戳驗證該token是否失效。
但是該方案的缺點是,一旦金鑰丟失,通過HMAC-SHA256和金鑰可以偽造任何使用者的token,所以這種方案並不安全。