Web安全通讯之JWT的Java实现
上篇文章中目的是介紹 Json Web Token(以下簡稱 jwt) ,由于我對 Java 比較熟悉就介紹?Java 服務端?的實現方式,其他語言原理是相同的哈~
PS:如果不清楚JWT,請先看?《Web安全通訊之Token與JWT》
參考博客:各種語言版本的基于HMAC-SHA256的base64加密
- 官網地址:https://jwt.io/
- jwt github:https://github.com/jwtk/jjwt
- Demo源碼地址:?https://github.com/wangcantian/SecurityCommDemo
- JWT Jar 包下載:http://pan.baidu.com/s/1pLqJYUv
下面按照這幾個方面來介紹它:
- Java 基本實現
- 開源庫 jjwt 的使用
- 源碼解析 jjwt
廢話不多說,擼起袖子就是干,上代碼
Java 實現
private static final String MAC_INSTANCE_NAME = "HMacSHA256";public static String Hmacsha256(String secret, String message) throws NoSuchAlgorithmException, InvalidKeyException {Mac hmac_sha256 = Mac.getInstance(MAC_INSTANCE_NAME);SecretKeySpec key = new SecretKeySpec(secret.getBytes(), MAC_INSTANCE_NAME);hmac_sha256.init(key);byte[] buff = hmac_sha256.doFinal(message.getBytes());return Base64.encodeBase64URLSafeString(buff); }// java jwt public void testJWT() throws InvalidKeyException, NoSuchAlgorithmException {String secret = "eerp";String header = "{\"type\":\"JWT\",\"alg\":\"HS256\"}";String claim = "{\"iss\":\"cnooc\", \"sub\":\"yrm\", \"username\":\"yrm\", \"admin\":true}";String base64Header = Base64.encodeBase64URLSafeString(header.getBytes());String base64Claim = Base64.encodeBase64URLSafeString(claim.getBytes());String signature = ShaUtil.Hmacsha256(secret, base64Header + "." + base64Claim);String jwt = base64Header + "." + base64Claim + "." + signature;System.out.println(jwt); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
使用開源庫 jjwt 實現 JWT
jjwt?是 java 對 JWT 的封裝,下面演示 Java 如何使用 jjwt
添加依賴
有兩種方法添加?
1. 使用 Maven 倉庫(推薦)
- 1
- 2
- 3
- 4
- 5
簽發 JWT
public static String createJWT() {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();JwtBuilder builder = Jwts.builder().setId(id) // JWT_ID.setAudience("") // 接受者.setClaims(null) // 自定義屬性.setSubject("") // 主題.setIssuer("") // 簽發者.setIssuedAt(new Date()) // 簽發時間.setNotBefore(new Date()) // 失效時間.setExpiration(long) // 過期時間.signWith(signatureAlgorithm, secretKey); // 簽名算法以及密匙return builder.compact(); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
驗證 JWT
public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody(); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
一般我們把驗證操作作為中間件或者攔截器就行了
Java 服務端Demo沒有用流行框架,基礎的 JSP + Servlet + JavaBean
下面貼出主要的類:?
*?TokenMgr.java?
驗證和簽發的 JWT 的操作類
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- SignFilter.java?
驗證 Token 的過濾器?
PS:Token 可以放在 URL、Cookie、請求頭Auth或者body中以一種特定格式解析,這里只是規定把 Token 放在 URL 或者表單示例。?
CheckResult:驗證結果模型,包含成功Claim、通過狀態、失敗碼。由于驗證結果基本三種狀態:通過,不通過,通過但過期,因此多出失敗碼來區分開。其實驗證結果狀態還有很多,據需求決定。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- web.xml
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
其中 CorsFilter 對 API 接口的響應頭添加?Content-Type : text/json?以及編碼格式等等,所以它對?/SERTEXT/api/*?的地址進行攔截,避免影響請求靜態頁面;Filter 的執行順序是根據解析 web.xml 文件中節點的?先后順序?決定的,需要把 CorsFilter 放首位,因為假如某處拋出異常會導致返回數據亂碼。
看看 jjwt 的源碼
PS:源碼從 GIT 倉庫 Clone 下來就行了?
從使用示例代碼看得出 jjwt 使用了?Builder模式?以及靈活多變的?鏈式調用?,builder() 出?JwtBuilder?對象。?
在進行一系列鏈式?set?方法后執行 compact() 方法返回我們想要的結果,來看看它到底是怎么簽名的:
DefaultJwtBuilder.java
@Override public String compact() {......// 進行參數判斷Header header = ensureHeader();Key key = this.key;if (key == null && !Objects.isEmpty(keyBytes)) {key = new SecretKeySpec(keyBytes, algorithm.getJcaName());}JwsHeader jwsHeader;if (header instanceof JwsHeader) {jwsHeader = (JwsHeader)header;} else {jwsHeader = new DefaultJwsHeader(header);}// 構造密匙對象if (key != null) {jwsHeader.setAlgorithm(algorithm.getValue());} else {//no signature - plaintext JWT:jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue());}if (compressionCodec != null) {jwsHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName());}String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json.");String base64UrlEncodedBody;if (compressionCodec != null) {byte[] bytes;try {bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims);} catch (JsonProcessingException e) {throw new IllegalArgumentException("Unable to serialize claims object to json.");}base64UrlEncodedBody = TextCodec.BASE64URL.encode(compressionCodec.compress(bytes));} else {base64UrlEncodedBody = this.payload != null ?TextCodec.BASE64URL.encode(this.payload) :base64UrlEncode(claims, "Unable to serialize claims object to json.");}// 這里已經組成了實現 Header 和 Playload 部分String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody;if (key != null) { //jwt must be signed:JwtSigner signer = createSigner(algorithm, key);String base64UrlSignature = signer.sign(jwt);jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature;} else {// no signature (plaintext), but must terminate w/ a period, see// https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1jwt += JwtParser.SEPARATOR_CHAR;}return jwt;}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
首先會進行 payload 以及 key 的判斷,原則是 payload 與 自定義 claims 不能為 null 以及不能同時賦值參數,key 和 keyBytes 不能同時存在;然后通過 ensureHeader() 獲取?Header?對象。
protected Header ensureHeader() {if (this.header == null) {this.header = new DefaultHeader();}return this.header; }- 1
- 2
- 3
- 4
- 5
- 6
如果沒有設置自定義 Header ,則實例一個默認 Header 對象 —-?DefaultHeader,其中 Header 接口是個繼承 Map 接口的集合,符合了 header 部分鍵值對形式。?
然后 Header 實例會被“轉換”為 JwsHeader 實例,其中 JwsHeader 接口繼承 Header 接口,多定義了“簽名”和“密匙ID”這個兩個屬性。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
最終通過 base64UrlEncode() 方法的到 base64url 編碼后的 header 字符串。
protected String base64UrlEncode(Object o, String errMsg) {byte[] bytes;try {// 使用 Jackson 框架將對象序列化bytes = toJson(o);} catch (JsonProcessingException e) {throw new IllegalStateException(errMsg, e);}// 將 byte 數組轉化為 base64url 編碼的 byte 數組return TextCodec.BASE64URL.encode(bytes); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
接著同理將 payload 或 claims base64url 編碼組成 playload 部分。?
最后就是簽名部分了。createSigner(algorithm, key)?方法實例一個?DefaultJwtSigner?對象,該對象進行統一的簽名和編碼操作,它的構造函數會傳入簽名算法枚舉?SignatureAlgorithm?對象,定義所有算法的名字、描述、組類等等。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
那?DefaultJwtSigner?怎么分別實現具體算法呢?
public class DefaultJwtSigner implements JwtSigner {private static final Charset US_ASCII = Charset.forName("US-ASCII");private final Signer signer;public DefaultJwtSigner(SignatureAlgorithm alg, Key key) {this(DefaultSignerFactory.INSTANCE, alg, key);}public DefaultJwtSigner(SignerFactory factory, SignatureAlgorithm alg, Key key) {Assert.notNull(factory, "SignerFactory argument cannot be null.");this.signer = factory.createSigner(alg, key);}@Overridepublic String sign(String jwtWithoutSignature) {byte[] bytesToSign = jwtWithoutSignature.getBytes(US_ASCII);byte[] signature = signer.sign(bytesToSign);return TextCodec.BASE64URL.encode(signature);} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
構造函數?DefaultJwtSigner?中有個單例簽名工廠 —-?DefaultSignerFactory,讓我們來看看這個工廠都做了些什么
public class DefaultSignerFactory implements SignerFactory {public static final SignerFactory INSTANCE = new DefaultSignerFactory();@Overridepublic Signer createSigner(SignatureAlgorithm alg, Key key) {Assert.notNull(alg, "SignatureAlgorithm cannot be null.");Assert.notNull(key, "Signing Key cannot be null.");switch (alg) {case HS256:case HS384:case HS512:return new MacSigner(alg, key);case RS256:case RS384:case RS512:case PS256:case PS384:case PS512:return new RsaSigner(alg, key);case ES256:case ES384:case ES512:return new EllipticCurveSigner(alg, key);default:throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing.");}} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
原來在工廠中根據不同算法實例化不同的簽名對象?Signer,看來具體簽名算法就是放在?Signer?接口的實現類了,由于我們上面使用?HMacSHA256?算法,關心?MacSigner?類就好了,讓我們看看它是怎么做的:
public class MacSigner extends MacProvider implements Signer {... ...@Overridepublic byte[] sign(byte[] data) {Mac mac = getMacInstance();return mac.doFinal(data);}protected Mac getMacInstance() throws SignatureException {try {return doGetMacInstance();} catch (NoSuchAlgorithmException e) {String msg = "Unable to obtain JCA MAC algorithm '" + alg.getJcaName() + "': " + e.getMessage();throw new SignatureException(msg, e);} catch (InvalidKeyException e) {String msg = "The specified signing key is not a valid " + alg.name() + " key: " + e.getMessage();throw new SignatureException(msg, e);}}protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException {Mac mac = Mac.getInstance(alg.getJcaName());mac.init(key);return mac;} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
是不是很熟悉?也是通過?Mac.getInstance(ALG_NAME)?獲取?Mac?對象后調用其?mac.doFinal(data)?獲取簽名后的 byte 數組,最后轉字符串啦,簽名代碼到這里基本結束了。?
上面提到的類如圖:?
JJWT 的驗證代碼就不講解了,原理是:取出 header 部分和 playload 部分,根據 header 定義的算法再一次簽名,比較這個簽名是否和 JWT 自帶的簽名是否完全相同,驗證是否成功。
總結
以上是生活随笔為你收集整理的Web安全通讯之JWT的Java实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: PHP小知识总结
- 下一篇: C++ primer——vector