OAuth2

基于OIDC实现istio来源身份验证

2019-08-11  本文已影响0人  尘风匿翎

本文介绍如何生成可以经过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的signatureaudiencesissuer三个属性进行校验,也会对有效期进行检查,而且只支持ES256和RS256两种算法,因此我们需要保证我们的token生成中这三项属性的规范性。

一、前置知识

JWT

https://jwt.io/

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,简单来说,一个JWT的TOKEN由三部分组成:

最终的结构如下:

  headers.payload.sinature

如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

OpenID connect

https://openid.net/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规范的框架的源码,可以有更加深入的了解。

上一篇下一篇

猜你喜欢

热点阅读