JWT 基本使用
在上一節中 session 共享功能使用 redis 進行存儲,用戶量激增時會導致 redis 崩潰,而 JWT 不依賴服務器,能夠避免這個問題。
1、傳統 session
1.1.認證方式
http協議本身是一種無狀態的協議,如果用戶向服務器提供了用戶名和密碼來進行用戶認證,下次請求時,用戶還要再一次進行用戶認證才行。因為根據http協議,服務器並不能知道是哪個用戶發出的請求,所以為了讓我們的應用能識別是哪個用戶發出的請求,我們只能在服務器存儲─份用戶登錄的信息,這份登錄信息會在響應時傳遞給瀏覽器,告訴其保存為cookie,以便下次請求時發送給我們的應用,這樣應用就能識別請求來自哪個用戶。
1.2.暴露的問題
- 用戶經過應用認證后,應用都要在服務端做一次記錄,以方便用戶下次請求的鑒別,通常而言session都是保存在內存中,而隨着認證用戶的增多,服務端的開銷會明顯增大;
- 用戶認證后,服務端做認證記錄,如果認證的記錄被保存在內存中的話,用戶下次請求還必須要請求在這台服務器上,這樣才能拿到授權的資源。在分布式的應用上,限制了負載均衡器的能力。以此限制了應用的擴展能力;
- session是基於cookie來進行用戶識別,cookie如果被截獲,用戶很容易受到CSRF(跨站偽造請求攻擊)攻擊;
- 在前后端分離系統中應用解耦后增加了部署的復雜性。通常用戶一次請求就要轉發多次。如果用session每次攜帶sessionid到服務
器,服務器還要查詢用戶信息。同時如果用戶很多。這些信息存儲在服務器內存中,給服務器增加負擔。還有就是sessionid就是一個特征值,表達的信息不夠豐富。不容易擴展。而且如果你后端應用是多節點部署。那么就需要實現session共享機制。不方便集群應用。
2、概念
Json web token (JWT) ,是一種基於 JSON的開放標准(RFC 7519),定義了一種簡潔的、自包含的方法用於通信雙方之間以 JSON 對象的形式安全的傳遞信息。 因為數字簽名的存在,這些信息是可信的,JWT 可以使用 HMAC 算法或者 RSA 的公鑰私鑰進行簽名。
3、作用
- 授權:一旦用戶登錄,每個后續請求將包括JWT,從而允許用戶訪問該令牌允許的路由,服務和資源。它的開銷很小並且可以在不同的域中使用。如:單點登錄。
- 信息交換:在各方之間安全地傳輸信息。JWT可進行簽名(如使用公鑰/私鑰對),因此可確保發件人。由於簽名是使用標頭和有效負載計算的,因此還可驗證內容是否被篡改。
4、認證流程
- 前端通過Web表單將自己的用戶名和密碼發送到后端的接口。該過程一般是HTTP的POST請求。建議的方式是通過SSL加密的傳輸(https協議),從而避免敏感信息被嗅探。
- 后端核對用戶名和密碼成功后,將用戶的id等其他信息作為JWT Payload(負載),將其與頭部分別進行Base64編碼拼接后簽名,形成一個JWT(Token)。
- 后端將JWT字符串作為登錄成功的返回結果返回給前端。前端可以將返回的結果保存在localStorage(瀏覽器本地緩存)或sessionStorage(session緩存)上,退出登錄時前端刪除保存的JWT即可。
- 前端在每次請求時將JWT放入HTTP的Header中的Authorization位。(解決XSS和XSRF問題)HEADER
后端檢查是否存在,如存在驗證JWT的有效性。例如,檢查簽名是否正確﹔檢查Token是否過期;檢查Token的接收方是否是自己(可選) - 驗證通過后后端使用JWT中包含的用戶信息進行其他邏輯操作,返回相應結果。
5、優點及結構
5.1、優點
- 簡潔(Compact):可以通過URL,POST參數或者在HTTP header發送,數據量小,傳輸速度也很快;
- 自包含(Self-contained):負載中包含了所有用戶所需要的信息,避免了多次查詢數據庫;
- Token是以JSON加密的形式保存在客戶端,所以JWT是跨語言的,原則上任何web形式都支持。
- 不需要在服務端保存會話信息,特別適用於分布式微服務。
5.2、結構
就是令牌token,是一個String字符串,由3部分組成,中間用點隔開,令牌組成:
- 標頭(Header)
- 有效載荷(Payload)
- 簽名(Signature)
token格式:head.payload.singurater 如:xxxxx.yyyy.zzzz
- Header:有令牌的類型和所使用的簽名算法,如HMAC、SHA256、RSA;使用Base64編碼組成;(Base64是一種編碼,不是一種加密過程,可以被翻譯成原來的樣子)
{
"alg" : "HS256",
"type" : "JWT"
}
- Payload :有效負載,包含聲明;聲明是有關實體(通常是用戶)和其他數據的聲明,不放用戶敏感的信息,如密碼。同樣使用Base64編碼
{
"sub" : "123",
"name" : "John Do",
"admin" : true
}
- Signature :前面兩部分都使用Base64進行編碼,前端可以解開知道里面的信息。Signature需要使用編碼后的header和payload
加上我們提供的一個密鑰,使用header中指定的簽名算法(HS256)進行簽名。簽名的作用是保證JWT沒有被篡改過
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
6、使用
6.1、引入依賴
<!--引入JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.0</version>
</dependency>
6.2、創建配置類
public class JWTUtils {
//自定義密鑰
private static final String SECRET_KEY = "@Tes*RI#s&";
/**
* 生成token
*
* @param map
* @return token
*/
public static String createToken(Map<String, String> map) {
JWTCreator.Builder builder = JWT.create();
//保存用戶信息
map.forEach((k,v)-> {
builder.withClaim(k,v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.MINUTE,200);
//設置過期時間
builder.withExpiresAt(instance.getTime());
//設置簽名
return builder.sign(Algorithm.HMAC256(SECRET_KEY));
}
/**
* 驗證token
* @param token
* @return 驗證信息
*/
public static Map verify(String token){
Map<String, Object> map = new HashMap<>();
try {
JWT.require(Algorithm.HMAC256(SECRET_KEY)).build().verify(token);
map.put("status", true);
return map;
}catch (TokenExpiredException e){
e.printStackTrace();
map.put("msg","token過期");
}catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg", "無效簽名");
}catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg","算法不一致");
}catch (Exception e) {
e.printStackTrace();
map.put("msg","token無效");
}
map.put("status", false);
return map;
}
}
常見異常信息如下:
SignatureVerificationException //簽名不一致異常
TokenExpiredException //令牌過期異常
AlgorithmMismatchException //算法不匹配異常
InvalidClaimException //失效的payload異常(傳給客戶端后,token被改動,驗證不一致)
6.3、過濾器獲取token
public class AccessFilter implements Filter {
Logger logger = LoggerFactory.getLogger(AccessFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//登錄接口不用驗證token
if (request.getRequestURI().contains("/login")){
filterChain.doFilter(request, servletResponse);
return;
}
//驗證token
String token = request.getHeader("token");
Map<String, Object> map = JWTUtils.verify(token);
//驗證成功
if ((Boolean)map.get("status")) {
DecodedJWT decodedJWT = JWTUtils.getToken(token);
logger.info("=========" + decodedJWT.getClaim("userName").asString());
//驗證失敗
} else {
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json; charset=UTF-8");
response.getWriter().println(json);
return;
}
filterChain.doFilter(request, servletResponse);
}
@Override
public void destroy() {
}
}
6.4、模擬登錄接口
@GetMapping(value = "/login")
@ResponseBody
public Result login(HttpSession session) {
Map<String, String> map = new HashMap<>();
map.put("userId", "1");
map.put("userName", "123456");
//生成token
String token = JWTUtils.createToken(map);
return ResultUtil.success(token);
}
返回 token 給客戶端,客戶端每次請求攜帶token,過濾器對 token 有效性進行校驗,驗證成功返回數據,驗證失敗返回登錄頁面。
參考資料:JWT詳細教程及使用