访问 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。
将 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。
下面是 HDInsight OAuth 客户端实用工具库依赖项的示例代码片段,请将
dependency
部分添加到 pom.xml。简单 JAR,仅包含用于提取 MSI 访问令牌的方便的 Java 实用工具类。
<dependency> <groupId>com.microsoft.azure.hdinsight</groupId> <artifactId>hdi-oauth-token-utils</artifactId> <version>1.0.0</version> </dependency>
与可传递依赖 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>