在本地开发期间使用服务主体向 Azure 服务验证 Python 应用的身份

在创建云应用程序时,开发人员需要在其本地工作站上调试和测试应用程序。 在本地开发期间,当应用程序在开发人员的工作站上运行时,它仍然必须向应用使用的任何 Azure 服务进行身份验证。 本文介绍如何设置要在本地开发期间使用的专用应用程序服务主体对象。

此关系图显示在本地开发人员环境中运行的应用如何从 .env 文件获取应用程序服务主体,然后使用该标识连接到 Azure 资源。

使用用于本地开发的专用应用程序服务主体可以在应用开发期间遵循最低特权原则。 由于权限的范围严格限定为开发期间应用所需的权限,因此可以防止应用代码意外访问仅供其他应用使用的 Azure 资源。 在将应用转移到生产环境时,这还可以防止出现 bug,因为该应用在开发环境中特权过高。

在 Azure 中注册应用时,将为该应用设置应用程序服务主体。 为本地开发注册应用时,建议:

  • 为每个处理该应用的开发人员单独创建应用注册。 这会为每个开发人员单独创建要在本地开发期间使用的应用程序服务主体,并避免开发人员共享单个应用程序服务主体的凭据的需要。
  • 为每个应用单独创建应用注册。 这会将应用的权限范围限定为该应用所需的权限。

在本地开发期间,将使用应用程序服务主体的标识设置环境变量。 Azure SDK for Python 会读取这些环境变量,并使用此类信息向所需的 Azure 资源对应用进行身份验证。

1 - 在 Azure 中注册应用程序

应用程序服务主体对象是使用 Azure 中的应用注册创建的。 可以使用 Azure 门户或 Azure CLI 完成此操作。

Azure CLI 命令可以在 Azure Cloud Shell 中或是安装了 Azure CLI 的工作站上运行。

首先,使用 az ad sp create-for-rbac 命令为应用创建新的服务主体。 该命令还会同时为应用创建应用注册。

az ad sp create-for-rbac --name <service-principal-name>

此命令的输出如下所示。 记下这些值或使此窗口保持打开状态,因为在后续步骤中需使用这些值,且无法再次查看密码(客户端密码)值。 但是,如果需要,可稍后添加新密码,而不会使服务主体或现有密码失效。

{
  "appId": "00001111-aaaa-2222-bbbb-3333cccc4444",
  "displayName": "<service-principal-name>",
  "password": "Ee5Ff~6Gg7.-Hh8Ii9Jj0Kk1Ll2Mm3_Nn4Oo5Pp6",
  "tenant": "aaaabbbb-0000-cccc-1111-dddd2222eeee"
}

2 - 创建用于本地开发的 Microsoft Entra 安全组

由于通常有多个开发人员共同处理应用程序,因此建议创建一个 Microsoft Entra 安全组,以便封装应用在本地开发中所需的角色(权限),而不是将角色分配给各个服务主体对象。 这种做法的优势如下:

  • 由于角色是在组级别分配的,因此可以确保为每个开发人员分配相同的角色。
  • 如果应用需要新角色,则只需将其添加到应用的 Microsoft Entra 组即可。
  • 如果有新的开发人员加入团队,请为该开发人员创建一个新的应用程序服务主体并将其添加到该组中,以确保开发人员拥有正确的权限来处理应用。

az ad group create 命令用于在 Microsoft Entra ID 中创建安全组。 --display-name--main-nickname 参数是必需的。 为组指定的名称应该基于应用程序的名称。 在组的名称中包含类似于“local-dev”的短语来指示组的用途也很有用。

az ad group create \
    --display-name MyDisplay \
    --mail-nickname MyDisplay  \
    --description "<group-description>"

复制命令输出中 id 属性的值。 这是该组的对象 ID。 后面的步骤需要用到它。 还可以使用 az ad group show 命令来检索此属性。

若要将成员添加到组中,需要获取应用程序服务主体的对象 ID(与应用程序 ID 不同)。 使用 az ad sp list 列出可用的服务主体。 --filter 参数命令接受 OData 样式筛选器,并可用于筛选列表,如图所示。 --query 参数将列限制为相关的列。

az ad sp list \
    --filter "startswith(displayName, 'msdocs')" \
    --query "[].{objectId:id, displayName:displayName}" \
    --output table

然后可以使用 az ad group member add 命令将成员添加到组中。

az ad group member add \
    --group <group-name> \
    --member-id <object-id>

注意

默认情况下,Microsoft Entra 安全组的创建仅限于目录中的某些特权角色。 如果无法创建组,请联系目录的管理员。 如果无法将成员添加到现有组,请联系组所有者或目录管理员。 若要了解详细信息,请参阅管理 Microsoft Entra 组和组成员身份

3 - 将角色分配到应用程序

接下来,需要确定应用在哪些资源上需要哪些角色(权限),并将这些角色分配到应用。 在此示例中,角色将分配给在步骤 2 中创建的 Microsoft Entra 组。 可以在资源、资源组或订阅范围分配角色。 此示例演示如何在资源组范围分配角色,因为大多数应用程序将其所有 Azure 资源分组到单个资源组中。

使用 az role assignment create 命令为用户、组或应用程序服务主体分配 Azure 中的角色。 可以使用组的对象 ID 来指定组。 可以使用主体的 appId 来指定应用程序服务主体。

az role assignment create --assignee <appId or objectId> \
    --scope /subscriptions/<subscriptionId>/resourceGroups/<resourceGroupName> \
    --role "<roleName>" 

若要获取可以分配的角色名称,请使用 az role definition list 命令。

az role definition list \
    --query "sort_by([].{roleName:roleName, description:description}, &roleName)" \
    --output table

例如,若要允许 appId 为 00001111-aaaa-2222-bbbb-3333cccc4444 的应用程序服务主体在 ID 为 aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e 的订阅中对 msdocs-python-sdk-auth-example 资源组中的所有存储帐户中的 Azure 存储 blob 容器和数据进行读取、写入和删除访问,你可以使用以下命令将应用程序服务主体分配给存储 Blob 数据参与者角色。

az role assignment create --assignee 00001111-aaaa-2222-bbbb-3333cccc4444 \
    --scope /subscriptions/aaaa0a0a-bb1b-cc2c-dd3d-eeeeee4e4e4e/resourceGroups/msdocs-python-sdk-auth-example \
    --role "Storage Blob Data Contributor"

有关使用 Azure CLI 在资源或订阅级别分配权限的信息,请参阅使用 Azure CLI 分配 Azure 角色一文。

4 - 设置本地开发环境环境

在运行时,DefaultAzureCredential 对象将在一组环境变量中查找服务主体信息。 由于大多数开发人员共同处理多个应用程序,因此建议在开发期间使用 python-dotenv 等包从存储在应用程序目录中的 .env 文件访问环境。 这会限定用于向 Azure 对应用程序进行身份验证的环境变量的范围,以便它们只能由此应用程序使用。

从未将 .env 文件签入源控制,因为它包含 Azure 的应用程序密钥。 Python 的标准 .gitignore 文件会自动从签入中排除 .env 文件。

若要使用 python-dotenv 包,请先在应用程序中安装该包。

pip install python-dotenv

然后,在应用程序根目录中创建 .env 文件。 如下所示,使用从应用注册进程获取的值设置环境变量值:

  • AZURE_CLIENT_ID → 应用 ID 值。
  • AZURE_TENANT_ID → 租户 ID 值。
  • AZURE_CLIENT_SECRET → 为应用生成的密码/凭据。
AZURE_CLIENT_ID=00001111-aaaa-2222-bbbb-3333cccc4444
AZURE_TENANT_ID=aaaabbbb-0000-cccc-1111-dddd2222eeee
AZURE_CLIENT_SECRET=Ee5Ff~6Gg7.-Hh8Ii9Jj0Kk1Ll2Mm3_Nn4Oo5Pp6

最后,在应用程序的启动代码中,使用 python-dotenv 库在启动时从 .env 文件中读取环境变量。

from dotenv import load_dotenv

if ( os.environ['ENVIRONMENT'] == 'development'):
    print("Loading environment variables from .env file")
    load_dotenv(".env")

5 - 在应用程序中实现 DefaultAzureCredential

若要向 Azure 对 Azure SDK 客户端对象进行身份验证,应用程序应使用 azure.identity 包中的 DefaultAzureCredential 类。 在此方案中,DefaultAzureCredential 将检测是否已设置环境变量 AZURE_CLIENT_IDAZURE_TENANT_IDAZURE_CLIENT_SECRET,并读取这些变量,以获取要用于连接到 Azure 的应用程序服务主体信息。

首先将 azure.identity 包添加到应用程序中。

pip install azure-identity

接下来,对于在应用中创建 Azure SDK 客户端对象的任何 Python 代码,你需要:

  1. azure.identity 模块中导入 DefaultAzureCredential 类。
  2. 创建 DefaultAzureCredential 对象。
  3. DefaultAzureCredential 对象传递给 Azure SDK 客户端对象构造函数。

以下代码片段中显示了此操作的示例。

from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient

# Acquire a credential object
token_credential = DefaultAzureCredential()

blob_service_client = BlobServiceClient(
        account_url="https://<my_account_name>.blob.core.windows.net",
        credential=token_credential)