一文了解什么是JWT
起源
需要了解一門技術(shù),首先從為什么產(chǎn)生開始說起是最好的。JWT 主要用于用戶登錄鑒權(quán),所以我們從最傳統(tǒng)的 session 認(rèn)證開始說起。
session認(rèn)證
眾所周知,http 協(xié)議本身是無狀態(tài)的協(xié)議,那就意味著當(dāng)有用戶向系統(tǒng)使用賬戶名稱和密碼進行用戶認(rèn)證之后,下一次請求還要再一次用戶認(rèn)證才行。因為我們不能通過 http 協(xié)議知道是哪個用戶發(fā)出的請求,所以如果要知道是哪個用戶發(fā)出的請求,那就需要在服務(wù)器保存一份用戶信息(保存至 session ),然后在認(rèn)證成功后返回 cookie 值傳遞給瀏覽器,那么用戶在下一次請求時就可以帶上 cookie 值,服務(wù)器就可以識別是哪個用戶發(fā)送的請求,是否已認(rèn)證,是否登錄過期等等。這就是傳統(tǒng)的 session 認(rèn)證方式。
session 認(rèn)證的缺點其實很明顯,由于 session 是保存在服務(wù)器里,所以如果分布式部署應(yīng)用的話,會出現(xiàn)session不能共享的問題,很難擴展。于是乎為了解決 session 共享的問題,又引入了 redis,接著往下看。
token認(rèn)證
這種方式跟 session 的方式流程差不多,不同的地方在于保存的是一個 token 值到 redis,token 一般是一串隨機的字符(比如UUID),value 一般是用戶ID,并且設(shè)置一個過期時間。每次請求服務(wù)的時候帶上 token 在請求頭,后端接收到token 則根據(jù) token 查一下 redis 是否存在,如果存在則表示用戶已認(rèn)證,如果 token 不存在則跳到登錄界面讓用戶重新登錄,登錄成功后返回一個 token 值給客戶端。
優(yōu)點是多臺服務(wù)器都是使用 redis 來存取 token,不存在不共享的問題,所以容易擴展。缺點是每次請求都需要查一下redis,會造成 redis 的壓力,還有增加了請求的耗時,每個已登錄的用戶都要保存一個 token 在 redis,也會消耗 redis 的存儲空間。
有沒有更好的方式呢?接著往下看。
什么是JWT
JWT (全稱:Json Web Token)是一個開放標(biāo)準(zhǔn)(RFC 7519),它定義了一種緊湊的、自包含的方式,用于作為 JSON 對象在各方之間安全地傳輸信息。該信息可以被驗證和信任,因為它是數(shù)字簽名的。
上面說法比較文縐縐,簡單點說就是一種認(rèn)證機制,讓后臺知道該請求是來自于受信的客戶端。
首先我們先看一個流程圖:
流程描述一下:
- 用戶使用賬號、密碼登錄應(yīng)用,登錄的請求發(fā)送到 Authentication Server。
- Authentication Server 進行用戶驗證,然后創(chuàng)建 JWT 字符串返回給客戶端。
- 客戶端請求接口時,在請求頭帶上 JWT。
- Application Server 驗證 JWT 合法性,如果合法則繼續(xù)調(diào)用應(yīng)用接口返回結(jié)果。
可以看出與token方式有一些不同的地方,就是不需要依賴 redis,用戶信息存儲在客戶端。所以關(guān)鍵在于生成 JWT 和解析 JWT 這兩個地方。
JWT的數(shù)據(jù)結(jié)構(gòu)
JWT 一般是這樣一個字符串,分為三個部分,以 “.” 隔開:
xxxxx.yyyyy.zzzzz
Header
JWT 第一部分是頭部分,它是一個描述 JWT 元數(shù)據(jù)的 Json 對象,通常如下所示。
{ "alg": "HS256", "typ": "JWT" }
alg 屬性表示簽名使用的算法,默認(rèn)為 HMAC SHA256(寫為HS256),typ 屬性表示令牌的類型,JWT 令牌統(tǒng)一寫為JWT。
最后,使用 Base64 URL 算法將上述 JSON 對象轉(zhuǎn)換為字符串保存。
Payload
JWT 第二部分是 Payload,也是一個 Json 對象,除了包含需要傳遞的數(shù)據(jù),還有七個默認(rèn)的字段供選擇。
- iss (issuer):簽發(fā)人/發(fā)行人
- sub (subject):主題
- aud (audience):用戶
- exp (expiration time):過期時間
- nbf (Not Before):生效時間,在此之前是無效的
- iat (Issued At):簽發(fā)時間
- jti (JWT ID):用于標(biāo)識該 JWT
如果自定義字段,可以這樣定義:
{ //默認(rèn)字段 "sub":"主題123", //自定義字段 "name":"java技術(shù)愛好者", "isAdmin":"true", "loginTime":"2021-12-05 12:00:03" }
需要注意的是,默認(rèn)情況下 JWT 是未加密的,任何人都可以解讀其內(nèi)容,因此一些敏感信息不要存放于此,以防信息泄露。
JSON 對象也使用 Base64 URL 算法轉(zhuǎn)換為字符串后保存,是可以反向反編碼回原樣的,這也是為什么不要在 JWT 中放敏感數(shù)據(jù)的原因。
Signature
header (base64URL 加密后的) payload (base64URL 加密后的) secret
JWT 第三部分是簽名。是這樣生成的,首先需要指定一個 secret,該 secret 僅僅保存在服務(wù)器中,保證不能讓其他用戶知道。這個部分需要 base64URL 加密后的 header 和 base64URL 加密后的 payload 使用 . 連接組成的字符串,然后通過header 中聲明的加密算法 進行加鹽secret組合加密,然后就得出一個簽名哈希,也就是Signature,且無法反向解密。
那么 Application Server 如何進行驗證呢?可以利用 JWT 前兩段,用同一套哈希算法和同一個 secret 計算一個簽名值,然后把計算出來的簽名值和收到的 JWT 第三段比較,如果相同則認(rèn)證通過。
JWT的優(yōu)點
- json格式的通用性,所以JWT可以跨語言支持,比如Java、JavaScript、PHP、Node等等。
- 可以利用Payload存儲一些非敏感的信息。
- 便于傳輸,JWT結(jié)構(gòu)簡單,字節(jié)占用小。
- 不需要在服務(wù)端保存會話信息,易于應(yīng)用的擴展。
怎么使用JWT
首先引入Maven依賴。
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
創(chuàng)建工具類,用于創(chuàng)建(生成) jwt 字符串和解析 jwt。
@Component public class JwtUtil { @Value("${jwt.secretKey}") private String secretKey; public String createJWT(String id, String subject, long ttlMillis, Map<String, Object> map) throws Exception { JwtBuilder builder = Jwts.builder() .setId(id) .setSubject(subject) // 發(fā)行者 .setIssuedAt(new Date()) // 發(fā)行時間 .signWith(SignatureAlgorithm.HS256, secretKey) // 簽名類型 與 密鑰 .compressWith(CompressionCodecs.DEFLATE);// 對載荷進行壓縮 if (!CollectionUtils.isEmpty(map)) { builder.setClaims(map); } if (ttlMillis > 0) { builder.setExpiration(new Date(System.currentTimeMillis() + ttlMillis)); } return builder.compact(); } public Claims parseJWT(String jwtString) { return Jwts.parser().setSigningKey(secretKey) .parseClaimsJws(jwtString) .getBody(); } }
接著在application.yml配置文件配置jwt.secretKey
。
## 用戶生成jwt字符串的secretKey jwt: secretKey: ak47
接著創(chuàng)建一個響應(yīng)體。
public class BaseResponse { private String code; private String msg; public static BaseResponse success() { return new BaseResponse("0", "成功"); } public static BaseResponse fail() { return new BaseResponse("1", "失敗"); } //構(gòu)造器、getter、setter方法 } public class JwtResponse extends BaseResponse { private String jwtData; public static JwtResponse success(String jwtData) { BaseResponse success = BaseResponse.success(); return new JwtResponse(success.getCode(), success.getMsg(), jwtData); } public static JwtResponse fail(String jwtData) { BaseResponse fail = BaseResponse.fail(); return new JwtResponse(fail.getCode(), fail.getMsg(), jwtData); } //構(gòu)造器、getter、setter方法 }
接著創(chuàng)建一個UserController:
@RestController @RequestMapping("/user") public class UserController { @Resource private UserService userService; @RequestMapping(value = "/login", method = RequestMethod.POST) public JwtResponse login(@RequestParam(name = "userName") String userName, @RequestParam(name = "passWord") String passWord){ String jwt = ""; try { jwt = userService.login(userName, passWord); return JwtResponse.success(jwt); } catch (Exception e) { e.printStackTrace(); return JwtResponse.fail(jwt); } } }
還有UserService:
@Service public class UserServiceImpl implements UserService { @Resource private JwtUtil jwtUtil; @Resource private UserMapper userMapper; @Override public String login(String userName, String passWord) throws Exception { //登錄驗證 User user = userMapper.findByUserNameAndPassword(userName, passWord); if (user == null) { return null; } //如果能查出,則表示賬號密碼正確,生成jwt返回 String uuid = UUID.randomUUID().toString().replace("-", ""); HashMap<String, Object> map = new HashMap<>(); map.put("name", user.getName()); map.put("age", user.getAge()); return jwtUtil.createJWT(uuid, "login subject", 0L, map); } }
還有UserMapper.xml:
@Mapper public interface UserMapper { User findByUserNameAndPassword(@Param("userName") String userName, @Param("passWord") String passWord); } <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="io.github.yehongzhi.jwtdemo.mapper.UserMapper"> <select id="findByUserNameAndPassword" resultType="io.github.yehongzhi.jwtdemo.model.User"> select * from user where user_name = #{userName} and pass_word = #{passWord} </select> </mapper>
user 表結(jié)構(gòu)如下:
啟動項目,然后用 postman 請求 login 接口。
返回的 jwt 字符串如下:
eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNqqVspLzE1VslJ6OnHFsxnzX67coKSjlJgOFDEzqAUAAAD__w.qib2DrjRKcFnY77Cuh_b1zSzXfISOpCA-g8PlAZCWoU
接著我們寫一個接口接收這個 jwt,并做驗證。
@RestController @RequestMapping("/jwt") public class TestController { @Resource private JwtUtil jwtUtil; @RequestMapping("/test") public Map<String, Object> test(@RequestParam("jwt") String jwt) { //這個步驟可以使用自定義注解+AOP編程做解析jwt的邏輯,這里為了簡便就直接寫在controller里 Claims claims = jwtUtil.parseJWT(jwt); String name = claims.get("name", String.class); String age = claims.get("age", String.class); HashMap<String, Object> map = new HashMap<>(); map.put("name", name); map.put("age", age); map.put("code", "0"); map.put("msg", "請求成功"); return map; } }
像這樣能正常解析成功的話,就表示該用戶登錄未過期,并且已認(rèn)證成功,所以可以正常調(diào)用服務(wù)。那么有人會問了,這個 jwt 字符串能不能被偽造呢?
除非你知道 secretKey,否則是不能偽造的。比如客戶端隨便猜一個 secretKey 的值,然后偽造一個jwt:
eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNqqVspLzE1VslJ6OnHFsxnzX67coKSjlJgOFDEzqAUAAAD__w.bHr9p3-t2qR4R50vifRVyaYYImm2viZqiTlDdZHmF5Y
然后傳進去解析,會報以下錯誤:
還記得原理吧,是根據(jù)前面兩部分(Header、Payload)加上 secretKey 使用 Header 指定的哈希算法計算出第三部分(Signature),所以可以看出最關(guān)鍵就是 secretKey。secretKey只有服務(wù)端自己知道,所以客戶端不知道 secretKey 的值是偽造不了jwt字符串的。
總結(jié)
最后講講 JWT 的缺點,因為任何技術(shù)都不是完美的,所以我們得用辯證思維去看待任何一項技術(shù)。
安全性沒法保證,所以 jwt 里不能存儲敏感數(shù)據(jù)。因為 jwt 的 payload 并沒有加密,只是用 Base64 編碼而已。無法中途廢棄。因為一旦簽發(fā)了一個 jwt,在到期之前始終都是有效的,如果用戶信息發(fā)生更新了,只能等舊的 jwt 過期后重新簽發(fā)新的 jwt。續(xù)簽問題。當(dāng)簽發(fā)的 jwt 保存在客戶端,客戶端一直在操作頁面,按道理應(yīng)該一直為客戶端續(xù)長有效時間,否則當(dāng) jwt有效期到了就會導(dǎo)致用戶需要重新登錄。那么怎么為 jwt 續(xù)簽?zāi)兀孔詈唵未直┚褪敲看魏灠l(fā)新的 jwt,但是由于過于暴力,會影響性能。如果要優(yōu)雅一點,又要引入 Redis 解決,但是這又把無狀態(tài)的 jw t硬生生變成了有狀態(tài)的,違背了初衷。
所以印證了那句話,沒有最好的技術(shù),只有適合的技術(shù)。
到此這篇關(guān)于一文了解什么是JWT的文章就介紹到這了,更多相關(guān)JWT內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
json數(shù)據(jù)傳到前臺并解析展示成列表的方法
今天小編就為大家分享一篇json數(shù)據(jù)傳到前臺并解析展示成列表的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08json的定義、標(biāo)準(zhǔn)格式及json字符串檢驗
今天分享和總結(jié)一些json的基本定義、格式、字符串的格式,以及在做測試的時候使用json時做一些簡單的校驗2014-05-05Javascript Jquery 遍歷Json的實現(xiàn)代碼
Javascript Jquery 遍歷Json的實現(xiàn)代碼,需要的朋友可以參考下。2010-03-03