基于OIDC实现istio来源身份验证
序
本文介绍如何生成可以经过istio来源身份验证的jwt token。istio的来源身份验证是通过OpenID connect规范实现的,这里只需要遵循OIDC的小部分规范便可以实现可以通过验证的token。
首先来看一下istio官方文档对来源身份验证的说明:
https://istio.io/zh/docs/concepts/security/#%e6%9d%a5%e6%ba%90%e8%ba%ab%e4%bb%bd%e8%ae%a4%e8%af%81
ISTIO的来源身份验证通过ENVOY完成,看一下envoy官方文档对JWT的说明:
https://www.envoyproxy.io/docs/envoy/latest/configuration/http_filters/jwt_authn_filter
可以知道,istio会对token的signature、audiences、issuer三个属性进行校验,也会对有效期进行检查,而且只支持ES256和RS256两种算法,因此我们需要保证我们的token生成中这三项属性的规范性。
一、前置知识
JWT
JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,简单来说,一个JWT的TOKEN由三部分组成:
- Headers: 头部信息,经过base64编码的JSON字符串
- Payload: 负载信息,经过base64编码的JSON字符串
- Sinature: 签名
最终的结构如下:
headers.payload.sinature
如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
OpenID connect
OpenID Connect 是一套基于 OAuth 2.0 协议的轻量认证级规范,提供通过 API 进行身份交互的框架。较 OAuth 而言, OpenID Connect 方式除了认证请求之外,还标明请求的用户身份。
简单来说,我们需要提供一个符合OIDC规范的认证服务端,它需要提供token生成能力和token校验所使用的公钥。认证服务端保管好一组签名所用的私钥,生成token时选择一个私钥对token进行签名并在token中注入相关信息(比如对应的公钥ID、算法、issuer、audiences、有效期等)。然后认证服务端需要提供一个接口来开放所有的公钥,这样ISTIO才能拿到公钥对token进行校验。
上述都是OIDC规范的一部分,这里并不严格实现OIDC规范,仅仅为了实现istio的来源身份验证。
二、JAVA实现
依赖
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格式化工具
用于对公私钥二进制内容的加解密,可以自行选择其他方式。
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格式化,方便后续操作。
jwk.toJson()返回的内容是一个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"
}
这里的公钥是经过OIDC规范特殊格式化的,不是base64。
生成token
注意issuer、keyId、clientId、subject等值的统一,要与下一步的开放接口一致,注意有效期可以自己定义,也可以向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规范)
最终该接口的返回值如下所示,可以自行设计接口,该接口的访问地址用于配置在istio中。
{
"keys": [{
"kty": "RSA",
"kid": "def_key_id_oidc",
"alg": "RS256",
"n": "itEiXnQl10vhzYKMc5YXkzOovq2Z_jqSkVWbzqKKJx9Cfxg2VHk8h7eA8PD5xVXCydV_nCu1thDidnh_iWyPQAOHmUrs26txLfVpoyYV2tzYd988eCugnEZAGXx4tXljvpeLOdDsAbtrm-HIyeJ5UE7egx7vmI1EJacqlM1JAZu4jEx99lW7P4ePfqcuytYnAWV1qL3FYKBtDs3Y3Whl4_gFsLErcqhRTIs8mrvhoOCrBYDyJ8nX-59oliaOGIKmPbyYPfQ5beJ-zwjAcn5Z6plZqJ3GtbpNyD6s5GO3WcwqttuCIGpwFdMyfuJl_QYH8sFlufsdyeSKHs_ncbcmOw",
"e": "AQAB"
}]
}
这里提供一个最简单的实现,推荐自行根据需求开发。
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>
实体类
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 = "生成公私钥对时的公钥JSON";
List<VerificationKey> keyList = new ArrayList<VerificationKey>();
VerificationKey key = JSON.parseObject(pubKeyJson, VerificationKey.class);
keyList.add(key);
keys.setKeys(keyList);
return keys;
}
}
openid-configuration接口(可选,非强制)
严格的OIDC规范,还需要开放一个接口用于声明所有的接口和相关的约束,包括jwksUri。该接口的访问路径是issuer/.well-known/openid-configuration,这里不再详细介绍,有兴趣的可以自行研究,或者等待后续的文章。
可以参考一下谷歌的OIDC,比如谷歌的issuer是https://accounts.google.com,则对应的接口是https://accounts.google.com/.well-known/openid-configuration,当然里面有许多属性可能是谷歌特有的,详细可以参考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中为想要执行来源身份验证服务配置一个policy,issuer与生成token时一致,jwksUri就是用于开放公钥的接口地址。如果不提供jwksUri,那么就会使用issuer/.well-known/openid-configuration来访问OIDC的声明接口,来找到jwks_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
个人能力有限,如有错误,欢迎指正!
有兴趣的可以研究一下一些开源的实现OIDC规范的框架的源码,可以有更加深入的了解。
- openid-connect-spring-java
- keycloak
- cas