1、HTTP無狀態及解決方案
HTTP一種是無狀態的協議,每次請求都是一次獨立的請求,一次交互之后就是陌生人。
以CSDN為例,先登錄一次,然后瀏覽器退出,這個時候在進入CSDN,按理說服務器是不知道你已經登陸了,所以需要你重新登錄,但實際卻是再次點進來之后,仍然保持登錄狀態。
這是因為解決了HTTP無狀態這個問題,解決方式有很多:
①使用Session和Cookie,將 無狀態 變為 有狀態
第一次請求時,服務器會自動記錄一個SessionId,然后把SessionId返回給瀏覽器。再次發起請求時,瀏覽器就會攜帶SessionId,而服務器就可以根據SessionId,去查詢會話信息(session)。
服務器可以往會話里保存用戶信息
?瀏覽器會自動保存SessionId
Session弊端:Session和Cookie適用于單機環境,默認情況下會話信息(session)是保存在服務器內存中,用戶量大的話,服務器內存壓力大。如果生產是集群環境,就沒辦法保證瀏覽器的請求每一次都傳到有會話信息的那一臺服務器。
這種也好解決,把會話信息存到Redis里,每次訪問,服務器根據SessionId,去Redis拿會話信息(SpringBoot實現了這個功能,引入【spring-session-data-redis】依賴包,做一些設置,就會自動將會話信息交給Redis管理),這樣做服務器的內存壓力也就會見。
②使用Token
在登錄時,生成一個Token放入Redis,之后把token返回給瀏覽器。瀏覽器發起其他請求時,攜帶這個token,服務器再去Redis看這個Token是否失效之類的。
③使用JWT
使用JWT和使用Token很像,但JWT不需要存儲到Redis,就能通過驗簽,知道用戶信息是否偽造、用戶登錄狀態是否過期等等。
2、JWT的基本介紹
JWT官網地址:https://jwt.io/
JWT,全稱JSON? WEB? TOKEN,是一種JSON格式的Web應用令牌,它是基于令牌去做認證。
什么是令牌,舉個例子,古代調兵用的虎符,這就相當于是令牌,有了虎符,才能調兵。而令牌也是,有了令牌,才能訪問后端接口。
以下是官網介紹
大致就是JWT能夠通過HMAC算法(默認算法),或者通過RSA、ECDSA算法生成數字簽名(Signature)。
JWT令牌的組成
JWT由三部分組成:Header(頭)、Payload(負載)、Signature(簽名),三部分用 "." 進行分隔
Header
兩部分組成:令牌的類型是什么,簽名(Signature)使用什么算法。
從官網復制的例子,表示令牌的類型是JWT,算法是 HMAC-SHA256
{"alg": "HS256","typ": "JWT" }
Header的JSON串經過Base64編碼就轉換成 "x.y.z" 的 "x"部分
Payload
負載有七個默認字段
iss:發行人
exp:到期時間
sub:主體
aud:用戶
nbf:在此之前不可用
iat:發布時間
jti:JWT ID用于標識該JWT
官方樣例如下:name 和 admin為自定義的字段
{"sub": "1234567890","name": "John Doe","admin": true
}
負載里面可以放自定義字段,這部分內容也是通過Base64編碼變成"x.y.z"部分的"y"部分,可以通過Base64解碼,拿到里面的信息。所以,官方也推薦不要往負載里存放敏感信息,比如密碼之類的,除非想故意被攻擊。
Signature
官方提供的例子如下,可以看見,簽名是通過前兩部分(Header 和 Payload)base64編碼之后的字符串拼接成一個新的字符串,以及指定一個密鑰(secret),并使用Header里指定的算法生成的簽名。因此,密鑰是一定不能暴露出去的。
HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
假如攻擊者對前兩部分進行隨意修改,并冒充前端發起請求。后端每次驗簽的時候,都會根據前兩部分內容,加上密鑰(secret),再去生成一次簽名,而由于攻擊者修改了內容,所以生成的簽名肯定和傳來的簽名不匹配,這就說明被篡改了,從而驗簽失敗。
3、JWT的優缺點
優點
1、JWT生成的令牌保存在客戶端(前端),服務端不會保存令牌,減少了服務器內存的損耗。
2、易擴展,負載中可以保存自定義的信息。
3、使用強密鑰生成簽名時,JWT提供了很好的安全性。
4、使用JWT,HTTP仍是無狀態的,和Session、Cookie的方式正好相反,JWT不需要在服務器存儲會話信息,非常適合集群環境。
缺點
1、JWT 本身不支持會話管理,不能主動使令牌失效。假如用戶修改了密碼,這個時候肯定要重新登錄,原先的JWT令牌按理就應該失效,但是,由于沒有到令牌指定的過期時間,所以原先的令牌仍然是有效的。(可以將令牌存到redis,當修改密碼后刪除令牌,當令牌沒有就強制用戶去登錄)
2、負載(Payload)中存放過多用戶數據時,會影響性能。
4、如何使用JWT
大致流程:用戶通過前端頁面進行登錄,業務校驗通過之后,生成一個令牌(JWT),后端將JWT返回給前端,前端進行保存。之后,前端每次訪問后端,都必須攜帶JWT,后端去驗證令牌(JWT)的合法性。
①導入JWT依賴
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.4.0</version>
</dependency>
②編寫工具類,生成令牌
前面提到了,JWT令牌由三部分組成,所以創建一個JWT令牌,只要保證有這三部分就可以了
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;import java.util.Base64;
import java.util.Calendar;
import java.util.HashMap;public class JWTUtil {//密鑰,一般是從配置文件讀取private static final String secretKey = "4008123123@#";private static final String algorithm = Algorithm.HMAC256(secretKey).getName();/*** 生成JWT令牌** @return*/public static String generateJwtToken(String userId, String secretKey) {Calendar exp = Calendar.getInstance();//過期時間:當前時間往后推20分鐘exp.add(Calendar.MINUTE, 20);System.out.println("過期時間:" + exp.getTime());// HashMap<String, Object> map = new HashMap<String, Object>();
// map.put("alg",algorithm);
// map.put("typ","JWT");String token = JWT.create()//header,即使不寫,也會使用默認的推薦算法HS256和JWT令牌類型
// .withHeader(map).withExpiresAt(exp.getTime()) //默認的負載字段,設置過期時間.withClaim("userId", userId) //負載---自定義字段.withClaim("username", "UMR123") //負載---自定義字段.sign(Algorithm.HMAC256(secretKey));System.out.println(token);return token;}/*** 驗簽(驗證JWT令牌的合法性)** @param jwtToken* @param secretKey* @return 用來獲取令牌信息*/public static DecodedJWT verifyToken(String jwtToken, String secretKey) {//生成驗證對象JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secretKey)).build();//如果驗證沒問題,就可以獲取到負載信息,如果簽名有問題(簽名不一致,令牌過期),就會報錯DecodedJWT verify = jwtVerifier.verify(jwtToken);System.out.println("負載(Payload)經過base64解碼:" + new String(Base64.getDecoder().decode(verify.getPayload())));System.out.println("userId信息:" + verify.getClaim("userId").asString());System.out.println("username信息:" + verify.getClaim("username").asString());System.out.println("令牌過期時間:" + verify.getExpiresAt());return verify;}public static void main(String[] args) {//生成令牌String jwtToken = generateJwtToken("user123", secretKey);System.out.println();verifyToken(jwtToken, secretKey);}
}
控制臺打印結果如下:?
服務端生成JWT令牌之后,客戶端在請求其他接口時,請求頭新增Authorization字段,放入JWT信息
Authorization: Bearer <token>
都是對數據完整性和用戶身份進行校驗,什么時候直接使用算法生成簽名,什么時候使用JWT?
當需求沒有要求過期時間,就可以使用直接使用算法,反之,用JWT令牌是最好的。