1.引子
我們知道,http是一種無狀態協議,即對於服務端應用來說,兩次http請求之間相互獨立,你不知道我,我不知道你。
那麼問題來了,比如說一個電商網站,購物的時候,需要瀏覽商品,將商品添加到購物車,需要下單結算。這就提出了要求:我們需要知道是誰選擇了商品a,是誰將商品a添加到了購物車,購物車又是誰的......等一系列問題,都指向了“誰”,即將多次不同http請求關聯起來。
如何關聯呢?
你應該已經反應過來了,登錄認證啊!用戶訪問購物網站,首先要做的是提供用戶名稱、密碼登錄到購物網站,然後在網站導航的地方,通常會提示:歡迎xxx!對吧,這樣以來,購物網站自然就知道了上面我們提到的一系列“誰”,到底是誰
這是從一個普通用戶的角度來描述的,我們是程序員,需要從技術的角度來進行描述。你說這也不難!不就是
-
當用戶訪問登錄接口的時候,根據用戶提供的用戶名稱、密碼查詢數據庫
-
如果用戶存在,購物網站應用服務器,比如說tomcat,生成一個session,並且存儲到購物網站應用服務器內存中
-
將session的唯一標識sessionId,返回給客戶端(瀏覽器),瀏覽器將sessionId存儲到cookie中
-
瀏覽器再登錄成功後,每次發起http請求,比如選擇商品、加入購物車、下訂單、結算都帶上sessionId,依據sessionId告訴服務器,一系列的誰,就是誰
-
購物網站應用服務端,根據sessionId,自然就知道了是誰在選擇商品,誰在將商品加入購物車,誰在下訂單.......
通過session、cookie將多次不同的http請求關聯了起來,解決了一系列誰是誰的問題,完美!不能再完美了!
這也就是我們熟悉的傳統session會話的解決方案,但是這種方案,其實是有瑕疵的,我們來看一下
-
會話session存儲在服務端,需要消耗應用服務器內存,如果網站生意比較好,動不動就有幾千萬、上億用戶,是不是單存儲會話session就需要消耗不少內存?
-
網站太受歡迎,高併發、大流量,一臺應用服務器扛不住,部署集羣方案吧,比如說增加網站應用服務器a、b、c。會話session都是應用服務器內部生成的,用戶在服務節點a登錄了,下次請求如果打到服務節點b,節點b如何知道用戶已經登錄過?
-
會話sessionId存儲在瀏覽器cookie中的對吧,CSRF(跨站請求僞造)怎麼辦?
-
移動互聯網時代,大家都用智能手機,玩着各種app,沒有瀏覽器不支持cookie怎麼辦?
-
分佈式、微服務時代,不同應用之間相互調用,服務a,不認識服務b的session啊,怎麼辦?
綜合上述總結一下,傳統會話session方案最大的不足在於消耗應用服務器內存、難以支撐應用彈性擴容縮容。
這纔有了業界當前流行的token方案,接下來讓我們一起來看token方案,在這裏關於token方案,我將結合jwt給你分享。
2.案例
2.1.token登錄認證方案
token登錄認證方案,實現思路上事實上與傳統session方案差不多,畢竟token與sessionId一樣,都是字符串嘛,我們從普通用戶操作流程進行分析
-
用戶打開購物網站,提供用戶名稱、密碼進行登錄請求
-
網站服務器,接收到用戶登錄請求,根據用戶名稱、密碼查詢數據庫,檢查用戶是否存在
-
如果用戶存在,根據某種規則,生成一個token(一個字符串)
-
將token響應給瀏覽器,用戶在之後的選擇商品、加入購物車、下訂單等請求中,都帶上token
-
網站服務器,校驗處理token即知道誰是誰了
你看,這就是token方案,對比傳統session方案,它不再需要
-
token不需要在服務器存儲,即不消耗服務器內存資源
-
服務繼續無狀態,支持按需隨意擴容縮容
2.2.什麼是jwt
jwt的全稱是(json web token)。 是爲了在網絡應用環境間傳遞聲明的一種基於json的開放標準,通過jwt實現的token被設計爲緊湊且安全,適用於分佈式站點單點登錄(sso)場景。
jwt實現的token主要由三部分組成,以.符號進行分割
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiI2NjYiLCJpYXQiOjE2MTg2NDYyNjUsImV4cCI6MTYxOTI1MTA2NX0.uVeXiEhqfhpWAnkiX8glIBE4nOG6o2zaQfRBOC-EiuY
-
頭header:標記token令牌類型,加密算法
-
載荷payload:token數據,放置一些用戶非敏感信息,比如說用戶id,用戶名稱(切記:jwt是可以解密,密碼、手機號碼等用戶敏感信息,千萬不要放在其中)
-
簽名sign:用於驗證token是否有效,放置篡改
2.3.jwt實現案例
2.3.1.導入依賴
<!--jjwt依賴-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
2.3.2.jwt工具類
/**
* token工具
*
* @author ThinkPad
* @version 1.0
* @date 2021/4/17 14:31
*/
@Slf4j
@Data
public class TokenJwtUtil {
/**
* 祕鑰,默認:aaabbbcccdddeeefffggghhhiiijjjkkklllmmm
*/
private String secret = "aaabbbcccdddeeefffggghhhiiijjjkkklllmmm";
/**
* 有效時間,默認一週,單位秒
*/
private Long expirationTime = 604800L;
/**
* 生成token
* @param claims token 數據
* @return
*/
public String generateToken(Map<String, Object> claims){
// 生成時間,過期時間
Date createdTime = new Date();
Date expirationTime = new Date(System.currentTimeMillis() + this.expirationTime * 1000);
// 祕鑰
byte[] keyBytes = this.secret.getBytes();
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
// 生成token
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(createdTime)
.setExpiration(expirationTime)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 校驗token是否有效
* @param token
* @return 有效返回true,無效返回false
*/
public Boolean validateToken(String token){
Date expirationTime = getExpirationDateFromToken(token);
return !expirationTime.before(new Date());
}
/**
* 獲取token過期時間
* @param token
* @return
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token).getExpiration();
}
/**
* 解析獲取token數據
* @param token
* @return
*/
public Claims getClaimsFromToken(String token) {
try{
return parser()
.setSigningKey(this.secret.getBytes())
.parseClaimsJws(token)
.getBody();
}catch (ExpiredJwtException
| UnsupportedJwtException
| MalformedJwtException
| IllegalArgumentException e){
log.error("解析token發生異常", e);
throw new IllegalArgumentException("Token invalided.");
}
}
}
2.3.3.測試使用
public static void main(String[] args) {
TokenJwtUtil tokenJwtUtil = new TokenJwtUtil();
// 1.生成token
Map<String, Object> claims = new HashMap<>();
claims.put("userId","666");
String token = tokenJwtUtil.generateToken(claims);
log.info("準備token數據:{}", claims);
log.info("生成token={}",token);
// 2.解析token頭
String[] split = token.split("\\.");
byte[] headBytes = Base64.decodeBase64(split[0]);
log.info("解析token頭:{}", new String(headBytes));
// 3.解析token數據
byte[] bodyBytes = Base64.decodeBase64(split[1]);
log.info("解析token數據:{}", new String(bodyBytes));
// 4.解析token簽名
byte[] signatureBytes = Base64.decodeBase64(split[2]);
log.info("解析token簽名:{}", new String(signatureBytes));
// 5.解析token過期時間
Date expirationDate = tokenJwtUtil.getExpirationDateFromToken(token);
log.info("解析token過期時間:{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(expirationDate));
// 6.校驗token是否有效
Boolean isValidate = tokenJwtUtil.validateToken(token);
log.info("校驗token是否有效:{}",isValidate);
}
[com.anan.edu.common.util.Test] - 準備token數據:{userId=666}
[com.anan.edu.common.util.Test] - 生成token=eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiI2NjYiLCJpYXQiOjE2MTg2NDYyNjUsImV4cCI6MTYxOTI1MTA2NX0.uVeXiEhqfhpWAnkiX8glIBE4nOG6o2zaQfRBOC-EiuY
[com.anan.edu.common.util.Test] - 解析token頭:{"alg":"HS256"}
[com.anan.edu.common.util.Test] - 解析token數據:{"userId":"666","iat":1618646265,"exp":1619251065}
[com.anan.edu.common.util.Test] - 解析token簽名:�W��Hj~Vy"_�% 8�ảl�A�A8/���
[com.anan.edu.common.util.Test] - 解析token過期時間:2021-04-24 15:57:45
[com.anan.edu.common.util.Test] - 校驗token是否有效:true