Spring框架整合Java Web Token問(wèn)題
Java Web Token
JSON Web Token(JWT)是一個(gè)非常輕巧的規(guī)范。
這個(gè)規(guī)范允許我們使用JWT在用戶和服務(wù)器之間傳遞安全可靠的信息。
JWT組成
一個(gè)JWT實(shí)際上就是一個(gè)字符串,它由三部分組成,頭部、載荷與簽名。
荷載
{ "iss": "John Wu JWT", "iat": 1441593502, "exp": 1441594722, "aud": "www.example.com", "sub": "jrocket@example.com", "from_user": "B", "target_user": "A" }
這里面的前五個(gè)字段都是由JWT的標(biāo)準(zhǔn)所定義的。
- iss: 該JWT的簽發(fā)者
- sub: 該JWT所面向的用戶
- aud: 接收該JWT的一方
- exp(expires): 什么時(shí)候過(guò)期,這里是一個(gè)Unix時(shí)間戳
- iat(issued at): 在什么時(shí)候簽發(fā)的
這些定義都可以在標(biāo)準(zhǔn)中找到。
將上面的JSON對(duì)象進(jìn)行[base64編碼]可以得到下面的字符串。
這個(gè)字符串我們將它稱作JWT的Payload(載荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
頭部
JWT還需要一個(gè)頭部,頭部用于描述關(guān)于該JWT的最基本的信息,例如其類型以及簽名所用的算法等。
這也可以被表示成一個(gè)JSON對(duì)象。
{ “typ”: “JWT”, “alg”: “HS256” }
在這里,我們說(shuō)明了這是一個(gè)JWT,并且我們所用的簽名算法(后面會(huì)提到)是HS256算法。(算法根據(jù)實(shí)際情況而變)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
簽名
簽名就是將荷載和頭部編碼用.號(hào)連接在一起就形成了
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0
我們將上面拼接完的字符串用HS256算法進(jìn)行加密。
在加密的時(shí)候,我們還需要提供一個(gè)密鑰(secret)。
如果我們用mystar作為密鑰的話,那么就可以得到我們加密后的內(nèi)容
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
最后將這一部分簽名也拼接在被簽名的字符串后面,我們就得到了完整的JWT
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
簽名的目的:
最后一步簽名的過(guò)程,實(shí)際上是對(duì)頭部以及載荷內(nèi)容進(jìn)行簽名。一般而言,加密算法對(duì)于不同的輸入產(chǎn)生的輸出總是不一樣的。對(duì)于兩個(gè)不同的輸入,產(chǎn)生同樣的輸出的概率極其地?。ㄓ锌赡鼙任页墒澜缡赘坏母怕蔬€?。?。所以,我們就把“不一樣的輸入產(chǎn)生不一樣的輸出”當(dāng)做必然事件來(lái)看待吧。
所以,如果有人對(duì)頭部以及載荷的內(nèi)容解碼之后進(jìn)行修改,再進(jìn)行編碼的話,那么新的頭部和載荷的簽名和之前的簽名就將是不一樣的。而且,如果不知道服務(wù)器加密的時(shí)候用的密鑰的話,得出來(lái)的簽名也一定會(huì)是不一樣的。
服務(wù)器應(yīng)用在接受到JWT后,會(huì)首先對(duì)頭部和載荷的內(nèi)容用同一算法再次簽名。那么服務(wù)器應(yīng)用是怎么知道我們用的是哪一種算法呢?別忘了,我們?cè)贘WT的頭部中已經(jīng)用alg字段指明了我們的加密算法了。
如果服務(wù)器應(yīng)用對(duì)頭部和載荷再次以同樣方法簽名之后發(fā)現(xiàn),自己計(jì)算出來(lái)的簽名和接受到的簽名不一樣,那么就說(shuō)明這個(gè)Token的內(nèi)容被別人動(dòng)過(guò)的,我們應(yīng)該拒絕這個(gè)Token,返回一個(gè)HTTP 401 Unauthorized響應(yīng)。
我們可以看到,JWT適合用于向Web應(yīng)用傳遞一些非敏感信息。例如在上面提到的完成加好友的操作,還有諸如下訂單的操作等等。
其實(shí)JWT還經(jīng)常用于設(shè)計(jì)用戶認(rèn)證和授權(quán)系統(tǒng),甚至實(shí)現(xiàn)Web應(yīng)用的單點(diǎn)登錄。
Spring使用JWT
Maven配置方式
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.2.0</version> </dependency>
JWT算法(了解)
JWS | 算法 | 介紹 |
---|---|---|
HS256 | HMAC256 | HMAC with SHA-256 |
HS384 | HMAC384 | HMAC with SHA-384 |
HS512 | HMAC512 | HMAC with SHA-512 |
RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
使用方法
選擇算法:
算法定義了一個(gè)令牌是如何被簽名和驗(yàn)證的。它可以用HMAC算法的原始值來(lái)實(shí)例化,也可以在RSA和ECDSA算法的情況下對(duì)密鑰對(duì)或密鑰提供程序進(jìn)行實(shí)例化。創(chuàng)建后,該實(shí)例可用于令牌簽名和驗(yàn)證操作。
在使用RSA或ECDSA算法時(shí),只需要簽署JWTs,就可以通過(guò)傳遞null值來(lái)避免指定公鑰。當(dāng)您需要驗(yàn)證JWTs時(shí),也可以使用私鑰進(jìn)行操作
使用靜態(tài)的字符密文或者key來(lái)獲取算法器:
//HMAC Algorithm algorithmHS = Algorithm.HMAC256("secret"); //RSA RSAPublicKey publicKey = //Get the key instance RSAPrivateKey privateKey = //Get the key instance Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey);
使用一個(gè)key提供者來(lái)獲取算法:
通過(guò)使用KeyProvider,您可以在運(yùn)行時(shí)更改密鑰,用于驗(yàn)證令牌簽名或?yàn)镽SA或ECDSA算法簽署一個(gè)新的令牌。
這是通過(guò)實(shí)現(xiàn)RSAKeyProvider或ECDSAKeyProvider方法實(shí)現(xiàn)的:
getPublicKeyById(String kid)
: 它在令牌簽名驗(yàn)證中調(diào)用,它應(yīng)該返回用于驗(yàn)證令牌的密鑰。如果使用了關(guān)鍵的輪換,例如JWK,它可以使用id來(lái)獲取正確的輪換鍵(或者只是一直返回相同的鍵)。getPrivateKey()
: 在令牌簽名期間調(diào)用它,它應(yīng)該返回用于簽署JWT的密鑰。getPrivateKeyId()
:在令牌簽名期間調(diào)用它,它應(yīng)該返回標(biāo)識(shí)由getPrivateKey()返回的鍵的id的id。這個(gè)值比JWTCreator.Builder和keyid(String)方法中的值更受歡迎。如果您不需要設(shè)置孩子的值,就避免使用KeyProvider實(shí)例化算法。
創(chuàng)建JWT
try { Algorithm algorithm = Algorithm.HMAC256("secret"); String token = JWT.create() .withIssuer("auth0") .sign(algorithm); } catch (UnsupportedEncodingException exception){ //UTF-8 encoding not supported } catch (JWTCreationException exception){ //Invalid Signing configuration / Couldn't convert Claims. }
如果Claim不能轉(zhuǎn)換為JSON,或者在簽名過(guò)程中使用的密鑰無(wú)效,那么將會(huì)拋出JWTCreationException異常。
驗(yàn)證令牌
首先需要通過(guò)調(diào)用jwt.require()和傳遞算法實(shí)例來(lái)創(chuàng)建一個(gè)JWTVerifier實(shí)例。
如果您要求令牌具有特定的Claim值,請(qǐng)使用構(gòu)建器來(lái)定義它們。
方法build()返回的實(shí)例是可重用的,因此您可以定義一次,并使用它來(lái)驗(yàn)證不同的標(biāo)記。
最后調(diào)用verifier.verify()來(lái)驗(yàn)證token
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE"; try { Algorithm algorithm = Algorithm.HMAC256("secret"); JWTVerifier verifier = JWT.require(algorithm) .withIssuer("auth0") .build(); //Reusable verifier instance DecodedJWT jwt = verifier.verify(token); } catch (UnsupportedEncodingException exception){ //UTF-8 encoding not supported } catch (JWTVerificationException exception){ //Invalid signature/claims }
時(shí)間驗(yàn)證
當(dāng)驗(yàn)證一個(gè)令牌時(shí),時(shí)間驗(yàn)證會(huì)自動(dòng)發(fā)生,導(dǎo)致在值無(wú)效時(shí)拋出一個(gè)JWTVerificationException。如果前面的任何一個(gè)字段都丟失了,那么在這個(gè)驗(yàn)證中就不會(huì)考慮這些字段。
要指定令牌仍然被認(rèn)為有效的余地窗口,在JWTVerifier builder中使用accept回旋()方法,并傳遞一個(gè)正值的秒值。這適用于上面列出的每一項(xiàng)。
JWTVerifier verifier = JWT.require(algorithm) .acceptLeeway(1) // 1 sec for nbf, iat and exp .build();
您還可以為給定的日期聲明指定一個(gè)自定義值,并為該聲明覆蓋缺省值。
JWTVerifier verifier = JWT.require(algorithm) .acceptLeeway(1) //1 sec for nbf and iat .acceptExpiresAt(5) //5 secs for exp .build();
信息解析
Algorithm (“alg”)
返回jwt的算法值或,如果沒(méi)有定義則返回null
String algorithm = jwt.getAlgorithm();
如果您需要在您的lib/app中測(cè)試此行為,將驗(yàn)證實(shí)例轉(zhuǎn)換為basever可視化,以獲得verific.build()方法的可見(jiàn)性,該方法可以接受定制的時(shí)鐘。
例如:
BaseVerification verification = (BaseVerification) JWT.require(algorithm) .acceptLeeway(1) .acceptExpiresAt(5); Clock clock = new CustomClock(); //Must implement Clock interface JWTVerifier verifier = verification.build(clock);
Type (“typ”)
返回jwt的類型值,如果沒(méi)有定義則返回null(多數(shù)情況類型值為jwt)
String type = jwt.getType();
Content Type (“cty”)
返回內(nèi)容的類型,如果沒(méi)有定義則返回null
String contentType = jwt.getContentType();
Key Id (“kid”)
返回key的id值,如果沒(méi)有定義則返回null
String keyId = jwt.getKeyId();
自定義字段
在令牌的頭部中定義的附加聲明可以通過(guò)調(diào)用getHeaderClaim() 獲取,即使無(wú)法找到,也會(huì)返回。您可以通過(guò)調(diào)用claim.isNull()來(lái)檢查聲明的值是否為null。
Claim claim = jwt.getHeaderClaim("owner");
當(dāng)使用jwt.create()創(chuàng)建一個(gè)令牌時(shí),您可以通過(guò)調(diào)用withHeader()來(lái)指定頭聲明,并同時(shí)傳遞聲明的映射。
Map<String, Object> headerClaims = new HashMap(); headerClaims.put("owner", "auth0"); String token = JWT.create() .withHeader(headerClaims) .sign(algorithm);
提示:在簽名過(guò)程之后,alg和typ值將始終包含在Header中。
JWT的負(fù)載(Payload)聲明
Issuer ("iss")
返回簽發(fā)者的名稱值,如果沒(méi)有在負(fù)載中定義則返回null
String issuer = jwt.getIssuer(); Subject ("sub")
返回jwt所面向的用戶的值,如果沒(méi)有在負(fù)載中定義則返回null
String subject = jwt.getSubject(); Audience ("aud")
返回該jwt由誰(shuí)接收,如果沒(méi)有在負(fù)載中定義則返回null
List<String> audience = jwt.getAudience(); Expiration Time ("exp")
返回該jwt的過(guò)期時(shí)間,如果在負(fù)載中沒(méi)有定義則返回null
Date expiresAt = jwt.getExpiresAt(); Not Before ("nbf")
Returns the Not Before value or null if it’s not defined in the Payload.
Date notBefore = jwt.getNotBefore(); Issued At ("iat")
返回在什么時(shí)候簽發(fā)的,如果在負(fù)載中沒(méi)有定義則返回null
Date issuedAt = jwt.getIssuedAt(); JWT ID ("jti")
返回該jwt的唯一標(biāo)志,如果在負(fù)載中沒(méi)有定義則返回null
String id = jwt.getId();
自定義聲明
在令牌有效負(fù)載中定義的附加聲明可以通過(guò)調(diào)用getClaims()或 getClaim()和傳遞聲明名來(lái)獲得。即使無(wú)法找到聲明,也將會(huì)有返回值。您可以通過(guò)調(diào)用claim.isNull()來(lái)檢查聲明的值是否為null。
Map<String, Claim> claims = jwt.getClaims(); //Key is the Claim name Claim claim = claims.get("isAdmin");
或者:
Claim claim = jwt.getClaim("isAdmin");
當(dāng)使用jwt.create()創(chuàng)建一個(gè)令牌時(shí),您可以通過(guò)調(diào)用withClaim()來(lái)指定自定義聲明,并同時(shí)傳遞名稱和值。
String token = JWT.create() .withClaim("name", 123) .withArrayClaim("array", new Integer[]{1, 2, 3}) .sign(algorithm);
您還可以通過(guò)調(diào)用withClaim()來(lái)驗(yàn)證jwt.require()的自定義聲明,并傳遞該名稱和所需的值。
JWTVerifier verifier = JWT.require(algorithm) .withClaim("name", 123) .withArrayClaim("array", 1, 2, 3) .build(); DecodedJWT jwt = verifier.verify("my.jwt.token");
實(shí)例
package course.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.google.common.collect.Maps; import course.pojo.User; import java.io.UnsupportedEncodingException; import java.util.Date; import java.util.Map; public class JwtUtils { //創(chuàng)建token public static String creatToken(User user) throws IllegalArgumentException, UnsupportedEncodingException{ Algorithm algorithm = Algorithm.HMAC256("secret"); String username = user.getUsername(); Map<String, Object> map = Maps.newHashMap(); map.put("alg", "HS256"); map.put("typ", "JWT"); String token = JWT.create().withHeader(map) .withClaim("username", username) .withExpiresAt(new Date(System.currentTimeMillis()+360000)) .sign(algorithm); return token; } //驗(yàn)證jwt public static DecodedJWT verifyJwt(String token){ DecodedJWT decodedJWT = null; try{ Algorithm algorithm = Algorithm.HMAC256("secret"); JWTVerifier jwtVerifier = JWT.require(algorithm).build(); decodedJWT = jwtVerifier.verify(token); }catch(IllegalArgumentException e){ e.printStackTrace(); }catch (UnsupportedEncodingException e){ e.printStackTrace(); }catch(JWTVerificationException e) { e.printStackTrace(); } return decodedJWT; } public static void main(String[] args) throws UnsupportedEncodingException{ // String username = "root"; // Integer id =1; // System.out.println(creatToken(username,id)); // String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDgxMzgxNDAsInVzZXJJZCI6MSwidXNlcm5hbWUiOiJyb290In0.OeRdHJZKmxFBqIN-A-uSNQK8JyKdzX-wcFR883oMqFA"; // System.out.println(verifyJwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MDgxMzgxNDAsInVzZXJJZCI6MSwidXNlcm5hbWUiOiJyb290In0.OeRdHJZKmxFBqIN-A-uSNQK8JyKdzX-wcFR883oMqFA")); // System.out.println(verifyJwt(token).getClaims().get("username").asString()); } }
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring boot通過(guò)HttpSessionListener監(jiān)聽(tīng)器統(tǒng)計(jì)在線人數(shù)的實(shí)現(xiàn)代碼
這篇文章主要介紹了Spring boot通過(guò)HttpSessionListener監(jiān)聽(tīng)器統(tǒng)計(jì)在線人數(shù)的實(shí)現(xiàn)代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-02-02解決SpringBoot自定義攔截器和跨域配置沖突的問(wèn)題
這篇文章主要介紹了解決SpringBoot自定義攔截器和跨域配置沖突的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08Java常用 Date 時(shí)間格式化、Calender日歷、正則表達(dá)式的用法小結(jié)
SimpleDateFormat?是Java中用于格式化和解析日期的類,它允許你將日期對(duì)象轉(zhuǎn)換為特定格式的字符串,或者將特定格式的字符串轉(zhuǎn)換為日期對(duì)象,這篇文章主要介紹了Java常用 Date 時(shí)間格式化、Calender日歷、正則表達(dá)式的用法,需要的朋友可以參考下2024-12-12java正則表達(dá)式的應(yīng)用 java讀取文件并獲取電話號(hào)碼
這篇文章主要介紹了java正則表達(dá)式的應(yīng)用,應(yīng)用的內(nèi)容是java讀取文件并獲取電話號(hào)碼,感興趣的小伙伴們可以參考一下2015-11-11java返回前端實(shí)體類json數(shù)據(jù)時(shí)忽略某個(gè)屬性方法
這篇文章主要給大家介紹了關(guān)于java返回前端實(shí)體類json數(shù)據(jù)時(shí)忽略某個(gè)屬性的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08如何基于java實(shí)現(xiàn)Gauss消元法過(guò)程解析
這篇文章主要介紹了如何基于java實(shí)現(xiàn)Gauss消元法過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10淺談常用Java數(shù)據(jù)庫(kù)連接池(小結(jié))
這篇文章主要介紹了淺談常用Java數(shù)據(jù)庫(kù)連接池(小結(jié)),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07