JAVA后端實現(xiàn)JWT令牌的示例
首先解釋一下JWT,在此之前,我們需要明確為什么需要JWT
登陸校驗
其實最常見的應用場景就是登陸校驗,我們希望某個用戶在初次打開網(wǎng)址時,首先應該進行登陸操作,而不是直接訪問某個功能,并且在登陸之后,訪問其他功能時不需要再次登陸。在此前提下,應運而生三種登陸校驗方式,下面我們一一講解。
會話跟蹤
什么是會話,我們摒棄其他科普詞條繁雜的專業(yè)詞匯介紹,我們就這樣認為:你去買東西,從你走進了一家商店開始,你就開始了你和“商店”的會話,你買完東西走出商店,此次會話結束。在這里面,“你”就是瀏覽器,“商店”就是服務器。我們買東西,不可能每買一個,都需要重新進入商店(登錄),也不可能不進入商店,就可以買東西(舉例,以線下商店為例)。
接下來我們正式講解什么是會話跟蹤,詞條解釋是這樣的:一種維護瀏覽器狀態(tài)的方法,服務器需要識別多次請求是否來自于同一瀏覽器,以便在同一次會話的多次請求間共享數(shù)據(jù)。
為什么我們需要會話跟蹤,這是因為HTTP協(xié)議是一種無狀態(tài)的協(xié)議。所謂無狀態(tài),指的是每一次請求都是獨立的,下一次請求并不會攜帶上一次請求的數(shù)據(jù)。而瀏覽器與服務器之間進行交互,基于HTTP協(xié)議也就意味著現(xiàn)在我們通過瀏覽器來訪問了登陸這個接口,實現(xiàn)了登陸的操作,接下來我們在執(zhí)行其他業(yè)務操作時,服務器也并不知道這個員工到底登陸了沒有。因為HTTP協(xié)議是無狀態(tài)的,兩次請求之間是獨立的,所以是無法判斷這個員工到底登陸了沒有。因此我們需要會話跟蹤,去“記錄”交互操作。
我們有三種會話跟蹤技術,分別是:cookie、session、令牌(只需了解jwt的可以跳過前兩項介紹)
cookie
cookie 是客戶端會話跟蹤技術,它是存儲在客戶端瀏覽器的,我們使用 cookie 來跟蹤會話,我們就可以在瀏覽器第一次發(fā)起請求來請求服務器的時候,我們在服務器端來設置一個cookie。
比如第一次請求了登錄接口,登錄接口執(zhí)行完成之后,我們就可以設置一個cookie,在 cookie 當中我們就可以來存儲用戶相關的一些數(shù)據(jù)信息。比如我可以在 cookie 當中來存儲當前登錄用戶的用戶名,用戶的ID。
服務器端在給客戶端在響應數(shù)據(jù)的時候,會自動的將 cookie 響應給瀏覽器,瀏覽器接收到響應回來的 cookie 之后,會自動的將 cookie 的值存儲在瀏覽器本地。接下來在后續(xù)的每一次請求當中,都會將瀏覽器本地所存儲的 cookie 自動地攜帶到服務端。
切記,以上三步都是自動進行。那么為什么會自動進行?
因為 cookie 它是 HTP 協(xié)議當中所支持的技術,而各大瀏覽器廠商都支持了這一標準。在 HTTP 協(xié)議官方給我們提供了一個響應頭和請求頭:
響應頭 Set-Cookie :設置Cookie數(shù)據(jù)的
請求頭 Cookie:攜帶Cookie數(shù)據(jù)的
接下來在服務端我們就可以獲取到 cookie 的值。我們可以去判斷一下這個 cookie 的值是否存在,如果不存在這個cookie,就說明客戶端之前是沒有訪問登錄接口的;如果存在 cookie 的值,就說明客戶端之前已經(jīng)登錄完成了。這樣我們就可以基于 cookie 在同一次會話的不同請求之間來共享數(shù)據(jù)。
我們使用代碼實現(xiàn):
@Slf4j @RestController public class SessionController { //設置Cookie @GetMapping("/c1") public Result cookie1(HttpServletResponse response){ response.addCookie(new Cookie("login_username","itheima")); //設置Cookie/響應Cookie return Result.success(); } //獲取Cookie @GetMapping("/c2") public Result cookie2(HttpServletRequest request){ Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if(cookie.getName().equals("login_username")){ System.out.println("login_username: "+cookie.getValue()); //輸出name為login_username的cookie } } return Result.success(); } }
訪問c1接口,設置Cookie,http://localhost:8080/c1
我們可以看到,設置的cookie,通過響應頭Set-Cookie響應給瀏覽器,并且瀏覽器會將Cookie,存儲在瀏覽器端。
訪問c2接口 http://localhost:8080/c2,此時瀏覽器會自動的將Cookie攜帶到服務端。
優(yōu)點:HTTP協(xié)議中支持的技術(像Set-Cookie 響應頭的解析以及 Cookie 請求頭數(shù)據(jù)的攜帶,都是瀏覽器自動進行的,是無需我們手動操作的)
缺點:
移動端APP(Android、IOS)中無法使用Cookie
不安全,用戶可以自己禁用Cookie
Cookie不能跨域(跨域參考:http://www.dbjr.com.cn/javascript/333320pbv.htm)
session
其實session的底層就是基于我們剛才所介紹的 Cookie 來實現(xiàn)的。
獲取Session
如果我們現(xiàn)在要基于 Session 來進行會話跟蹤,瀏覽器在第一次請求服務器的時候,我們就可以直接在服務器當中來獲取到會話對象Session。如果是第一次請求Session ,會話對象是不存在的,這個時候服務器會自動的創(chuàng)建一個會話對象Session 。而每一個會話對象Session ,它都有一個ID(示意圖中Session后面括號中的1,就表示ID),我們稱之為 Session 的ID。
響應Cookie (JSESSIONID)
接下來,服務器端在給瀏覽器響應數(shù)據(jù)的時候,它會將 Session 的 ID 通過 Cookie 響應給瀏覽器。其實在響應頭當中增加了一個 Set-Cookie 響應頭。這個 Set-Cookie 響應頭對應的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服務器端會話對象 Session 的 ID。瀏覽器會自動識別這個響應頭,然后自動將Cookie存儲在瀏覽器本地。
查找Session
接下來,在后續(xù)的每一次請求當中,都會將 Cookie 的數(shù)據(jù)獲取出來,并且攜帶到服務端。接下來服務器拿到JSESSIONID這個 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就會從眾多的 Session 當中來找到當前請求對應的會話對象Session。
這樣我們是不是就可以通過 Session 會話對象在同一次會話的多次請求之間來共享數(shù)據(jù)了?好,這就是基于 Session 進行會話跟蹤的流程。
我們使用代碼實現(xiàn):
@Slf4j @RestController public class SessionController { @GetMapping("/s1") public Result session1(HttpSession session){ log.info("HttpSession-s1: {}", session.hashCode()); session.setAttribute("loginUser", "tom"); //往session中存儲數(shù)據(jù) return Result.success(); } @GetMapping("/s2") public Result session2(HttpServletRequest request){ HttpSession session = request.getSession(); log.info("HttpSession-s2: {}", session.hashCode()); Object loginUser = session.getAttribute("loginUser"); //從session中獲取數(shù)據(jù) log.info("loginUser: {}", loginUser); return Result.success(loginUser); } }
訪問 s1 接口,http://localhost:8080/s1
請求完成之后,在響應頭中,就會看到有一個Set-Cookie的響應頭,里面響應回來了一個Cookie,就是JSESSIONID,這個就是服務端會話對象 Session 的ID。
訪問 s2 接口,http://localhost:8080/s2
接下來,在后續(xù)的每次請求時,都會將Cookie的值,攜帶到服務端,那服務端呢,接收到Cookie之后,會自動的根據(jù)JSESSIONID的值,找到對應的會話對象Session。 兩次請求,獲取到的Session會話對象的hashcode是一樣的,就說明是同一個會話對象。而且,第一次請求時,往Session會話對象中存儲的值,第二次請求時,也獲取到了。 那這樣,我們就可以通過Session會話對象,在同一個會話的多次請求之間來進行數(shù)據(jù)共享了。
優(yōu)缺點
優(yōu)點:Session是存儲在服務端的,安全
缺點:
服務器集群環(huán)境下無法直接使用Session
移動端APP(Android、IOS)中無法使用Cookie
用戶可以自己禁用Cookie
Cookie不能跨域
是的,想必你們也發(fā)現(xiàn)了,cookie的缺點,session也有,畢竟session就是基于cookie實現(xiàn)的。
下面我們開始本文目標:JWT
令牌
這里我們所提到的令牌,其實它就是一個用戶身份的標識,看似很高大上,很神秘,其實本質(zhì)就是一個字符串。想必很多90后都曾經(jīng)擁有過一個實體的QQ令牌,并且現(xiàn)在Steam也有登錄的令牌。實質(zhì)上他們的功能作用是一樣的。其實它就是一個用戶身份的標識,本質(zhì)就是一個字符串。
如果通過令牌技術來跟蹤會話,我們就可以在瀏覽器發(fā)起請求。在請求登錄接口的時候,如果登錄成功,我就可以生成一個令牌,令牌就是用戶的合法身份憑證。接下來我在響應數(shù)據(jù)的時候,我就可以直接將令牌響應給前端。
接下來我們在前端程序當中接收到令牌之后,就需要將這個令牌存儲起來。這個存儲可以存儲在 cookie 當中,也可以存儲在其他的存儲空間(比如:localStorage)當中。
接下來,在后續(xù)的每一次請求當中,都需要將令牌攜帶到服務端。攜帶到服務端之后,接下來我們就需要來校驗令牌的有效性。如果令牌是有效的,就說明用戶已經(jīng)執(zhí)行了登錄操作,如果令牌是無效的,就說明用戶之前并未執(zhí)行登錄操作。
此時,如果是在同一次會話的多次請求之間,我們想共享數(shù)據(jù),我們就可以將共享的數(shù)據(jù)存儲在令牌當中就可以了。
優(yōu)缺點
優(yōu)點:
支持PC端、移動端
解決集群環(huán)境下的認證問題
減輕服務器的存儲壓力(無需在服務器端存儲)
缺點:需要自己實現(xiàn)(包括令牌的生成、令牌的傳遞、令牌的校驗)
針對于這三種方案,現(xiàn)在企業(yè)開發(fā)當中使用的最多的就是第三種令牌技術進行會話跟蹤。而前面的這兩種傳統(tǒng)的方案,現(xiàn)在企業(yè)項目開發(fā)當中已經(jīng)很少使用了。所以在我們的課程當中,我們也將會采用令牌技術來解決案例項目當中的會話跟蹤問題。
JWT
JWT全稱:JSON Web Token (官網(wǎng):JSON Web Tokens - jwt.io)
定義了一種簡潔的、自包含的格式,用于在通信雙方以json數(shù)據(jù)格式安全的傳輸信息。由于數(shù)字簽名的存在,這些信息是可靠的。
簡潔:是指jwt就是一個簡單的字符串??梢栽谡埱髤?shù)或者是請求頭當中直接傳遞。
自包含:指的是jwt令牌,看似是一個隨機的字符串,但是我們是可以根據(jù)自身的需求在jwt令牌中存儲自定義的數(shù)據(jù)內(nèi)容。如:可以直接在jwt令牌中存儲用戶的相關信息。
簡單來講,jwt就是將原始的json數(shù)據(jù)格式進行了安全的封裝,這樣就可以直接基于jwt在通信雙方安全的進行信息傳輸了。
JWT的組成: (JWT令牌由三個部分組成,三個部分之間使用英文的點來分割)
第一部分:Header(頭), 記錄令牌類型、簽名算法等。 例如:{"alg":"HS256","type":"JWT"}
第二部分:Payload(有效載荷),攜帶一些自定義信息、默認信息等。 例如:{"id":"1","username":"Tom"}
第三部分:Signature(簽名),防止Token被篡改、確保安全性。將header、payload,并加入指定秘鑰,通過指定簽名算法計算而來。
簽名的目的就是為了防jwt令牌被篡改,而正是因為jwt令牌最后一個部分數(shù)字簽名的存在,所以整個jwt 令牌是非常安全可靠的。一旦jwt令牌當中任何一個部分、任何一個字符被篡改了,整個令牌在校驗的時候都會失敗,所以它是非常安全可靠的。
JWT是如何將原始的JSON格式數(shù)據(jù),轉變?yōu)樽址哪兀?/p>
其實在生成JWT令牌時,會對JSON格式的數(shù)據(jù)進行一次編碼:進行base64編碼
Base64:是一種基于64個可打印的字符來表示二進制數(shù)據(jù)的編碼方式。既然能編碼,那也就意味著也能解碼。所使用的64個字符分別是A到Z、a到z、 0- 9,一個加號,一個斜杠,加起來就是64個字符。任何數(shù)據(jù)經(jīng)過base64編碼之后,最終就會通過這64個字符來表示。當然還有一個符號,那就是等號。等號它是一個補位的符號
需要注意的是Base64是編碼方式,而不是加密方式。
JWT令牌最典型的應用場景就是登錄認證:
1. 在瀏覽器發(fā)起請求來執(zhí)行登錄操作,此時會訪問登錄的接口,如果登錄成功之后,我們需要生成一個jwt令牌,將生成的 jwt令牌返回給前端。
2. 前端拿到jwt令牌之后,會將jwt令牌存儲起來。在后續(xù)的每一次請求中都會將jwt令牌攜帶到服務端。
3. 服務端統(tǒng)一攔截請求之后,先來判斷一下這次請求有沒有把令牌帶過來,如果沒有帶過來,直接拒絕訪問,如果帶過來了,還要校驗一下令牌是否是有效。如果有效,就直接放行進行請求的處理。
在JWT登錄認證的場景中我們發(fā)現(xiàn),整個流程當中涉及到兩步操作:
1. 在登錄成功之后,要生成令牌。
2. 每一次請求當中,要接收令牌并對令牌進行校驗。
簡單介紹了JWT令牌以及JWT令牌的組成之后,接下來我們就來學習基于Java代碼如何生成和校驗JWT令牌。
首先我們先來實現(xiàn)JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依賴:
<!-- JWT依賴--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
接下來我們生成算法:
@Test public void genJwt(){ Map<String,Object> claims = new HashMap<>(); claims.put("id",1); claims.put("username","HongKongDoll"); String jwt = Jwts.builder() .setClaims(claims) //自定義內(nèi)容(載荷) .signWith(SignatureAlgorithm.HS256, "bug,start") //簽名算法 .setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期 .compact(); System.out.println(jwt); }
走你:
這時我們使用utools上的插件進行解碼(或者找任一在線解碼網(wǎng)站都可)
第一部分解析出來,看到JSON格式的原始數(shù)據(jù),所使用的簽名算法為HS256。
第二個部分是我們自定義的數(shù)據(jù),之前我們自定義的數(shù)據(jù)就是id,還有一個exp代表的是我們所設置的過期時間。
由于前兩個部分是base64編碼,所以是可以直接解碼出來。但最后一個部分并不是base64編碼,是經(jīng)過簽名算法計算出來的,所以最后一個部分是不會解析的。
實現(xiàn)了JWT令牌的生成,下面我們接著使用Java代碼來校驗JWT令牌(解析生成的令牌):
@Test public void parseJwt(){ Claims claims = Jwts.parser() .setSigningKey("bug,start")//指定簽名密鑰(必須保證和生成令牌時使用相同的簽名密鑰) .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNzA0ODU1NTk5LCJ1c2VybmFtZSI6IkhvbmdLb25nRG9sbCJ9.JLg62R07Zr_IEZtaZ4oAQNkGoNIdGKrLbcy-OUCTTPU") .getBody(); System.out.println(claims); }
走我:
令牌解析后,我們可以看到id和過期時間,如果在解析的過程當中沒有報錯,就說明解析成功了。
接下來,為了驗證JWT的可靠性,我們修改JWT中其中任一字母,重新進行解析:
走他:
我只是把第一位的e換成了E,結果就報錯:JWT解析異常。說明解析JWT只要修改其中任一字符,就會解析失敗。
這時候又有一位未來首付要問了,那么還有其他的解析失敗因素嗎?當然有啦,那就是過期時間,我們在上面設置了過期時間,那么我們現(xiàn)在把生成策略中過期時間換成1秒過期:
我們重新解析一下:
這次報的是JWT過期異常。
通過以上測試,我們在使用JWT令牌時需要注意:
JWT校驗時使用的簽名秘鑰,必須和生成JWT令牌時使用的秘鑰是配套的。
如果JWT令牌解析校驗時報錯,則說明 JWT令牌被篡改 或 過期失效了,令牌非法。
接下來,我們進入實戰(zhàn)部分:
JWT令牌的生成和校驗的基本操作我們已經(jīng)學習完了,接下來我們就需要在案例當中通過JWT令牌技術來跟蹤會話。具體的思路我們前面已經(jīng)分析過了,主要就是兩步操作:
生成令牌
在登錄成功之后來生成一個JWT令牌,并且把這個令牌直接返回給前端校驗令牌
攔截前端請求,從請求中獲取到令牌,對令牌進行解析校驗
那我們首先來完成:登錄成功之后生成JWT令牌,并且把令牌返回給前端。
在controller同級下,建一個包,用來放我們的工具類,然后新建一個class:
/** * @Description JWT工具類 * @Author QingNing * @Date 2024/1/9 */ package com.ycg.vue.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import java.util.Map; public class JwtUtils { private static final String signKey = "bug,start";//簽名密鑰 private static final Long expire = 3600000L; //有效時間 /** * 生成JWT令牌 * @param claims JWT第二部分負載 payload 中存儲的內(nèi)容 * @return */ public static String generateJwt(Map<String, Object> claims){ String jwt = Jwts.builder() .addClaims(claims)//自定義信息(有效載荷) .signWith(SignatureAlgorithm.HS256, signKey)//簽名算法(頭部) .setExpiration(new Date(System.currentTimeMillis() + expire))//過期時間 .compact(); return jwt; } /** * 解析JWT令牌 * @param jwt JWT令牌 * @return JWT第二部分負載 payload 中存儲的內(nèi)容 */ public static Claims parseJWT(String jwt){ Claims claims = Jwts.parser() .setSigningKey(signKey)//指定簽名密鑰 .parseClaimsJws(jwt)//指定令牌Token .getBody(); return claims; } }
然后在登陸功能中添加JWT生成策略:
以下是本人的登陸代碼,僅做參考
@ApiOperation(value = "用戶登錄") @PostMapping("/login") public result user(@RequestBody UserVo userVo){ User user = userService.login(userVo); //非空判斷 if(!Objects.isNull(user)){ //生成 Map<String , Object> claims = new HashMap<>(); claims.put("id", user.getId()); claims.put("username",user.getUsername()); //使用JWT工具類,生成身份令牌 String token = JwtUtils.generateJwt(claims); return result.success(token); } return result.error("用戶名或密碼錯誤,請重新輸入"); }
我們打開swagger進行查看是否成功。
我們發(fā)現(xiàn),已經(jīng)返回了token,后續(xù)的每一次請求當中,都會將這個令牌攜帶到服務端。
此時我們只需要在前端的返回方法中,添加回調(diào)方法:
即可將jwt令牌存到請求頭中,在進行其他操作時就會攜帶jwt令牌,直至本次會話結束。后續(xù)可以使用攔截器進行判斷。
至此,我們就完成了JWT令牌的實現(xiàn)。
到此這篇關于JAVA后端實現(xiàn)JWT令牌的示例的文章就介紹到這了,更多相關JAVA JWT令牌內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
一文詳解SpringBoot?Redis多數(shù)據(jù)源配置
Spring?Boot默認只允許一種?Redis?連接池配置,且配置受限于?Lettuce?包,不夠靈活,所以本文將為大家介紹如何自定義Redis配置方案實現(xiàn)多數(shù)據(jù)源支持,需要的可以參考下2024-11-11