將應用程式移轉至使用適用於 Kafka Azure 事件中樞 的無密碼連線
本文說明如何從傳統驗證方法移轉至更安全、無密碼的連線,以及 Kafka Azure 事件中樞。
必須驗證對 Kafka Azure 事件中樞 的應用程式要求。 適用於 Kafka 的 Azure 事件中樞 提供不同方式讓應用程式安全地連線。 其中一種方式是使用 連接字串。 不過,您應該盡可能排定應用程式中無密碼連線的優先順序。
Spring Cloud Azure 4.3.0 之後支持無密碼連線。 本文是從 Spring Cloud Stream Kafka 應用程式移除認證的移轉指南。
比較驗證選項
當應用程式向 Kafka 的 Azure 事件中樞 進行驗證時,它會提供連線事件中樞命名空間的授權實體。 Apache Kafka 通訊協定提供多個簡單驗證和安全性層 (SASL) 機制來進行驗證。 根據 SASL 機制,您可以使用兩個驗證選項來授權存取您的安全資源:Microsoft Entra 驗證和共用存取簽章 (SAS) 驗證。
Microsoft Entra 驗證
Microsoft Entra 驗證是使用 Microsoft Entra 識別碼中定義的身分識別連線到 Kafka Azure 事件中樞 的機制。 透過 Microsoft Entra 驗證,您可以在集中位置管理服務主體身分識別和其他 Microsoft 服務,以簡化許可權管理。
使用 Microsoft Entra ID 進行驗證可提供下列優點:
- 以統一的方式驗證跨 Azure 服務的使用者。
- 在單一位置管理密碼原則和密碼輪替。
- Microsoft Entra ID 支援的多種驗證形式,這可以消除儲存密碼的需求。
- 客戶可以使用外部 (Microsoft Entra ID) 群組來管理事件中樞許可權。
- 針對連線至 Kafka Azure 事件中樞 的應用程式,支援令牌型驗證。
SAS 驗證
事件中樞也提供共用存取簽章(SAS),以委派存取 Kafka 資源的事件中樞。
雖然可以使用 SAS 連線到 Kafka Azure 事件中樞,但請務必謹慎使用。 您必須勤奮地不要在不安全的位置公開 連接字串。 任何獲得 連接字串 存取權的人都能夠進行驗證。 例如,如果不小心將 連接字串 簽入原始檔控制、透過不安全的電子郵件傳送、貼入錯誤的聊天,或由不應該擁有許可權的人員檢視,惡意使用者可能會存取應用程式。 相反地,使用 OAuth 2.0 令牌型機制來授權存取,可提供優於 SAS 的安全性和易於使用。 請考慮更新您的應用程式以使用無密碼連線。
無密碼連線簡介
透過無密碼連線,您可以連線到 Azure 服務,而不需將任何認證儲存在應用程式程式代碼、其組態檔或環境變數中。
許多 Azure 服務都支援無密碼連線,例如透過 Azure 受控識別。 這些技術提供強固的安全性功能,您可以從 Azure 身分識別用戶端連結庫使用 DefaultAzureCredential 來實作。 在本教學課程中,您將瞭解如何更新現有的應用程式以使用DefaultAzureCredential
,而不是 連接字串 之類的替代方案。
DefaultAzureCredential
支援多個驗證方法,並自動判斷應該在運行時間使用哪些方法。 這種方法可讓您的應用程式在不同的環境中使用不同的驗證方法(本機開發與生產環境),而不實作環境特定的程序代碼。
您可以在 Azure 身分識別連結庫概觀中找到搜尋認證的順序和位置DefaultAzureCredential
。 例如,在本機工作時, DefaultAzureCredential
通常會使用開發人員用來登入Visual Studio的帳戶進行驗證。 當應用程式部署至 Azure 時, DefaultAzureCredential
會自動切換為使用 受控識別。 此轉換不需要變更程序代碼。
若要確保連線是無密碼的,您必須同時考慮本機開發和生產環境。 如果任一位置都需要 連接字串,則應用程式不是無密碼的。
在本機開發環境中,您可以使用適用於 Visual Studio Code 或 IntelliJ 的 Azure CLI、Azure PowerShell、Visual Studio 或 Azure 外掛程式進行驗證。 在此情況下,您可以在應用程式中使用該認證,而不是設定屬性。
當您將應用程式部署至 Azure 主控環境,例如虛擬機時,您可以在該環境中指派受控識別。 然後,您不需要提供認證來連線到 Azure 服務。
注意
受控識別提供安全性身分識別來代表應用程式或服務。 身分識別由 Azure 平台負責管理,因此您不需要佈建或輪替任何密碼。 您可以在概觀檔中深入瞭解受控識別。
移轉現有的應用程式以使用無密碼連線
下列步驟說明如何將現有的應用程式移轉至使用無密碼連線,而不是 SAS 解決方案。
0) 準備工作環境以進行本機開發驗證
首先,使用下列命令來設定一些環境變數。
export AZ_RESOURCE_GROUP=<YOUR_RESOURCE_GROUP>
export AZ_EVENTHUBS_NAMESPACE_NAME=<YOUR_EVENTHUBS_NAMESPACE_NAME>
export AZ_EVENTHUB_NAME=<YOUR_EVENTHUB_NAME>
將佔位元取代為下列值,此值會在整個文章中使用:
<YOUR_RESOURCE_GROUP>
:您將使用之資源組名。<YOUR_EVENTHUBS_NAMESPACE_NAME>
:您將使用的 Azure 事件中樞 命名空間名稱。<YOUR_EVENTHUB_NAME>
:您將使用的事件中樞名稱。
1) 授與 Azure 事件中樞 的許可權
如果您想要使用 Microsoft Entra 驗證在本機執行此範例,請確定您的使用者帳戶已透過適用於 IntelliJ 的 Azure 工具組、Visual Studio Code Azure 帳戶外掛程式或 Azure CLI 進行驗證。 此外,請確定帳戶已獲得足夠的許可權。
在 Azure 入口網站 中,使用主要搜尋列或左側導覽來尋找事件中樞命名空間。
在 [事件中樞概觀] 頁面上,從左側功能表中選取 [訪問控制][IAM ]。
在 [ 訪問控制 (IAM)] 頁面上,選取 [ 角色指派] 索引標籤 。
從頂端功能表選取 [新增 ],然後 從產生的下拉功能表中新增角色指派 。
使用搜尋方塊將結果篩選為所需的角色。 在此範例中,搜尋 Azure 事件中樞 數據傳送者和 Azure 事件中樞 數據接收者,然後選取相符的結果,然後選擇 [下一步]。
在 [指派存取權] 底下,選取 [使用者、群組或服務主體],然後選擇 [選取成員]。
在對話框中,搜尋您的 Microsoft Entra 使用者名稱(通常是您的 user@domain 電子郵件位址),然後選擇對話方塊底部的 [ 選取 ]。
選取 [ 檢閱 + 指派 ] 以移至最終頁面,然後 再次檢閱 + 指派 以完成程式。
如需授與存取角色的詳細資訊,請參閱 使用 Microsoft Entra ID 授權事件中樞資源的存取權。
2) 登入並移轉應用程式程式代碼以使用無密碼連線
針對本機開發,請確定您已使用您在事件中樞上指派角色的相同 Microsoft Entra 帳戶進行驗證。 您可以透過 Azure CLI、Visual Studio、Azure PowerShell 或其他工具進行驗證,例如 IntelliJ。
使用下列命令透過 Azure CLI 登入 Azure:
az login
接下來,使用下列步驟來更新 Spring Kafka 應用程式以使用無密碼連線。 雖然在概念上很類似,但每個架構都會使用不同的實作詳細數據。
在您的專案中,開啟 pom.xml 檔案並新增下列參考:
<dependency> <groupId>com.azure</groupId> <artifactId>azure-identity</artifactId> <version>1.6.0</version> </dependency>
移轉之後,請在您的專案中實 作 AuthenticationCallbackHandler 和 OAuthBearerToken 以進行 OAuth2 驗證,如下列範例所示。
public class KafkaOAuth2AuthenticateCallbackHandler implements AuthenticateCallbackHandler { private static final Duration ACCESS_TOKEN_REQUEST_BLOCK_TIME = Duration.ofSeconds(30); private static final String TOKEN_AUDIENCE_FORMAT = "%s://%s/.default"; private Function<TokenCredential, Mono<OAuthBearerTokenImp>> resolveToken; private final TokenCredential credential = new DefaultAzureCredentialBuilder().build(); @Override public void configure(Map<String, ?> configs, String mechanism, List<AppConfigurationEntry> jaasConfigEntries) { TokenRequestContext request = buildTokenRequestContext(configs); this.resolveToken = tokenCredential -> tokenCredential.getToken(request).map(OAuthBearerTokenImp::new); } private TokenRequestContext buildTokenRequestContext(Map<String, ?> configs) { URI uri = buildEventHubsServerUri(configs); String tokenAudience = buildTokenAudience(uri); TokenRequestContext request = new TokenRequestContext(); request.addScopes(tokenAudience); return request; } @SuppressWarnings("unchecked") private URI buildEventHubsServerUri(Map<String, ?> configs) { String bootstrapServer = Arrays.asList(configs.get(BOOTSTRAP_SERVERS_CONFIG)).get(0).toString(); bootstrapServer = bootstrapServer.replaceAll("\\[|\\]", ""); URI uri = URI.create("https://" + bootstrapServer); return uri; } private String buildTokenAudience(URI uri) { return String.format(TOKEN_AUDIENCE_FORMAT, uri.getScheme(), uri.getHost()); } @Override public void handle(Callback[] callbacks) throws UnsupportedCallbackException { for (Callback callback : callbacks) { if (callback instanceof OAuthBearerTokenCallback) { OAuthBearerTokenCallback oauthCallback = (OAuthBearerTokenCallback) callback; this.resolveToken .apply(credential) .doOnNext(oauthCallback::token) .doOnError(throwable -> oauthCallback.error("invalid_grant", throwable.getMessage(), null)) .block(ACCESS_TOKEN_REQUEST_BLOCK_TIME); } else { throw new UnsupportedCallbackException(callback); } } } @Override public void close() { // NOOP } }
public class OAuthBearerTokenImp implements OAuthBearerToken { private final AccessToken accessToken; private final JWTClaimsSet claims; public OAuthBearerTokenImp(AccessToken accessToken) { this.accessToken = accessToken; try { claims = JWTParser.parse(accessToken.getToken()).getJWTClaimsSet(); } catch (ParseException exception) { throw new SaslAuthenticationException("Unable to parse the access token", exception); } } @Override public String value() { return accessToken.getToken(); } @Override public Long startTimeMs() { return claims.getIssueTime().getTime(); } @Override public long lifetimeMs() { return claims.getExpirationTime().getTime(); } @Override public Set<String> scope() { // Referring to https://docs.microsoft.com/azure/active-directory/develop/access-tokens#payload-claims, the scp // claim is a String, which is presented as a space separated list. return Optional.ofNullable(claims.getClaim("scp")) .map(s -> Arrays.stream(((String) s) .split(" ")) .collect(Collectors.toSet())) .orElse(null); } @Override public String principalName() { return (String) claims.getClaim("upn"); } public boolean isExpired() { return accessToken.isExpired(); } }
當您建立 Kafka 產生者或取用者時,請新增支援 SASL/OAUTHBEARER 機制所需的設定。 下列範例顯示您的程式代碼在移轉前後的外觀。 在這兩個範例中,以事件中
<eventhubs-namespace>
樞命名空間的名稱取代 佔位符。在移轉之前,您的程式代碼看起來應該像下列範例:
Properties properties = new Properties(); properties.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, "<eventhubs-namespace>.servicebus.windows.net:9093"); properties.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL"); properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.put(SaslConfigs.SASL_MECHANISM, "PLAIN"); properties.put(SaslConfigs.SASL_JAAS_CONFIG, String.format("org.apache.kafka.common.security.plain.PlainLoginModule required username=\"$ConnectionString\" password=\"%s\";", connectionString)); return new KafkaProducer<>(properties);
移轉之後,您的程式代碼看起來應該像下列範例。 在此範例中,請將
<path-to-your-KafkaOAuth2AuthenticateCallbackHandler>
佔位元取代為您實KafkaOAuth2AuthenticateCallbackHandler
作 的完整類別名稱。Properties properties = new Properties(); properties.put(CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG, "<eventhubs-namespace>.servicebus.windows.net:9093"); properties.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SASL_SSL"); properties.put(SaslConfigs.SASL_MECHANISM, "OAUTHBEARER"); properties.put(SaslConfigs.SASL_JAAS_CONFIG, "org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required"); properties.put(SaslConfigs.SASL_LOGIN_CALLBACK_HANDLER_CLASS, "<path-to-your-KafkaOAuth2AuthenticateCallbackHandler>"); return new KafkaProducer<>(properties);
在本機執行應用程式
進行這些程式代碼變更之後,請在本機執行您的應用程式。 假設您已登入相容的 IDE 或命令行工具,例如 Azure CLI、Visual Studio 或 IntelliJ,新的組態應該會挑選您的本機認證。 您在 Azure 中指派給本機開發人員使用者的角色,可讓您的應用程式在本機連線到 Azure 服務。
3) 設定 Azure 裝載環境
將應用程式設定為使用無密碼連線並在本機執行之後,相同的程式代碼可以在部署至 Azure 之後向 Azure 服務進行驗證。 例如,部署至已指派受控識別的 Azure Spring Apps 實例的應用程式可以連線到 Kafka 的 Azure 事件中樞。
在本節中,您將執行兩個步驟,讓應用程式以無密碼的方式在 Azure 主控環境中執行:
- 為您的 Azure 裝載環境指派受控識別。
- 將角色指派給受控識別。
注意
Azure 也提供 Service 連線 or,可協助您將主控服務與事件中樞連線。 使用 Service 連線 or 來設定裝載環境,您可以省略將角色指派給受控識別的步驟,因為服務 連線 or 會為您執行此動作。 下一節說明如何以兩種方式設定 Azure 主控環境:一種是透過服務 連線 或另一種方式,直接設定每個裝載環境。
重要
服務 連線 或命令需要 Azure CLI 2.41.0 或更高版本。
為您的 Azure 裝載環境指派受控識別
下列步驟說明如何為各種 Web 主機服務指派系統指派的受控識別。 受控識別可以使用您先前設定的應用程式設定,安全地連線到其他 Azure 服務。
您也可以使用 Azure CLI 在 Azure 主控環境中指派受控識別。
您可以使用 az webapp identity assign 命令,將受控識別指派給 Azure App 服務 實例,如下列範例所示。
export AZURE_MANAGED_IDENTITY_ID=$(az webapp identity assign \
--resource-group $AZ_RESOURCE_GROUP \
--name <app-service-name> \
--query principalId \
--output tsv)
將角色指派給受控識別
接下來,將許可權授與您建立的受控識別,以存取事件中樞命名空間。 您可以將角色指派給受控識別來授與許可權,就像您和本機開發用戶一樣。
如果您使用 Service 連線 or 連線服務,則不需要完成此步驟。 您已為您處理下列必要的群組態:
如果您在建立連線時選取受控識別,則會為您的應用程式建立系統指派的受控識別,並在事件中樞上指派 Azure 事件中樞 數據傳送者和Azure 事件中樞 數據接收者角色。
如果您選擇使用 連接字串,連接字串 會新增為應用程式環境變數。
測試應用程式
進行這些程式代碼變更之後,請在瀏覽器中瀏覽至您裝載的應用程式。 您的應用程式應該能夠順利連線到 Kafka 的 Azure 事件中樞。 請記住,角色指派可能需要幾分鐘的時間,才能透過 Azure 環境傳播。 您的應用程式現在已設定為在本機和生產環境中執行,而開發人員不需要在應用程式本身管理秘密。
下一步
在本教學課程中,您已瞭解如何將應用程式遷移至無密碼連線。
您可以閱讀下列資源,以更深入地探索本文中討論的概念:
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應