<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          不懂就學(xué),什么是JWT?

          共 7387字,需瀏覽 15分鐘

           ·

          2021-12-12 23:37

          文章已收錄到我的Github精選,歡迎Star:https://github.com/yehongzhi/learningSummary

          起源

          需要了解一門技術(shù),首先從為什么產(chǎn)生開始說起是最好的。JWT主要用于用戶登錄鑒權(quán),所以我們從最傳統(tǒng)的session認(rèn)證開始說起。

          session認(rèn)證

          眾所周知,http協(xié)議本身是無狀態(tài)的協(xié)議,那就意味著當(dāng)有用戶向系統(tǒng)使用賬戶名稱和密碼進(jìn)行用戶認(rèn)證之后,下一次請求還要再一次用戶認(rèn)證才行。因?yàn)槲覀儾荒芡ㄟ^http協(xié)議知道是哪個用戶發(fā)出的請求,所以如果要知道是哪個用戶發(fā)出的請求,那就需要在服務(wù)器保存一份用戶信息(保存至session),然后在認(rèn)證成功后返回cookie值傳遞給瀏覽器,那么用戶在下一次請求時就可以帶上cookie值,服務(wù)器就可以識別是哪個用戶發(fā)送的請求,是否已認(rèn)證,是否登錄過期等等。這就是傳統(tǒng)的session認(rèn)證方式。

          session認(rèn)證的缺點(diǎn)其實(shí)很明顯,由于session是保存在服務(wù)器里,所以如果分布式部署應(yīng)用的話,會出現(xiàn)session不能共享的問題,很難擴(kuò)展。于是乎為了解決session共享的問題,又引入了redis,接著往下看。

          token認(rèn)證

          這種方式跟session的方式流程差不多,不同的地方在于保存的是一個token值到redis,token一般是一串隨機(jī)的字符(比如UUID),value一般是用戶ID,并且設(shè)置一個過期時間。每次請求服務(wù)的時候帶上token在請求頭,后端接收到token則根據(jù)token查一下redis是否存在,如果存在則表示用戶已認(rèn)證,如果token不存在則跳到登錄界面讓用戶重新登錄,登錄成功后返回一個token值給客戶端。

          優(yōu)點(diǎn)是多臺服務(wù)器都是使用redis來存取token,不存在不共享的問題,所以容易擴(kuò)展。缺點(diǎn)是每次請求都需要查一下redis,會造成redis的壓力,還有增加了請求的耗時,每個已登錄的用戶都要保存一個token在redis,也會消耗redis的存儲空間。

          有沒有更好的方式呢?接著往下看。

          什么是JWT

          JWT(全稱:Json Web Token)是一個開放標(biāo)準(zhǔn)(RFC 7519),它定義了一種緊湊的、自包含的方式,用于作為JSON對象在各方之間安全地傳輸信息。該信息可以被驗(yàn)證和信任,因?yàn)樗菙?shù)字簽名的。

          上面說法比較文縐縐,簡單點(diǎn)說就是一種認(rèn)證機(jī)制,讓后臺知道該請求是來自于受信的客戶端。

          首先我們先看一個流程圖:

          流程描述一下:

          1. 用戶使用賬號、密碼登錄應(yīng)用,登錄的請求發(fā)送到Authentication Server。
          2. Authentication Server進(jìn)行用戶驗(yàn)證,然后創(chuàng)建JWT字符串返回給客戶端。
          3. 客戶端請求接口時,在請求頭帶上JWT。
          4. Application Server驗(yàn)證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:發(fā)行人、exp:到期時間、sub:主題、aud:用戶、nbf:在此之前不可用、iat:發(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)換為字符串保存。

          Signature

          JWT第三部分是簽名。是這樣生成的,首先需要指定一個secret,該secret僅僅保存在服務(wù)器中,保證不能讓其他用戶知道。然后使用Header指定的算法對Header和Payload進(jìn)行計(jì)算,然后就得出一個簽名哈希。也就是Signature。

          那么Application Server如何進(jìn)行驗(yàn)證呢?可以利用JWT前兩段,用同一套哈希算法和同一個secret計(jì)算一個簽名值,然后把計(jì)算出來的簽名值和收到的JWT第三段比較,如果相同則認(rèn)證通過。

          JWT的優(yōu)點(diǎn)

          • json格式的通用性,所以JWT可以跨語言支持,比如Java、JavaScript、PHP、Node等等。
          • 可以利用Payload存儲一些非敏感的信息。
          • 便于傳輸,JWT結(jié)構(gòu)簡單,字節(jié)占用小。
          • 不需要在服務(wù)端保存會話信息,易于應(yīng)用的擴(kuò)展。

          怎么使用JWT

          首先引入Maven依賴。

          <dependency>
          ????<groupId>io.jsonwebtokengroupId>
          ????<artifactId>jjwtartifactId>
          ????<version>0.9.1version>
          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?map)?throws?Exception?{
          ????????JwtBuilder?builder?=?Jwts.builder()
          ????????????????.setSubject(null)?//?發(fā)行者
          ????????????????.setId(id)
          ????????????????.setSubject(subject)
          ????????????????.setIssuedAt(new?Date())?//?發(fā)行時間
          ????????????????.signWith(SignatureAlgorithm.HS256,?secretKey)?//?簽名類型?與?密鑰
          ????????????????.compressWith(CompressionCodecs.DEFLATE);//?對載荷進(jìn)行壓縮
          ????????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?{
          ????????//登錄驗(yàn)證
          ????????User?user?=?userMapper.findByUserNameAndPassword(userName,?passWord);
          ????????if?(user?==?null)?{
          ????????????return?null;
          ????????}
          ????????//如果能查出,則表示賬號密碼正確,生成jwt返回
          ????????String?uuid?=?UUID.randomUUID().toString().replace("-",?"");
          ????????HashMap?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);

          }

          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)如下:

          啟動項(xiàng)目,然后用POSTMAN請求login接口。

          返回的jwt字符串如下:

          eyJhbGciOiJIUzI1NiIsInppcCI6IkRFRiJ9.eNqqVspLzE1VslJ6OnHFsxnzX67coKSjlJgOFDEzqAUAAAD__w.qib2DrjRKcFnY77Cuh_b1zSzXfISOpCA-g8PlAZCWoU

          接著我們寫一個接口接收這個jwt,并做驗(yàn)證。

          @RestController
          @RequestMapping("/jwt")
          public?class?TestController?{

          ????@Resource
          ????private?JwtUtil?jwtUtil;

          ????@RequestMapping("/test")
          ????public?Map?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?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ìn)去解析,會報(bào)以下錯誤:

          還記得原理吧,是根據(jù)前面兩部分(Header、Payload)加上secretKey使用Header指定的哈希算法計(jì)算出第三部分(Signature),所以可以看出最關(guān)鍵就是secretKey。secretKey只有服務(wù)端自己知道,所以客戶端不知道secretKey的值是偽造不了jwt字符串的。

          總結(jié)

          最后講講JWT的缺點(diǎn),任何技術(shù)都不是完美的,所以我們得用辯證思維去看待任何一項(xiàng)技術(shù)。

          • 安全性沒法保證,所以jwt里不能存儲敏感數(shù)據(jù)。因?yàn)閖wt的payload并沒有加密,只是用Base64編碼而已。
          • 無法中途廢棄。因?yàn)橐坏┖灠l(fā)了一個jwt,在到期之前始終都是有效的,如果用戶信息發(fā)生更新了,只能等舊的jwt過期后重新簽發(fā)新的jwt。
          • 續(xù)簽問題。當(dāng)簽發(fā)的jwt保存在客戶端,客戶端一直在操作頁面,按道理應(yīng)該一直為客戶端續(xù)長有效時間,否則當(dāng)jwt有效期到了就會導(dǎo)致用戶需要重新登錄。那么怎么為jwt續(xù)簽?zāi)??最簡單粗暴就是每次簽發(fā)新的jwt,但是由于過于暴力,會影響性能。如果要優(yōu)雅一點(diǎn),又要引入Redis解決,但是這又把無狀態(tài)的jwt硬生生變成了有狀態(tài)的,違背了初衷。

          所以印證了那句話,沒有最好的技術(shù),只有適合的技術(shù)。感謝大家的閱讀,希望看完之后能對你有所收獲。

          覺得有用就點(diǎn)個贊吧,你的點(diǎn)贊是我創(chuàng)作的最大動力~

          我是一個努力讓大家記住的程序員。我們下期再見?。?!

          能力有限,如果有什么錯誤或者不當(dāng)之處,請大家批評指正,一起學(xué)習(xí)交流!

          瀏覽 34
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线成人超碰 | 97成人毛片 | 人人操人人看人人摸 | 欧美亚洲综合色图 | 中文字幕第6页 |