访问 Azure 服务的 MSI 支持

目前,在 Azure HDInsight 非 ESP 群集中,访问 Azure 资源(如 SqlDB、Cosmos DB、EH、KV、Kusto)的用户作业要么使用用户名和密码,要么使用 MSI 证书密钥。 这不符合 Azure 安全准则。

本文介绍用于提取非 ESP 群集中的 OAuth 令牌的 HDInsight 接口和代码详细信息。

先决条件

  • 此功能在最新的 HDInsight-5.1、5.0 和 4.0 版本中提供。 请确保重新创建或安装了此群集版本。
  • HDInsight 群集必须具有 ADL-Gen2 存储作为主存储,这样就可以为此存储启用基于 MSI 的访问。 这一相同的 MSI 用于所有作业资源访问。 确保为此 MSI 授予所需的 IAM 权限以访问 Azure 资源。
  • IMDS 终结点不适用于 HDI 工作器节点,只能使用此 HDInsight 实用工具提取访问令牌。

提供了两个 Java 客户端实现来提取访问令牌。

  • 选项 1:用于提取访问令牌的 HDInsight 实用工具和 API 用法。
  • 选项 2:用于提取访问令牌的 HDInsight 实用工具、TokenCredential 实现。

注意

默认情况下,范围为“.default”。 我们将在未来提供实用工具 API 中的机制来传递用户提供的范围参数。

如何从 Maven Central 下载实用工具 jar

按照以下步骤从 Maven Central 下载客户端 JAR。

直接从 Maven Central 下载 Maven 内部版本中的 JAR。

  1. 将 maven central 添加为存储库之一以解析 maven 依赖项,如果已添加,则忽略。

    将以下代码片段添加到 pom.xml 文件的 repositories 部分:

    <repository>
        <id>central</id>
        <url>https://repo.maven.apache.org/maven2/</url>
        <releases>
            <enabled>true</enabled>
        </releases>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
    

    Maven 中心提供了两种类型的客户端 JAR。

  2. 下面是 HDInsight OAuth 客户端实用工具库依赖项的示例代码片段,请将 dependency 部分添加到 pom.xml。

    1. 简单 JAR,仅包含用于提取 MSI 访问令牌的方便的 Java 实用工具类。

       <dependency>
       <groupId>com.microsoft.azure.hdinsight</groupId>
       <artifactId>hdi-oauth-token-utils</artifactId>
       <version>1.0.0</version>
       </dependency>
      
    2. 与可传递依赖 JAR 捆绑的遮蔽实用工具 JAR。

      <dependency>
      <groupId>com.microsoft.azure.hdinsight</groupId>
      <artifactId>hdi-oauth-token-utils-shaded</artifactId>
      <version>1.0.2</version>
      </dependency>
      

重要

确保以下项位于类路径中。

  • Hadoop 的 core-site.xml
  • 来自此群集位置 /usr/hdp/<hdi-version>/hadoop/client/* 的所有客户端 jar
  • azure-core-1.49.0.jar, okhttp3-4.9.3 及其可传递的依赖 jar。

访问令牌的结构

访问令牌结构如下所示。

package com.azure.core.credential;
import java.time.OffsetDateTime;

/** Represents an immutable access token with a token string and an expiration time 
* in date format. By default, 24hrs is the expiration time out.
*/
public class AccessToken {

  public String getToken();

  public OffsetDateTime getExpiresAt();
}

选项 1 - 用于提取访问令牌的 HDInsight 实用工具和 API 用法

实现了一个方便的 Java 实用工具类,通过提供目标资源 URI(可以是 EH、KV、Kusto、SqlDB、Cosmos DB)来提取 MSI 访问令牌。

如何使用 API

若要提取该令牌,可以在作业应用程序代码中调用 API。

import com.microsoft.azure.hdinsight.oauthtoken.utils.HdiIdentityTokenServiceUtils;
import com.azure.core.credential.AccessToken;

// uri can be EH, Kusto 
// By default, the Scope is ".default". 
// We will provide a mechanism to take user supplied scope, in future.
String msiResourceUri = https://vault.azure.cn/;
HdiIdentityTokenServiceUtils tokenUtils = new HdiIdentityTokenServiceUtils();
AccessToken token = tokenUtils.getAccessToken(msiResourceUri);

选项 2 - 用于提取访问令牌的 HDInsight 实用工具、TokenCredential 实现

提供了 HdiIdentityTokenCredential 功能 java 类,这是 com.azure.core.credential.TokenCredential 接口的标准实现。

注意

HdiIdentityTokenCredential 类可与各种 Azure SDK 客户端库配合使用,在不进行手动访问令牌管理的情况下对请求进行身份验证并访问 Azure 服务。

示例

以下是 HDInsight oauth 实用工具示例,它们可用于在作业应用程序中获取给定目标资源 URI 的访问令牌:

如果客户端是 Key Vault

对于 Azure Key Vault,SecretClient 实例使用 TokenCredential 对访问令牌进行身份验证和提取:

import com.azure.core.credential.TokenCredential;
import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import com.microsoft.azure.hdinsight.oauthtoken.credential.HdiIdentityTokenCredential;

// Replace <resource-uri> with your Key Vault URI.
TokenCredential hdiTokenCredential = new HdiIdentityTokenCredential("<resource-uri>");

// Create a SecretClient to call the service.
SecretClient secretClient = new SecretClientBuilder()
              .vaultUrl("<your-key-vault-url>") // Replace with your Key Vault URL.
              .credential(hdiTokenCredential) // Add HDI identity token credential.
              .buildClient();

// Retrieve a secret from the Key Vault.
// Replace with your secret name.
KeyVaultSecret secret = secretClient.getSecret("<your-secret-name>");

如果客户端是事件中心

Azure 事件中心的示例,它不直接提取访问令牌。 它使用 TokenCredential 进行身份验证,此凭据会处理访问令牌的提取。

import com.azure.messaging.eventhubs.EventHubClientBuilder;
import com.azure.messaging.eventhubs.EventHubProducerClient;
import com.azure.core.credential.TokenCredential;
import com.microsoft.azure.hdinsight.oauthtoken.credential.HdiIdentityTokenCredential;
HdiIdentityTokenCredential hdiTokenCredential = new HdiIdentityTokenCredential("https://eventhubs.chinacloudapi.cn");
// Create a producer client
EventHubProducerClient producer = new EventHubClientBuilder()
    .credential("<fully-qualified-namespace>", "<event-hub-name>", hdiTokenCredential)
            .buildProducerClient();

// Use the producer client ....

如果客户端是 MySql 数据库

Azure Sql 数据库的示例,它不直接提取访问令牌。

使用访问令牌回调进行连接:以下示例演示如何实现和设置 accessToken 回调

    package com.microsoft.azure.hdinsight.oauthtoken;

    import com.azure.core.credential.AccessToken;
    import com.microsoft.azure.hdinsight.oauthtoken.utils.HdiIdentityTokenServiceUtils;
    import com.microsoft.sqlserver.jdbc.SQLServerAccessTokenCallback;
    import com.microsoft.sqlserver.jdbc.SqlAuthenticationToken;

    public class HdiSQLAccessTokenCallback implements SQLServerAccessTokenCallback {

        @Override
        public SqlAuthenticationToken getAccessToken(String spn, String stsurl) {
            try {
    		    HdiIdentityTokenServiceUtils provider = new HdiIdentityTokenServiceUtils();
                AccessToken token = provider.getAccessToken("https://database.chinacloudapi.cn/";);
                return new SqlAuthenticationToken(token.getToken(), token.getExpiresAt().getTime());
            } catch (Exception e) {
                // handle exception... 
                return null;
            }
        }
    }

    package com.microsoft.azure.hdinsight.oauthtoken;

    import java.sql.DriverManager;

    public class HdiTokenClassBasedConnectionWithDriver {

        public static void main(String[] args) throws Exception {

    		// Below is the sample code to use hdi sql callback.
    		// Replaces <dbserver> with your server name and replaces <dbname> with your db name.
    		String connectionUrl = "jdbc:sqlserver://<dbserver>.database.chinacloudapi.cn;"
    				+ "database=<dbname>;"
    				+ "accessTokenCallbackClass=com.microsoft.azure.hdinsight.oauthtoken.HdiSQLAccessTokenCallback;"
    				+ "encrypt=true;"
    				+ "trustServerCertificate=false;"
    				+ "loginTimeout=30;";

    		DriverManager.getConnection(connectionUrl);

    	}

    }

    package com.microsoft.azure.hdinsight.oauthtoken;

    import com.microsoft.azure.hdinsight.oauthtoken.HdiSQLAccessTokenCallback;
    import com.microsoft.sqlserver.jdbc.SQLServerDataSource;
    import java.sql.Connection;

    public class HdiTokenClassBasedConnectionWithDS {

        public static void main(String[] args) throws Exception {

    		HdiSQLAccessTokenCallback callback = new HdiSQLAccessTokenCallback();

            SQLServerDataSource ds = new SQLServerDataSource();
            ds.setServerName("<db-server>"); // Replaces <db-server> with your server name.
            ds.setDatabaseName("<dbname>"); // Replace <dbname> with your database name.
            ds.setAccessTokenCallback(callback);

            ds.getConnection();
    	}
    }

如果客户端是 Kusto

Azure Sql 数据库的示例,它不直接提取访问令牌。

使用 tokenproviderCallback 进行连接:

以下示例演示 accessToken 回调提供程序,

public void createConnection () {

    final String clusterUrl = "https://xyz.chinaeast.kusto.chinacloudapi.cn";

    ConnectionStringBuilder conStrBuilder = ConnectionStringBuilder.createWithAadTokenProviderAuthentication(clusterUrl, new Callable<String>() {

      public String call() throws Exception {

        // Call HDI util class with scope. This returns the AT and from that get token string and return.
        // AccessToken contains expiry time and user can cache the token once acquired and call for a new one
        // if it is about to expire (Say, <= 30mins for expiry). 
        HdiIdentityTokenServiceUtils hdiUtil = new HdiIdentityTokenServiceUtils();

        AccessToken token = hdiUtil.getAccessToken(clusterUrl);

        return token.getToken();

      }

    });
  }

使用预提取的访问令牌进行连接:

显式提取 accesstoken 并将其作为选项传递。

String targetResourceUri = "https://<my-kusto-cluster>";
HdiIdentityTokenServiceUtils tokenUtils = new HdiIdentityTokenServiceUtils();
AccessToken token = tokenUtils.getAccessToken(targetResourceUri);

df.write
  .format("com.microsoft.kusto.spark.datasource")
  .option(KustoSinkOptions.KUSTO_CLUSTER, "MyCluster")
  .option(KustoSinkOptions.KUSTO_DATABASE, "MyDatabase")
  .option(KustoSinkOptions.KUSTO_TABLE, "MyTable")
  .option(KustoSinkOptions.KUSTO_ACCESS_TOKEN, token.getToken())
  .option(KustoOptions., "MyTable")
  .mode(SaveMode.Append)
  .save()

注意

HdiIdentityTokenCredential 类可与各种 Azure SDK 客户端库结合使用,对请求进行身份验证并访问 Azure 服务,而无需手动管理访问令牌。

故障排除

HdiIdentityTokenCredential 实用工具集成到 Spark 作业中,但在运行时(作业执行)期间访问令牌时遇到以下异常。

User class threw exception: java.lang.NoSuchFieldError: Companion
at okhttp3.internal.Util.<clinit>(Util.kt:70)
at okhttp3.internal.concurrent.TaskRunner.<clinit>(TaskRunner.kt:309)
at okhttp3.ConnectionPool.<init>(ConnectionPool.kt:41)
at okhttp3.ConnectionPool.<init>(ConnectionPool.kt:47)
at okhttp3.OkHttpClient$Builder.<init>(OkHttpClient.kt:471)
at com.microsoft.azure.hdinsight.oauthtoken.utils.HdiIdentityTokenServiceUtils.getAccessToken(HdiIdentityTokenServiceUtils.java:142)
at com.microsoft.azure.hdinsight.oauthtoken.credential.HdiIdentityTokenCredential.getTokenSync(HdiIdentityTokenCredential.java:83)

答案:

下面是 hdi-oauth-util 库的 maven 依赖关系树。 用户需要确保这些 jar 在运行时(在作业容器中)可用。

[INFO] +- com.azure:azure-core:jar:1.49.0:compile
[INFO] |  +- com.azure:azure-json:jar:1.1.0:compile
[INFO] |  +- com.azure:azure-xml:jar:1.0.0:compile
[INFO] |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.5:compile
[INFO] |  +- com.fasterxml.jackson.core:jackson-core:jar:2.13.5:compile
[INFO] |  +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.13.5:compile
[INFO] |  \- io.projectreactor:reactor-core:jar:3.4.36:compile
[INFO] |     \- org.reactivestreams:reactive-streams:jar:1.0.4:compile
[INFO] \- com.squareup.okhttp3:okhttp:jar:4.9.3:compile
[INFO]    +- com.squareup.okio:okio:jar:2.8.0:compile
[INFO]    |  \- org.jetbrains.kotlin:kotlin-stdlib-common:jar:1.4.0:compile
[INFO]    \- org.jetbrains.kotlin:kotlin-stdlib:jar:1.4.10:compile

生成 spark uber jar 时,用户需要确保这些 jar 已着色并包含在 uber jar 中。 可以引用以下插件。

    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>${maven.plugin.shade.version}</version>
    <configuration>
    <createDependencyReducedPom>false</createDependencyReducedPom>
    <relocations>
    <relocation>
    <pattern>okio</pattern>
    <shadedPattern>com.shaded.okio</shadedPattern>
    </relocation>
    <relocation>
    <pattern>okhttp</pattern>
    <shadedPattern>com.shaded.okhttp</shadedPattern>
    </relocation>
    <relocation>
    <pattern>okhttp3</pattern>
    <shadedPattern>com.shaded.okhttp3</shadedPattern>
    </relocation>
    <relocation>
    <pattern>kotlin</pattern>
    <shadedPattern>com.shaded.kotlin</shadedPattern>
    </relocation>
    <relocation>
    <pattern>com.fasterxml.jackson</pattern>
    <shadedPattern>com.shaded.com.fasterxml.jackson</shadedPattern>
    </relocation>
    <relocation>
    <pattern>com.azure</pattern>
    <shadedPattern>com.shaded.com.azure</shadedPattern>
    </relocation>