与许多 Azure API 一样,Azure OpenAI 服务允许开发人员使用API密钥无密钥(通过Entra ID)进行身份验证。由于尽量避免使用密钥是安全最佳实践,因此我们在本文中将详细介绍如何使开发人员轻松地迁移到无密钥 Azure OpenAI 身份验证。如果您想立即行动,这里还提供了一个新的无密钥部署模板。

密钥的风险

首先让我们来谈谈API密钥的风险,使用密钥很省心,因为设置看起来很简单——你只需要一个端点URL和密钥:

1
2
3
4
5
client = openai.AzureOpenAI(
    api_version="2024-02-15-preview",
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.geten("AZURE_OPENAI_KEY")
)

但是在代码库中使用API密钥可能会导致各种问题。举几个例子:

  • 密钥可能被开发人员不小心放入了源代码控制库中,可能因为某个开发人员将 getenv() 调用替换为一个硬编码的字符串,或者是因为某个开发人员将 .env 文件添加到提交中。
  • 一旦将密钥放入源代码控制中,它们将在内部被公开,并且由于恶意行为者可以访问代码库,因此它们也面临更大的外部泄露风险。
  • 在大型公司中,多个开发人员可能在不知情的情况下使用相同的密钥,使用彼此的资源,并发现由于配额错误而导致服务失败。

我见过所有这些情况的发生,我不希望它们发生在其他开发人员身上。更安全的方法是使用身份验证令牌,因为它们是动态获得的,具有有限的生命周期,并且不会以明文形式存储。

使用 Entra ID 验证 Azure OpenAI

该代码使用 OpenAI Python 包Azure Python SDK 进行 Azure OpenAI 身份验证。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from azure.identity import DefaultAzureCredential, get_bearer_token_provider

azure_credential = DefaultAzureCredential()
token_provider = get_bearer_token_provider(azure_credential,
    "https://cognitiveservices.azure.com/.default")
client = AzureOpenAI(
    api_version="2024-02-15-preview",
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    azure_ad_token_provider=token_provider
)

和基于密钥的代码的区别:

  • 该代码使用 DefaultAzureCredential 与 Azure 进行身份验证,它会遍历许多可能的凭据类型,直到找到一个有效的 Azure 登录身份。
  • 代码根据凭据获取 Azure OpenAI 令牌提供商,并将其设置为 azure_ad_token_provider ,SDK 将在必要时使用该令牌提供商获取访问令牌,甚至为我们刷新令牌。

本地访问 Azure OpenAI

下一步是确保运行代码的人有权访问 Azure OpenAI 服务。默认情况下,即使你自己创建了 Azure OpenAI 服务,你也不会有权限。这是一个安全措施,以确保你不会意外地从本地机器访问生产资源(当你的代码处理数据库写操作时,尤其有帮助)。

要访问 Azure OpenAI 资源,您需要“Cognitive Services OpenAI User”角色(Role ID为“5e0bd9bd-7b93-4f28-af87-19fc36ad61bd”),可以使用 Azure PortalAzure CLIARM/Bicep 进行分配。

使用 Azure CLI 分配角色

首先,设置以下环境变量:

  • PRINCIPAL_ID: 您登录账户的主ID。您可以通过运行 az ad signed-in-user show --query id -o tsv 命令使用 Azure CLI 获取该ID,或者您可以打开 Azure 门户,搜索“Microsoft Entra ID”,选择“用户”选项卡,筛选您的账户,然后复制位于您的电子邮件地址下方的“object ID”。
  • SUBSCRIPTION_ID: 登录账号的订阅ID,可以在 Azure Portal 中 Azure OpenAI 资源的 Overview 页面看到。
  • RESOURCE_GROUP: Azure OpenAI 资源的资源组。

然后使用 Azure CLI 运行以下命令:

1
2
3
4
5
az role assignment create \
        --role "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" \
        --assignee-object-id "$PRINCIPAL_ID" \
        --scope /subscriptions/"$SUBSCRIPTION_ID"/resourceGroups/"$RESOURCE_GROUP" \
        --assignee-principal-type User

使用 ARM/Bicep 分配角色

我们使用 Azure Developer CLI 部署所有示例,该工具依赖于 Bicep 文件来声明基础设施即代码。这使得部署更加可复用,因此对于部署生产应用程序来说是一个很好的方法。

这个 Bicep 资源创建角色,假设设置了 principalId 参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(subscription().id, resourceGroup().id,
             principalId, roleDefinitionId)
  properties: {
    principalId: principalId
    principalType: 'User'
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions',
                                 '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')
  }
}

你也可以看出这个main.bicep的示例是如何使用模块来设置角色的。

使用 Azure Portal 分配角色

如果您无法使用这些自动化方法(首选方法),也可以使用Azure门户创建该角色:

  • 打开 Azure OpenAI 资源
  • 从左侧导航中选择“访问控制(IAM)”
  • 在顶部菜单中选择“添加”
  • 搜索“Cognitive Services User”,并在搜索结果中选择它
  • 选择“Assign access to: User, group, or service principal”
  • 搜索您的电子邮件地址
  • 选择“Review and assign”

从生产环境主机访问 Azure OpenAI

下一步是确保您的部署应用程序也可以使用 DefaultAzureCredential 令牌访问 Azure OpenAI 资源。这需要设置一个托管身份,并为托管身份分配相同的角色。有两种托管身份:系统分配的和用户分配的。所有 Azure 托管平台都支持托管身份。我们将以 App Service 和系统分配的身份为例开始。

为应用服务管理身份

下面是如何在 Bicep 代码中创建一个具有系统分配标识的应用服务:

1
2
3
4
5
6
resource appService 'Microsoft.Web/sites@2022-03-01' = {
  name: name
  location: location
  identity: { type: 'SystemAssigned'}
  ...
}

有关更多详细信息,请参阅《Managed Identity for App Service》一文。

为托管标识分配角色

角色分配过程对于主机和对于用户基本相同,但是主体ID必须设置为托管标识的主体ID,并且主体类型为“ServicePrincipal”。

例如,这个 Bicep 为一个 App Service 系统分配的标识分配角色:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(subscription().id, resourceGroup().id,
             principalId, roleDefinitionId)
  properties: {
    principalId: appService.identity.principalId
    principalType: 'ServicePrincipal'
    roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions',
                                 '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')
  }
}

Azure 容器应用的用户分配身份

也可以使用与上面类似的方法为 Azure 容器应用程序分配系统身份。下面的示例是使用用户分配的身份,以便在 ACA 应用程序被提供之前,我们可以给出对 Azure 容器注册中心的相同身份访问。这就是用户分配身份的优势,能跨多个 Azure 资源重用。

首先,我们在容器应用 Bicep 资源之外创建一个新的身份:

1
2
3
4
resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: '${prefix}-id-aca'
  location: location
}

然后我们把这个标识赋给容器应用 Bicep 资源:

1
2
3
4
5
6
7
8
9
resource app 'Microsoft.App/containerApps@2022-03-01' = {
  name: name
  location: location
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: { '${userIdentity.id}': {} }
  }
  ...
}

当使用用户分配的标识时,我们需要修改对 AzureDefaultCredential 的调用,以告诉它使用哪个标识,因为您可能有多个用户分配的标识(而不仅仅是宿主环境的单个系统分配的标识)。

下面的代码从环境变量中获取身份的ID,并将其指定为 Managed Identity 凭证的 client_id

1
2
default_credential = azure.identity.ManagedIdentityCredential(
    client_id=os.getenv("AZURE_OPENAI_CLIENT_ID"))

在本地 Docker 容器中访问 Azure OpenAI

现在你应该能够在本地开发和生产中访问 Azure OpenAI。除非你正在使用本地 Docker 容器进行开发。默认情况下,Docker 容器没有访问任何本地凭据的方法,所以你会在日志中看到身份验证错误。过去,可以通过使用 volumes 来访问凭据,但自从 Azure 开始加密本地凭据后,如何在本地容器内轻松地进行身份验证就成了悬而未决的问题

我们有一些替代的选择:

  • 在 Docker 容器中使用本地开发密钥。这会带来我们之前讨论过的密钥问题,但在本地非生产部署中使用密钥可以降低使用密钥的风险。
  • 使用 Azure OpenAI 兼容的端点运行本地模型(通过llamafile或ollama),你会看到模型的答案有相当大的差异,所以当你在应用程序的提示工程方面工作时,你不会想这样做。
  • 出于本地开发的目的,可以在容器外运行该应用程序。如果您希望享受本地容器化的好处,仍然可以在 VS Code Dev Container 中运行它,它确实允许进行 Azure 身份验证。这通常是我所采取的方法。

混合做法

正如你所看到的,没有密钥就无法完全直接地对 Azure OpenAI 进行身份验证,这取决于你在本地如何开发以及你在哪里部署。

下面的代码在环境变量中设置密钥时使用密钥,在环境变量中设置身份ID时使用用户分配的管理身份,否则使用 DefaultAzureCredential

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from azure.identity import DefaultAzureCredential, ManagedIdentityCredential
from azure.identity import get_bearer_token_provider

client_args = {}
if os.getenv("AZURE_OPENAI_KEY"):
  client_args["api_key"] = os.getenv("AZURE_OPENAI_KEY")
else:
  if client_id := os.getenv("AZURE_OPENAI_CLIENT_ID"):
    # Authenticate using a user-assigned managed identity on Azure
    azure_credential = ManagedIdentityCredential(
      client_id=client_id)
  else:
    # Authenticate using the default Azure credential chain
    default_credential = DefaultAzureCredential()
  client_args["azure_ad_token_provider"] = get_bearer_token_provider(
    azure_credential, "https://cognitiveservices.azure.com/.default")

openai_client = openai.AsyncAzureOpenAI(
  api_version=os.getenv("AZURE_OPENAI_API_VERSION") or "2024-02-15-preview",
  azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
  **client_args,
)

放心使用无密钥身份验证

以下是一些帮助您在 OpenAI 项目中实现无密钥身份验证的示例: