序
本文介紹如何生成可以經(jīng)過istio來源身份驗(yàn)證的jwt token。istio的來源身份驗(yàn)證是通過OpenID connect規(guī)范實(shí)現(xiàn)的擅笔,這里只需要遵循OIDC的小部分規(guī)范便可以實(shí)現(xiàn)可以通過驗(yàn)證的token。
首先來看一下istio官方文檔對來源身份驗(yàn)證的說明:
ISTIO的來源身份驗(yàn)證通過ENVOY完成,看一下envoy官方文檔對JWT的說明:
可以知道彤路,istio會對token的signature蜻拨、audiences、issuer三個(gè)屬性進(jìn)行校驗(yàn)躯保,也會對有效期進(jìn)行檢查,而且只支持ES256和RS256兩種算法澎语,因此我們需要保證我們的token生成中這三項(xiàng)屬性的規(guī)范性途事。
一验懊、前置知識
JWT
JSON Web Token(縮寫 JWT)是目前最流行的跨域認(rèn)證解決方案,簡單來說尸变,一個(gè)JWT的TOKEN由三部分組成:
- Headers: 頭部信息鲁森,經(jīng)過base64編碼的JSON字符串
- Payload: 負(fù)載信息,經(jīng)過base64編碼的JSON字符串
- Sinature: 簽名
最終的結(jié)構(gòu)如下:
headers.payload.sinature
如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
OpenID connect
OpenID Connect 是一套基于 OAuth 2.0 協(xié)議的輕量認(rèn)證級規(guī)范振惰,提供通過 API 進(jìn)行身份交互的框架歌溉。較 OAuth 而言, OpenID Connect 方式除了認(rèn)證請求之外骑晶,還標(biāo)明請求的用戶身份痛垛。
簡單來說,我們需要提供一個(gè)符合OIDC規(guī)范的認(rèn)證服務(wù)端桶蛔,它需要提供token生成能力和token校驗(yàn)所使用的公鑰匙头。認(rèn)證服務(wù)端保管好一組簽名所用的私鑰,生成token時(shí)選擇一個(gè)私鑰對token進(jìn)行簽名并在token中注入相關(guān)信息(比如對應(yīng)的公鑰ID仔雷、算法蹂析、issuer、audiences碟婆、有效期等)电抚。然后認(rèn)證服務(wù)端需要提供一個(gè)接口來開放所有的公鑰,這樣ISTIO才能拿到公鑰對token進(jìn)行校驗(yàn)竖共。
上述都是OIDC規(guī)范的一部分蝙叛,這里并不嚴(yán)格實(shí)現(xiàn)OIDC規(guī)范,僅僅為了實(shí)現(xiàn)istio的來源身份驗(yàn)證公给。
二借帘、JAVA實(shí)現(xiàn)
依賴
maven
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.6.5</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
base64格式化工具
用于對公私鑰二進(jìn)制內(nèi)容的加解密,可以自行選擇其他方式淌铐。
private static String decodeBase64(String src) {
return new String(Base64.getDecoder().decode(src));
}
private static byte[] decodeBase64ToBytes(String src) {
return Base64.getDecoder().decode(src);
}
private static String encodeBase64(byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
生成簽名密鑰對
這里使用RS256算法肺然,注意管理好keyId,這里我把公私鑰都使用base64格式化腿准,方便后續(xù)操作际起。
jwk.toJson()返回的內(nèi)容是一個(gè)JSON,我們需要記錄一下公鑰的JSON用于開放給istio释涛。
public void generateKey() throws Exception {
String keyId = "def_test";
RsaJsonWebKey jwk = RsaJwkGenerator.generateJwk(2048);
jwk.setKeyId(keyId);
jwk.setAlgorithm(AlgorithmIdentifiers.RSA_USING_SHA256);
System.out.println(encodeBase64(jwk.getRsaPublicKey().getEncoded()));
System.out.println(encodeBase64(jwk.getRsaPrivateKey().getEncoded()));
String publicKey = jwk.toJson(RsaJsonWebKey.OutputControlLevel.PUBLIC_ONLY);
String privateKey = jwk.toJson(RsaJsonWebKey.OutputControlLevel.INCLUDE_PRIVATE);
System.out.println("publicKey: " + publicKey);
System.out.println("privateKey: " + privateKey);
}
公鑰JSON示例:
{
"kty": "RSA",
"kid": "def_key_id_oidc",
"alg": "RS256",
"n": "itEiXnQl10vhzYKMc5YXkzOovq2Z_jqSkVWbzqKKJx9Cfxg2VHk8h7eA8PD5xVXCydV_nCu1thDidnh_iWyPQAOHmUrs26txLfVpoyYV2tzYd988eCugnEZAGXx4tXljvpeLOdDsAbtrm-HIyeJ5UE7egx7vmI1EJacqlM1JAZu4jEx99lW7P4ePfqcuytYnAWV1qL3FYKBtDs3Y3Whl4_gFsLErcqhRTIs8mrvhoOCrBYDyJ8nX-59oliaOGIKmPbyYPfQ5beJ-zwjAcn5Z6plZqJ3GtbpNyD6s5GO3WcwqttuCIGpwFdMyfuJl_QYH8sFlufsdyeSKHs_ncbcmOw",
"e": "AQAB"
}
這里的公鑰是經(jīng)過OIDC規(guī)范特殊格式化的加叁,不是base64倦沧。
生成token
注意issuer唇撬、keyId、clientId展融、subject等值的統(tǒng)一窖认,要與下一步的開放接口一致,注意有效期可以自己定義,也可以向token中注入自定義的信息扑浸。
public static String createToken(String issuer,
String keyId,
String clientId,
String subject,
String secret,
Map<String, Object> headers,
Map<String, String> payload) {
JWTCreator.Builder builder = JWT.create();
builder.withIssuer(issuer);
builder.withKeyId(keyId);
builder.withSubject(subject);
builder.withAudience(clientId);
Calendar calendar = Calendar.getInstance();
builder.withIssuedAt(calendar.getTime());
//有效期 自行配置
calendar.add(Calendar.HOUR, 24);
builder.withExpiresAt(calendar.getTime());
if (null != headers && headers.size() > 0) {
builder.withHeader(headers);
}
if (null != payload && payload.size() > 0) {
for (Map.Entry<String, String> entry : payload.entrySet()) {
builder.withClaim(entry.getKey(), entry.getValue());
}
}
Algorithm algorithm = Algorithm.HMAC256(decodeBase64ToBytes(secret));
return builder.sign(algorithm);
}
開放接口 jwksUri
使用rest開放公鑰(OIDC規(guī)范)
最終該接口的返回值如下所示烧给,可以自行設(shè)計(jì)接口,該接口的訪問地址用于配置在istio中喝噪。
{
"keys": [{
"kty": "RSA",
"kid": "def_key_id_oidc",
"alg": "RS256",
"n": "itEiXnQl10vhzYKMc5YXkzOovq2Z_jqSkVWbzqKKJx9Cfxg2VHk8h7eA8PD5xVXCydV_nCu1thDidnh_iWyPQAOHmUrs26txLfVpoyYV2tzYd988eCugnEZAGXx4tXljvpeLOdDsAbtrm-HIyeJ5UE7egx7vmI1EJacqlM1JAZu4jEx99lW7P4ePfqcuytYnAWV1qL3FYKBtDs3Y3Whl4_gFsLErcqhRTIs8mrvhoOCrBYDyJ8nX-59oliaOGIKmPbyYPfQ5beJ-zwjAcn5Z6plZqJ3GtbpNyD6s5GO3WcwqttuCIGpwFdMyfuJl_QYH8sFlufsdyeSKHs_ncbcmOw",
"e": "AQAB"
}]
}
這里提供一個(gè)最簡單的實(shí)現(xiàn)础嫡,推薦自行根據(jù)需求開發(fā)。
maven依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>
實(shí)體類
public class VerificationKeys {
public VerificationKeys() {
}
public VerificationKeys(List<VerificationKey> keys) { this.keys = keys; }
private List<VerificationKey> keys;
public List<VerificationKey> getKeys() { return keys; }
public void setKeys(List<VerificationKey> keys) { this.keys = keys; }
}
public class VerificationKey {
/**
* 公鑰ID
*/
private String kid;
/**
* 公鑰算法類型
*/
private String kty;
/**
* 公鑰算法
*/
private String alg;
/**
* 公鑰用途: sig 簽名;enc 加密
*/
private String use;
/**
* 公鑰
*/
private String n;
/**
* AQAB
*/
private String e;
public String getKid() { return kid; }
public void setKid(String kid) { this.kid = kid; }
public String getAlg() { return alg; }
public void setAlg(String alg) { this.alg = alg; }
public String getKty() { return kty; }
public void setKty(String kty) { this.kty = kty; }
public String getUse() { return use; }
public void setUse(String use) { this.use = use; }
public String getN() { return n; }
public void setN(String n) { this.n = n; }
public String getE() { return e; }
public void setE(String e) { this.e = e; }
}
接口
@RestController
@RequestMapping("/auth")
public class AuthController {
@RequestMapping(value = "/token_keys", method = {RequestMethod.GET, RequestMethod.POST})
public VerificationKeys tokenKeys() {
VerificationKeys keys = new VerificationKeys();
String pubKeyJson = "生成公私鑰對時(shí)的公鑰JSON";
List<VerificationKey> keyList = new ArrayList<VerificationKey>();
VerificationKey key = JSON.parseObject(pubKeyJson, VerificationKey.class);
keyList.add(key);
keys.setKeys(keyList);
return keys;
}
}
openid-configuration接口(可選酝惧,非強(qiáng)制)
嚴(yán)格的OIDC規(guī)范榴鼎,還需要開放一個(gè)接口用于聲明所有的接口和相關(guān)的約束,包括jwksUri晚唇。該接口的訪問路徑是issuer/.well-known/openid-configuration巫财,這里不再詳細(xì)介紹,有興趣的可以自行研究哩陕,或者等待后續(xù)的文章平项。
可以參考一下谷歌的OIDC,比如谷歌的issuer是https://accounts.google.com悍及,則對應(yīng)的接口是https://accounts.google.com/.well-known/openid-configuration,當(dāng)然里面有許多屬性可能是谷歌特有的闽瓢,詳細(xì)可以參考OIDC的官方文檔。
{
issuer: "https://accounts.google.com",
authorization_endpoint: "https://accounts.google.com/o/oauth2/v2/auth",
token_endpoint: "https://oauth2.googleapis.com/token",
userinfo_endpoint: "https://openidconnect.googleapis.com/v1/userinfo",
revocation_endpoint: "https://oauth2.googleapis.com/revoke",
jwks_uri: "https://www.googleapis.com/oauth2/v3/certs",
response_types_supported: [
"code",
"token",
"id_token",
"code token",
"code id_token",
"token id_token",
"code token id_token",
"none"
],
subject_types_supported: [
"public"
],
id_token_signing_alg_values_supported: [
"RS256"
],
scopes_supported: [
"openid",
"email",
"profile"
],
token_endpoint_auth_methods_supported: [
"client_secret_post",
"client_secret_basic"
],
claims_supported: [
"aud",
"email",
"email_verified",
"exp",
"family_name",
"given_name",
"iat",
"iss",
"locale",
"name",
"picture",
"sub"
],
code_challenge_methods_supported: [
"plain",
"S256"
]
}
三心赶、istio配置
在istio中為想要執(zhí)行來源身份驗(yàn)證服務(wù)配置一個(gè)policy鸳粉,issuer與生成token時(shí)一致,jwksUri就是用于開放公鑰的接口地址园担。如果不提供jwksUri届谈,那么就會使用issuer/.well-known/openid-configuration來訪問OIDC的聲明接口,來找到j(luò)wks_uri并拿到公鑰弯汰。
---
apiVersion: "authentication.istio.io/v1alpha1"
kind: Policy
metadata:
name: policy-test
namespace: default
spec:
targets:
- name: service-test
origins:
- jwt:
issuer: "http://127.0.0.1:8080/"
jwksUri: "http://127.0.0.1:8080/auth/token_keys"
principalBinding: USE_ORIGIN
還支持排除某些路徑艰山,或僅作用于某些路徑(摘自官方文檔)
---
apiVersion: authentication.istio.io/v1alpha1
kind: Policy
metadata:
name: productpage-mTLS-with-JWT
namespace: frod
spec:
targets:
- name: productpage
ports:
- number: 9000
peers:
- mtls:
origins:
- jwt:
issuer: "https://securetoken.google.com"
audiences:
- "productpage"
jwksUri: "https://www.googleapis.com/oauth2/v1/certs"
jwt_headers:
- "x-goog-iap-jwt-assertion"
trigger_rules:
- excluded_paths:
- exact: /health_check
principalBinding: USE_ORIGIN
---
issuer: https://example.com
jwks_uri: https://example.com/.well-known/jwks.json
trigger_rules:
- excluded_paths:
- exact: /health_check
- prefix: /status/
---
issuer: https://example.com
jwks_uri: https://example.com/.well-known/jwks.json
trigger_rules:
- included_paths:
- prefix: /admin
---
issuer: https://example.com
jwks_uri: https://example.com/.well-known/jwks.json
trigger_rules:
- excluded_paths:
- exact: /status/version
included_paths:
- prefix: /status/
詳情參考官方英文文檔 https://istio.io/docs/reference/config/istio.authentication.v1alpha1/
END
個(gè)人能力有限,如有錯(cuò)誤咏闪,歡迎指正曙搬!
有興趣的可以研究一下一些開源的實(shí)現(xiàn)OIDC規(guī)范的框架的源碼,可以有更加深入的了解鸽嫂。
- openid-connect-spring-java
- keycloak
- cas