阿里雲API Gateway在JSON Web Token(JWT)這種結構化令牌的基礎上實現了一套基於使用者體系對使用者的API進行授權訪問的機制,滿足使用者個人化安全設定的需求。
一、基於token的認證
1.1 簡介
很多對外開放的API需要識別要求者的身份,並據此判斷所請求的資源是否可以返回給要求者。token就是一種用於身分識別驗證的機制,基於這種機制,應用不需要在服務端保留使用者的認證資訊或者會話資訊,可實現無狀態、分布式的Web應用授權,為應用的擴充提供了便利。
1.2 流程描述
上圖是API Gateway利用JWT鑒權外掛程式實現認證的整個商務程序時序圖,下面我們用文字來詳細描述圖中標註的步驟:
用戶端向API Gateway發送請求,請求中攜帶token;
API Gateway使用使用者外掛程式中配置的公開金鑰對請求中的token進行驗證,驗證通過後,將請求透傳給後端服務;
後端服務進行處理後返回應答;
API Gateway將後端服務應答返回給用戶端。
在整個過程中,API Gateway利用token認證機制,實現了使用者使用自己的使用者體系對API進行授權的能力。下面我們就要介紹API Gateway實現token認證所使用的結構化令牌Json Web Token(JWT)。
1.3 JWT
1.3.1 簡介
Json Web Token(JWT),是為了在網路應用環境間傳遞聲明而執行的一種基於JSON的開放標準RFC7519。JWT一般可以用作獨立的身分識別驗證令牌,可以包含使用者標識、使用者角色和許可權等資訊,以便於從資原始伺服器擷取資源,也可以增加一些額外的其他商務邏輯所必須的聲明資訊,特別適用於分布式網站的登入情境。
1.3.2 JWT的構成
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
如上面的例子所示,JWT就是一個字串,由三部分構成:
Header(頭部)
Payload(資料)
Signature(簽名)
Header
JWT的頭部承載兩個資訊:
宣告類型,這裡是JWT
聲明加密的演算法
完整的頭部就像下面這樣的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然後將頭部進行Base64編碼(該編碼是可以對稱解碼的),構成了第一部分。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Payload
載荷就是存放有效資訊的地方。定義細節如下:
iss:令牌頒發者。表示該令牌由誰建立,該聲明是一個字串
sub: Subject Identifier,iss提供的終端使用者的標識,在iss範圍內唯一,最長為255個ASCII個字元,區分大小寫
aud:Audience(s),令牌的受眾,分大小寫字串數組
exp:Expiration time,令牌的到期時間戳記。超過此時間的token會作廢, 該聲明是一個整數,是1970年1月1日以來的秒數
iat: 令牌的頒發時間,該聲明是一個整數,是1970年1月1日以來的秒數
jti: 令牌的唯一標識,該聲明的值在令牌頒發者建立的每一個令牌中都是唯一的,為了防止衝突,它通常是一個密碼學隨機值。這個值相當於向結構化令牌中加入了一個攻擊者無法獲得的隨機熵組件,有利於防止令牌猜測攻擊和重放攻擊。
也可以新增使用者系統需要使用的自訂欄位,比如下面的例子添加了name
使用者暱稱:
{
"sub": "1234567890",
"name": "John Doe"
}
然後將其進行Base64編碼,得到Jwt的第二部分:
JTdCJTBBJTIwJTIwJTIyc3ViJTIyJTNBJTIwJTIyMTIzNDU2Nzg5MCUyMiUyQyUwQSUyMCUyMCUyMm5hbWUlMjIlM0ElMjAlMjJKb2huJTIwRG9lJTIyJTBBJTdE
Signature
這個部分需要Base64編碼後的Header和Base64編碼後的Payload使用 .
串連組成的字串,然後通過Header中聲明的加密方式進行加密($secret
表示使用者的私密金鑰),然後就構成了JWT的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, '$secret');
將這三部分用 .
串連成一個完整的字串,就構成了 1.3.2 節最開始的JWT樣本。
1.3.3 授權範圍與時效
API Gateway會認為使用者頒發的token有權利訪問整個分組下的所有綁定JWT外掛程式的API。如果需要更細粒度的許可權管理,還需要後端服務自行解開token進行許可權認證。API Gateway會驗證token中的exp欄位,一旦這個欄位到期了,API Gateway會認為這個token無效而將請求直接打回。到期時間這個值必須設定,並且到期時間一定要小於7天。
1.3.4 JWT的幾個特點
JWT 預設是不加密,不能將秘密資料寫入 JWT。
JWT 不僅可以用於認證,也可以用於交換資訊。有效使用 JWT,可以降低伺服器查詢資料庫的次數。JWT 的最大缺點是,由於伺服器不儲存 session 狀態,因此無法在使用過程中廢止某個 token,或者更改 token 的許可權。也就是說,一旦 JWT 簽發了,在到期之前就會始終有效,除非伺服器部署額外的邏輯。
JWT 本身包含了認證資訊,一旦泄露,任何人都可以獲得該令牌的所有許可權。為了減少盜用,JWT 的有效期間應該設定得比較短。對於一些比較重要的許可權,使用時應該再次對使用者進行認證。
為了減少盜用,JWT 不應該使用 HTTP 協議明碼傳輸,要使用HTTPS 協議傳輸。
二、使用者系統如何應用JWT外掛程式保護API
2.1 產生一對JWK(JSON Web 密鑰)
方法一、線上產生:
使用者可以在這個網站https://mkjwk.org 產生用於token產生與驗證的私密金鑰與公開金鑰, 私密金鑰用於授權服務簽發JWT,公開金鑰配置到JWT外掛程式中用於API Gateway對請求驗簽,目前API Gateway支援的金鑰組的密碼編譯演算法為RSA SHA256,金鑰組的加密的位元為2048。
方法二、本地產生:
本文應用Java樣本說明,其他語言使用者也可以找到相關的工具產生金鑰組。 建立一個Maven專案,加入如下依賴:
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.7.0</version>
</dependency>
使用如下的代碼產生一對RSA密鑰:
RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
rsaJsonWebKey.setKeyId("authServer");
final String publicKeyString = rsaJsonWebKey.toJson(JsonWebKey.OutputControlLevel.PUBLIC_ONLY);
final String privateKeyString = rsaJsonWebKey.toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE);
2.2 使用JWK中的私密金鑰實現頒發token的認證服務
需要使用2.1節中線上產生的 Keypair
JSON字串(三個方框內的第一個)或者本地產生的 privateKeyString
JSON字串作為私密金鑰來頒發token,用於授權可信的使用者訪問受保護的API,具體實現請參考本文第三節的樣本。
向客戶頒發token的形式由使用者根據具體的業務情境決定,可以將頒發token的功能部署到生產環境,配置成普通API後由訪問者通過使用者名稱密碼獲得,也可以直接在本地環境產生token 後,直接拷貝給指定使用者使用。
2.3 將JWK中的公開金鑰配置到JWT外掛程式中
在左側導覽列選擇API管理>外掛程式管理。
在外掛程式管理頁面,單擊右上方的。
在建立外掛程式頁面,外掛程式類型選擇,下面是一個JWT鑒權外掛程式的配置及說明,具體配置說明可參見JWT認證外掛程式。
---
parameter: X-Token # 從指定的參數中擷取JWT, 對應API的參數
parameterLocation: header # API為映射模式時可選, API為透傳模式下必填, 用於指定JWT的讀取位置, 僅支援`query`,`header`
claimParameters: # claims參數轉換, 網關會將jwt claims映射為後端參數
- claimName: aud # claim名稱,支援公用和私人
parameterName: X-Aud # 映射後參數名稱
location: header # 映射後參數位置, 支援`query,header,path,formData`
- claimName: userId # claim名稱,支援公用和私人
parameterName: userId # 映射後參數名稱
location: query # 映射後的參數位置, 支援`query,header,path,formData`
preventJtiReplay: false # 是否開啟針對`jti`的防重放檢查, 預設false
#
# `Json Web Key`的`Public Key`, 即本文2.1節產生的公開金鑰部分
jwk:
kty: RSA
e: AQAB
use: sig
kid: uniq_key
alg: RS256
n: qSVxcknOm0uCq5vGsOmaorPDzHUubBmZZ4UXj-9do7w9X1uKFXAnqfto4TepSNuYU2bA_-tzSLAGBsR-BqvT6w9SjxakeiyQpVmexxnDw5WZwpWenUAcYrfSPEoNU-0hAQwFYgqZwJQMN8ptxkd0170PFauwACOx4Hfr-9FPGy8NCoIO4MfLXzJ3mJ7xqgIZp3NIOGXz-GIAbCf13ii7kSStpYqN3L_zzpvXUAos1FJ9IPXRV84tIZpFVh2lmRh0h8ImK-vI42dwlD_hOIzayL1Xno2R0T-d5AwTSdnep7g-Fwu8-sj4cCRWq3bd61Zs2QOJ8iustH0vSRMYdP5oYQ
2.4 JWT外掛程式綁定API
在外掛程式列表頁找到建立好的JWT鑒權外掛程式,單擊按鈕,在彈出框中添加指定分組和環境下的API到彈出框右側API列表中,單擊,綁定完成。
目前,控制台的API調試功能並沒有支援JWT外掛程式,建議使用者通過Postman或者直接在系統命令列中應用curl
命令測試綁定JWT外掛程式的API。
三、頒發token的認證服務範例程式碼
import java.security.PrivateKey;
import org.jose4j.json.JsonUtil;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.RsaJwkGenerator;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.NumericDate;
import org.jose4j.lang.JoseException;
public class GenerateJwtDemo {
public static void main(String[] args) throws JoseException {
//使用在API Gateway設定的keyId
String keyId = "uniq_key";
//使用本文2.1節產生的Keypare
String privateKeyJson = "{\n"
+ " \"kty\": \"RSA\",\n"
+ " \"d\": "
+
"\"O9MJSOgcjjiVMNJ4jmBAh0mRHF_TlaVva70Imghtlgwxl8BLfcf1S8ueN1PD7xV6Cnq8YenSKsfiNOhC6yZ_fjW1syn5raWfj68eR7cjHWjLOvKjwVY33GBPNOvspNhVAFzeqfWneRTBbga53Agb6jjN0SUcZdJgnelzz5JNdOGaLzhacjH6YPJKpbuzCQYPkWtoZHDqWTzCSb4mJ3n0NRTsWy7Pm8LwG_Fd3pACl7JIY38IanPQDLoighFfo-Lriv5z3IdlhwbPnx0tk9sBwQBTRdZ8JkqqYkxUiB06phwr7mAnKEpQJ6HvhZBQ1cCnYZ_nIlrX9-I7qomrlE1UoQ\",\n"
+ " \"e\": \"AQAB\",\n"
+ " \"kid\": \"myJwtKey\",\n"
+ " \"alg\": \"RS256\",\n"
+ " \"n\": \"vCuB8MgwPZfziMSytEbBoOEwxsG7XI3MaVMoocziP4SjzU4IuWuE_DodbOHQwb_thUru57_Efe"
+
"--sfATHEa0Odv5ny3QbByqsvjyeHk6ZE4mSAV9BsHYa6GWAgEZtnDceeeDc0y76utXK2XHhC1Pysi2KG8KAzqDa099Yh7s31AyoueoMnrYTmWfEyDsQL_OAIiwgXakkS5U8QyXmWicCwXntDzkIMh8MjfPskesyli0XQD1AmCXVV3h2Opm1Amx0ggSOOiINUR5YRD6mKo49_cN-nrJWjtwSouqDdxHYP-4c7epuTcdS6kQHiQERBd1ejdpAxV4c0t0FHF7MOy9kw\"\n"
+ "}";
JwtClaims claims = new JwtClaims();
claims.setGeneratedJwtId();
claims.setIssuedAtToNow();
//到期時間一定要設定,並且小於7天
NumericDate date = NumericDate.now();
date.addSeconds(120*60);
claims.setExpirationTime(date);
claims.setNotBeforeMinutesInThePast(1);
claims.setSubject("YOUR_SUBJECT");
claims.setAudience("YOUR_AUDIENCE");
//添加自訂參數,所有值請都使用String類型
claims.setClaim("userId", "1213234");
claims.setClaim("email", "userEm***@youapp.com");
JsonWebSignature jws = new JsonWebSignature();
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
//必須設定
jws.setKeyIdHeaderValue(keyId);
jws.setPayload(claims.toJson());
PrivateKey privateKey = new RsaJsonWebKey(JsonUtil.parseJson(privateKeyJson)).getPrivateKey();
jws.setKey(privateKey);
String jwtResult = jws.getCompactSerialization();
System.out.println("Generate Json Web token , result is " + jwtResult);
}
}
上述樣本中有以下幾個地方需要重點關註:
keyId需要三個環節都一致,且全域唯一:
privateKeyJson 使用2.1節中線上產生的
Keypair
JSON字串(三個方框內的第一個)或者本地產生的privateKeyString
JSON字串。到期時間一定要設定,並且小於7天。
添加自訂參數,請都使用String類型的值。
四、API Gateway錯誤應答列表
Status | Code | Message | Description |
400 | I400JR | JWT required | 未找到JWT參數 |
403 | S403JI | Claim jti is required when preventJtiReplay:true | 當在JWT授權外掛程式中配置了防重放功能時,請求未提供有效jti |
403 | S403JU | Claim jti in JWT is used | 當在JWT授權外掛程式中配置了防重放功能時,請求提供的jti已被使用 |
403 | A403JT | Invalid JWT: ${Reason} | 請求中提供的JWT非法 |
400 | I400JD | JWT Deserialize Failed: ${Token} | 請求中提供的JWT解析失 |
403 | A403JK | No matching JWK, kid:${kid} not found | 請求JWT中的kid沒有匹配的JWK |
403 | A403JE | JWT is expired at ${Date} | 請求中提供的JWT已到期 |
400 | I400JP | Invalid JWT plugin config: ${JWT} | JWT授權外掛程式配置錯誤 |
當出現非預期應答碼時,請檢查HTTP應答中的X-Ca-Error-Code
頭中擷取ErrorCode
,從X-Ca-Error-Message
頭中擷取ErrorMessage
當出現A403JT
或I400JD
錯誤碼時,可訪問jwt.io
網站來檢查自己的Token
合法性與格式。