概述
會話從字面理解就是"兩方交流",那問題就來了,HTTP(超文本傳輸協議)里面的"傳輸"不就包含了"兩方交流"的意思嗎?為什么要多此一舉提出會話技術呢?
談到這個,我們就需要先了解 HTTP 協議。HTTP 協議是一中無狀態的協議,它的每次請求都是相互獨立的,那就產生了一個問題,“交流"的另一方,也就是服務器,面對這么多請求,如何判斷每一個請求都是再和誰"交流”?所以這就衍生出了會話技術。
說到這里,那大家都明白了。會話技術就是用來解決"交流"過程中,身份識別的問題。解決了這個問題,才能夠跟蹤和管理用戶狀態信息、提供個性化服務等等。
會話技術發展到現在,主流的是三種方案
Cookie
Session
Token
下面我們依次講解這三個會話跟蹤方案
1.Cookie
首先我們要解決什么 Cookie 這個問題?
Cookie 是一種客戶端會話跟蹤方案,本質就是瀏覽器端的一種“鍵值對”存儲機制,相當于客戶端和服務器之間傳輸的一串字符串。Cookie 存儲在客戶端瀏覽器中。當我們使用 Cookie 來跟蹤會話時,服務器會在 HTTP 響應頭部的set-cookie 字段中設置 Cookie。當客戶端發送后續請求時,會將該 Cookie 信息包含在 HTTP 請求頭中發送給服務器。
那解決信息不共享的問題,我們是不是就可以通過 Cookie 進行解決了。最簡單的實現方式,是不是只需要將身份信息當作 Cookie 就可以了。說到這里相信你你已經很清楚 Cookie 的作用了。
那為了方便在請求中攜帶 Cookie,我們還需要遵循 HTP 協議。Cookie 也是 HTP 協議當中所支持的技術,各大瀏覽器廠商都支持該標準。所以瀏覽器接受到響應回來的 Cookie 時,會自動的將 Cookie 存儲在瀏覽器中,后續的每一次請求中,瀏覽器會自動的將本地存儲的 Cookie 攜帶到服務端中。服務端接收到請求中,可以通過解釋 Cookie,判斷瀛湖的信息了,這樣就解決了在不同的請求中進行身份信息共享的問題。
接下來我們繼續學習如何具體的使用 Cookie 進行身份認證交流
Cookie 認證流程通常包括以下步驟:
用戶訪問需要身份驗證的網站。如果用戶未經過身份驗證,則服務器將重定向用戶到登錄頁面
這部分內容需要另一個技術:統一攔截技術,不是我們本次討論的重點
用戶輸入用戶名和密碼,發送 Http 請求傳遞給服務器進行驗證。
服務器驗證用戶的憑據,并創建一個會話來保存用戶的身份驗證狀態。在這個過程中,服務器生成一個 Cookie,包含了用戶的唯一標識信息
服務器將 Cookie 設置在響應頭中的 set-cookie 字段中,然后發送響應
當用戶發送后續請求時,瀏覽器會將 Cookie 設置請求頭中的 cookie 字段中
服務器解析請求頭中的 Cookie,并解析 Cookie 進行身份的校驗工作
代碼實現
@Slf4j
@RestController
public class SessionController {
// 設置 Cookie
@GetMapping("/c1")
public Result cookie1(HttpServletResponse response){
// 設置 Cookie
response.addCookie(new Cookie(“username”,“AirMan”));
return Result.success();
}
// 獲取 Cookie
@GetMapping("/c2")
public Result cookie2(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
// 校驗 Cookie
for (Cookie cookie : cookies) {
if(cookie.getName().equals(“username”)){
// 輸出 name 為 username 的 cookie
System.out.println("username: " + cookie.getValue());
}
}
return Result.success();
}
}
為什么有了 Cookie 還有 Session,Token 技術的出現呢?那是不是就說明了 Cookie 有一些缺點?
Cookie 最主要的缺點就是移動端 APP(Android、IOS) 中無法使用 Cookie;其次就是 Cookie 不能跨域;還有就是不安全,客戶端可以隨意修改 Cookie 內容,服務端很難保證數據真實性。
什么是跨域?
只要協議不同(http,https),IP 不同,端口不同,滿足其中之一都叫做跨域。
因為現在的項目都是前后端分離的,前端部署在服務器 192.168.150.200 上,端口 80,后端部署在 192.168.150.100上,端口 8080,所以因為跨域的原因,服務器設置的 Cookie 將無法使用。
2.Session
Session 解決了 Cookie 的什么問題?
Session 主要解決 Cookie 不安全的問題。如果將用戶的手機號,密碼等敏感信息存放在 Cookie 中,那么風險對于雙方都是很大的。對于瀏覽器端,容易發生泄露問題。對于服務器端,因為客戶端可以隨意修改 Cookie 內容,服務端很難保證數據真實性。
那下面我們來講它是如何解決的
Session 的底層就是基于我們剛才所介紹的 Cookie 來實現的。
它是服務器端會話跟蹤技術,所以它是存儲在服務器端的。當用戶第一次訪問 Web 應用程序時,服務器會創建一個 Session,并給它分配一個唯一的標識符(session ID),然后將該標識符發送給客戶端。客戶端收到 session ID 后,通常會將其存儲在 cookie 中,以便后續請求時將其發送回服務器。服務器通過 session ID 可以找到對應的 Session,并從中讀取或修改用戶信息和狀態。session 存儲在服務器端,sessionId 會被存儲到客戶端的 cookie 中。
看到這里相比你就明白了,Session 技術無非就是請求頭中的 cookie 字段保存的是 sessionID,響應頭的 set-cookie 字段保存的也是 sessionID,而具體的用戶信息則保存在服務端。sessionID 只起到一個標識的作用。
所以 Cookie 更像是一個“身份證”,每次訪問網站時出示,表明“我是誰,我的身份證號是多少”。
而Session 更像是“指紋認證”,每次訪問網站時出示,不需要表明“我是誰…”。指紋所對于的信息全保存在服務端
Session 的流程和 Cookie 類似,就不再贅述,代碼實現如下,下面的代碼和 Cookie 很相似,但千萬不要混稀了。Session 是在服務端的,loginUser 的內容不會存放到 Cookie 中。Cookie 只存放一個 SessionID,瀏覽器端是沒有 loginUser 這個信息的。
@Slf4j
@RestController
public class SessionController {
@GetMapping("/s1")
public Result session1(HttpSession session){
log.info(“HttpSession-s1: {}”, session.hashCode());
// 往 session 中存儲數據,不是往 Cookie 中存放內容!
session.setAttribute(“loginUser”, “AirMan”);
return Result.success();
}
@GetMapping("/s2")
public Result session2(HttpServletRequest request){
HttpSession session = request.getSession();
log.info(“HttpSession-s2: {}”, session.hashCode());
// 從 session 中獲取數據,也就是從服務器中取內容
Object loginUser = session.getAttribute(“loginUser”);
log.info(“loginUser: {}”, loginUser);
return Result.success(loginUser);
}
}
為什么有了 Session 還有 Token 技術的出現呢?那是不是就說明了 Session 有一些缺點?
Session 的底層就是基于 Cookie 來實現的。Session 除了解決 Cookie 不安全的問題,其他問題都繼承過來了:
服務器集群環境下無法直接使用 Session
現在的項目基本都是集群部署,Session 保存在一個服務器中,如果這個服務器宕機了,其他服務器是沒有 SessionID 等信息的。早期解決方案是通過服務器實時同步解決,但這樣每臺服務器都要存放,開銷就很大。更好的解決方案是通過** Redis 解決共享 Session 的問題**,這里不過多討論
移動端 APP(Android、IOS) 中無法使用 Cookie
Cookie 不能跨域
那下面我們就要解決
3.Token
Token 解決了 Session 和 Cookie 的什么問題?
它解決了移動端無法使用的問題,也解決了集群環境無法使用 Cookie 的問題
那下面我們來講 Token 的實現思路
Token 是服務端生成的一串字符串,以作客戶端進行請求的一個令牌,當第一次登錄后,服務器生成一個 Token 便將此 Token 返回給客戶端,客戶端接收到令牌之后,就需要將這個令牌存儲起來。這個存儲可以存儲在 cookie 當中,也可以存儲在其他的存儲空間(比如:Authorizatrion 字段)當中。在后續的每一次請求當中,都需要將令牌攜帶到服務端。攜帶到服務端之后,接下來我們就需要來校驗令牌的有效性。如果令牌是有效的,就說明用戶已經執行了登錄操作,如果令牌是無效的,就說明用戶之前并未執行登錄操作
所以 Token 就成為了"交流"的暗號,暗號正確了,我們才能繼續交流。那么暗號的生成一定要安全。
所以 Token 的優點顯而易見
支持PC端、移動端
解決集群環境下的認證問題
減輕服務器的存儲壓力(無需在服務器端存儲)
唯一的缺點就是需要自己實現 Token 的生成(包括令牌的生成、令牌的傳遞、令牌的校驗)
Token 的類型有很多,企業開發中常使用的是 JSON Web Token(JWT),JWT 是一種開放標準(RFC 7519),用于在各方之間安全地傳輸信息。JWT 通常被用作訪問令牌,它包含了被加密的用戶信息和其他元數據,可以被用于身份驗證和授權,其本質就是一個字符串。
所以 JWT 令牌最典型的應用場景就是登錄認證
JWT 的組成: (JWT 令牌由三個部分組成,三個部分之間使用英文的點來分割)
第一部分:Header(頭), 記錄令牌類型、簽名算法等
例如:{“alg”:“HS256”,“type”:“JWT”}
第二部分:Payload(有效載荷),攜帶一些自定義信息、默認信息等
例如:{“id”:“1”,“username”:“Tom”}
第三部分:Signature(簽名),防止 Token 被篡改、確保安全性
將 header、payload,并加入指定秘鑰,通過指定簽名算法計算而來。簽名的目的就是為了防 JWT 令牌被篡改,而正是因為 JWT 令牌最后一個部分數字簽名的存在,所以整個 JWT 令牌是非常安全可靠的。一旦 JWT 令牌當中任何一個部分、任何一個字符被篡改了,整個令牌在校驗的時候都會失敗,所以它是非常安全可靠的
JWT 是如何將原始的 JSON 格式數據,轉變為字符串的呢?
在生成 JWT 令牌時,會對 JSON 格式的數據進行一次編碼:進行 base64 編碼(Base64是編碼方式,而不是加密方式)
利用 JWT 進行會話維持之后,整個流程如下
在瀏覽器發起請求來執行登錄操作,此時會訪問登錄的接口,如果登錄成功之后,我們需要生成一個jwt令牌,將生成的 jwt令牌返回給前端。
前端拿到jwt令牌之后,會將jwt令牌存儲起來。在后續的每一次請求中都會將jwt令牌攜帶到服務端。
服務端統一攔截請求之后,先來判斷一下這次請求有沒有把令牌帶過來,如果沒有帶過來,直接拒絕訪問,如果帶過來了,還要校驗一下令牌是否是有效。如果有效,就直接放行進行請求的處理。
具體的代碼實現:
首先需要引入 JWT 的依賴
io.jsonwebtokenjjwt0.9.1@Test public void genJwt(){Map}
public static Claims parseJWT(String secretKey, String token) {
// 得到 DefaultJwtParser
Claims claims = Jwts.parser()
// 設置簽名的秘鑰
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 設置需要解析的 jwt
.parseClaimsJws(token)
.getBody();
return claims;
}
通過上述代碼即可生成一個 JWT 令牌并進行解析,需要注意的是密鑰一定盡可能的復雜
另外為了讓瀏覽器請求的時候自動攜帶 Token ,所以當登錄請求完成后,前端就會將 JWT 令牌存儲在瀏覽器本地。
服務器響應的JWT令牌存儲在本地瀏覽器哪里了呢?
在當前案例中,JWT令牌存儲在瀏覽器的本地存儲空間Local Storage中了。 Local Storage是瀏覽器的本地存儲。JWT 令牌存儲在瀏覽器的本地存儲空間Local Storage中了。Local Storage是瀏覽器的本地存儲,在移動端也是支持的
之后再發起一個查詢數據的請求,此時可以看到在請求頭中包含一個 token(JWT令牌),后續的每一次請求當中,都會將這個令牌攜帶到服務端。一般情況下,默認都是把 token 放在 Authorization 的鍵值對中。當然也可以自定義請求頭中的鍵值對,如下
最終提供給大家一個 JWT 的工具類
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘鑰
*
* @param secretKey jwt秘鑰
* @param ttlMillis jwt過期時間(毫秒)
* @param claims 設置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定簽名的時候使用的簽名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的時間long expMillis = System.currentTimeMillis() + ttlMillis;Date exp = new Date(expMillis);// 設置jwt的bodyString jwtStr = Jwts.builder()// 如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值之后,就是覆蓋了那些標準的聲明的.setClaims(claims)// 設置簽名使用的簽名算法和簽名使用的秘鑰.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))// 設置過期時間.setExpiration(exp).compact();return jwtStr;
}/*** Token解密** @param secretKey jwt秘鑰 此秘鑰一定要保留好在服務端, 不能暴露出去, 否則sign就可以被偽造, 如果對接多個客戶端建議改造成多個* @param token 加密后的token* @return*/
public static Claims parseJWT(String secretKey, String token) {// 得到DefaultJwtParserClaims claims = Jwts.parser()// 設置簽名的秘鑰.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))// 設置需要解析的jwt.parseClaimsJws(token).getBody();return claims;
}
}