你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

附属密钥模式

Azure
Azure 存储

使用向客户端授予对特定资源的受限直接访问权限的令牌,以便卸载从应用程序进行的传输数据。 这在使用云托管存储系统或队列的应用程序中特别有用,可以最大程度降低成本并提高可伸缩性和性能。

上下文和问题

客户端程序和 Web 浏览器通常需要从应用程序的存储读取和写入文件或数据流。 通常,应用程序会处理数据的移动 — 通过从存储提取数据并流式传输到客户端,或通过从客户端读取上传的流并将它存储在数据存储中。 但是,此方法会占用宝贵资源,如计算、内存和带宽。

数据存储能够直接处理数据的上传和下载,而无需应用程序执行任何处理来移动这些数据。 但是,这通常要求客户端有权访问存储的安全凭据。 这可以是一种有用技术,可最大程度减少数据传输成本和应用程序扩大要求,并最大程度提高性能。 不过,这意味着应用程序不再能够管理数据的安全性。 客户端与数据存储连接以便进行直接访问之后,应用程序无法充当守护程序。 它不再控制过程,无法阻止从数据存储进行的后续上传或下载。

这在需要为不受信任的客户端提供服务的分布式系统中不是现实的方法。 相反,应用程序必须能够以精细方式安全地控制对数据的访问,但是仍设置此连接,然后允许客户端直接与数据存储进行通信以执行所需的读取或写入操作,从而减少服务器上的负载。

解决方案

你需要解决以下问题:在存储无法管理客户端的身份验证和授权的情况下,控制对数据存储的访问。 一种典型解决方案是限制对数据存储公用连接的访问,并向客户端提供数据存储可以验证的密钥或令牌。

此密钥或令牌通常称为附属密钥。 它提供对特定资源的限时访问,仅允许通过精细控制执行预定义操作,如写入存储但不读取,或是在 Web 浏览器中上传和下载。 应用程序可以快速、方便地创建附属密钥并颁发给客户端设备和 Web 浏览器,使客户端可以执行所需操作,而无需应用程序直接处理数据传输。 这样可从应用程序和服务器中消除处理开销以及对性能和可伸缩性的影响。

客户端使用此令牌在特定时间段内访问数据存储中的特定资源,并且访问权限会受到特定限制,如图所示。 在指定时间段之后,密钥会成为无效状态,不允许访问资源。

典型的附属密钥模式工作流示意图。

显示使用附属密钥模式的系统工作流示例的示意图。 步骤 1 显示用户请求目标资源。 步骤 2 显示附属密钥应用程序检查请求的有效性并生成访问令牌。 步骤 3 显示要返回给用户的令牌。 步骤 4 显示用户使用令牌访问目标资源。

还可以配置具有其他依赖项(如数据范围)的密钥。 例如,根据数据存储功能,密钥可以指定数据存储中的一个完整表,或是仅指定表中的特定行。 在云存储系统中,密钥可以指定一个容器,或仅指定容器中的特定项。

应用程序也可以使密钥失效。 如果客户端通知服务器数据传输操作已完成,则这是很有用的方法。 服务器随后可以使该密钥失效,防止进一步的访问。

使用此模式可以简化资源访问管理,因为无需创建用户并进行身份验证,授予权限,然后再次删除用户,或者更糟的是,将该权限作为长期权限。 通过此模式还可以轻松地限制位置、权限和有效期 — 只需在运行时生成密钥即可实现所有这些目的。 重要因素是尽可能严格地限制有效期,特别是资源的位置,以便接收者只能将它用于预期用途。

问题和注意事项

在决定如何实现此模式时,请考虑以下几点:

管理密钥的有效性状态和有效期。 如果泄露或受侵害,则密钥可有效地解锁目标项,使它可在有效期内用于恶意用途。 根据颁发方式,密钥通常可以进行撤销或禁用。 可以更改服务器端策略,也可以使签名时使用的服务器密钥失效。 指定较短的有效期可最大程度降低允许对数据存储进行未经授权操作的风险。 但是,如果有效期太短,则客户端可能无法在密钥过期之前完成操作。 如果需要对受保护资源进行多次访问,则允许授权用户在有效期过期之前续订密钥。

控制密钥提供的访问级别。 通常,密钥应只允许用户执行完成任务所需的操作,例如只读访问(如果客户端不应能够将数据上传到数据存储)。 对于文件上传,常常指定提供只写权限的密钥以及位置和有效期。 准确指定密钥所应用于的资源或资源集至关重要。

考虑如何控制用户的行为。 实现此模式意味着会失去一些对用户有权访问的资源的控制权。 可以施加的控制级别受可用于服务或目标数据存储的策略和权限的功能所限制。 例如,通常无法创建密钥来限制要写入存储的数据的大小或密钥可以用于访问文件的次数。 这可能会对数据传输导致巨大的意外成本(即使由预期客户端所使用),可能由导致重复上传或下载的代码错误所引起。 若要尽可能限制文件可以上传的次数,请强制客户端在一个操作完成时通知应用程序。 例如,某些数据存储会引发应用程序代码可以用于监视操作和控制用户行为的事件。 但是,在一个租户的所有用户使用相同密钥的多租户方案中,难以对各个用户强制实施配额。 授予用户创建权限可以通过使令牌有效地单次使用来帮助控制要更新的数据量。 创建权限不允许覆盖,因此每个令牌只能用于一个写入活动。

验证,并根据需要清理所有上传的数据。 授予密钥访问权限的恶意用户可以上传旨在危害系统的数据。 或者,授权用户上传的数据可能无效,以及在处理时,可能会导致错误或系统故障。 若要防止此情况出现,请确保所有上传的数据在使用之前都经过验证,并检查是否存在恶意内容。

审核所有操作。 许多基于密钥的机制可以记录诸如上传、下载和失败这类操作。 这些日志通常可以合并到一个审核进程中,还可用于计费(如果基于文件大小或数据量对用户进行收费)。 使用这些日志可检测可能由密钥提供程序方面的问题或意外删除存储的访问策略所导致的身份验证失败。

安全地传递密钥。 密钥可以嵌入在用户在网页中激活的 URL 中,也可以在服务器重定向操作中使用,以便自动进行下载。 应始终使用 HTTPS 通过安全通道传递密钥。

在传输中保护敏感数据。 通常会使用 TLS 执行通过应用程序的敏感数据传递,并且应对直接访问数据存储的客户端强制执行此操作。

实现此模式时要注意的其他问题包括:

  • 如果客户端不会(或无法)向服务器通知操作完成,并且唯一的限制是密钥的有效期,则应用程序无法执行审核操作,如对上传或下载数进行计数,或阻止多次上传或下载。

  • 可以生成的密钥策略的灵活性可能会受到限制。 例如,某些机制仅允许使用定时有效期。 其他一些机制则无法指定足够的读取/写入权限粒度。

  • 如果指定密钥或令牌有效期的开始时间,请确保它稍早于当前服务器时间,从而允许使用可能略微不同步的服务器时钟。 如果未指定,则默认情况下通常是当前服务器时间。

  • 包含密钥的 URL 可能记录在服务器日志文件中。 虽然密钥通常会在使用日志文件进行分析之前过期,不过请确保限制对它们的访问。 如果日志数据传输到监视系统或存储在其他位置,请考虑实现延迟以防止密钥泄漏,直到其有效期过期后。

  • 如果客户端代码在 Web 浏览器中运行,则浏览器可能需要支持跨域资源共享 (CORS),以便使在 Web 浏览器中执行的代码可以从向页面提供服务的一个域访问不同域中的数据。 某些较旧的浏览器和某些数据存储不支持 CORS,在这些浏览器中运行的代码可能无法使用附属密钥提供对不同域(如云存储帐户)中数据的访问。

  • 虽然客户端不需要为最终资源预先配置身份验证,但客户端确实需要预先建立对附属密钥服务的身份验证方式。

  • 密钥只能分发给具有适当授权的经过身份验证的客户端。

  • 生成访问令牌是特权操作,因此必须使用严格的访问策略来保护附属密钥服务。 该服务可能允许第三方访问敏感系统,因此该服务的安全性尤为重要。

何时使用此模式

此模式可用于以下情况:

  • 最大程度减少资源加载并最大程度提高性能和可伸缩性。 使用附属密钥无需锁定资源,无需远程服务器调用,对可以颁发的附属密钥数没有限制,并且可避免通过应用程序代码执行数据传输所导致的单一故障点。 创建附属密钥通常是使用密钥对字符串进行签名的简单加密操作。

  • 最大程度减少运营成本。 实现对存储和队列的直接访问在资源和成本方面十分高效,可以减少网络往返,并且可能会减少所需的计算资源数。

  • 当客户端定期上传或下载数据时,尤其是在具有大型卷或每个操作都涉及大型文件时。

  • 当应用程序可用的计算资源有限时(由于托管限制或考虑到成本)。 在此方案中,如果有许多并发数据上传或下载,则该模式甚至更加有用,因为它会将应用程序从处理数据传输中解放出来。

  • 当数据存储在远程数据存储或不同的区域中时。 如果应用程序需要充当守护程序,则对于区域之间,或是客户端与应用程序之间,随后是应用程序与数据中心之间的公用或专用网络上的数据传输额外带宽,可能会进行收费。

此模式在以下情况中可能不起作用:

  • 如果客户端已经可以对后端服务进行唯一身份验证(例如,使用 RBAC),则不要使用此模式。

  • 如果应用程序在存储数据之前或将它发送到客户端之前,必须对数据执行一些任务。 例如,如果应用程序需要执行验证、记录访问成功或是对数据执行转换。 但是,某些数据存储和客户端能够进行协商并执行简单转换,如压缩和解压缩(例如,Web 浏览器通常可以处理 gzip 格式)。

  • 如果现有应用程序的设计使得难以采用此模式。 使用此模式通常需要对传递和接收数据使用不同的体系结构方法。

  • 如果需要维护审核线索或控制执行数据传输操作的次数,并且使用的附属密钥机制不支持服务器可以用于管理这些操作的通知。

  • 如果需要限制数据的大小,尤其是在上传操作过程中。 对此情况的唯一解决方法是使应用程序在操作完成之后查看数据大小,或是在指定时间段之后或定期检查上传大小。

工作负荷设计

架构师应评估如何在其工作负载的设计中使用“附属密钥模式”,以解决 Azure Well-Architected Framework 支柱中涵盖的目标和原则。 例如:

支柱 此模式如何支持支柱目标
安全设计决策有助于确保工作负荷数据和系统的机密性完整性可用性 此模式使客户端可以直接访问资源,而无需长期或有效凭据。 所有访问请求都以可审核事务开头。 然后,授予的访问权限在范围和持续时间上都受到限制。 这种模式还使撤销授予的访问权限变得更加容易。

- SE:05 标识和访问管理
成本优化的重点是维持和提高工作负载的投资回报率 这种设计将处理作为客户端和资源之间的排他性关系进行卸载,而无需添加直接处理所有客户端请求的组件。 当客户端请求频繁或大到需要大量代理资源时,这种好处最为显著。

- CO:09 流成本
性能效率通过在缩放、数据和代码方面进行优化, 帮助工作负载高效地满足需求 不使用中间资源将访问卸载处理代理为客户端与资源之间的独占关系,而无需需要以高性能方式处理所有客户端请求的大使组件。 当代理不为事务增加值时,使用此模式的好处最为显著。

- PE:07 代码和基础结构

与任何设计决策一样,请考虑对可能采用此模式引入的其他支柱的目标进行权衡。

示例

Azure 在 Azure 存储上支持共享访问签名,以便对 blob、表和队列中的数据进行精细的访问控制以及用于服务总线队列和主题。 可以配置共享访问签名令牌,以提供特定访问权限,如对特定表、表中的键范围、队列、blob 或 blob 容器进行的读取、写入、更新和删除。 有效期可以是指定的时间段。 此功能非常适合使用附属密钥进行访问。

假设有一个工作负载,它有数百个移动或桌面客户端频繁上传大型二进制文件。 如果没有这种模式,工作负载基本上有两种选择。 第一种是向所有客户端提供永久访问和配置,以便直接向存储帐户上传数据。 另一种是实现网关路由模式,以设置一个终结点,其中客户端使用代理访问存储,但这可能不会向事务添加额外的值。 这两种方法都存在模式背景下解决的问题:

  • 长久的、预共享密钥。 可能没有太多方法向不同的客户端提供不同的密钥。
  • 增加了运行具有足够资源来处理当前接收的大型文件的计算服务的费用。
  • 通过在上传过程中添加额外的计算层和网络跃点,可能会减缓客户端交互。

使用附属密钥模式可以解决安全性、成本优化和性能方面的问题。

显示客户端在首次从 API 获得访问令牌之后访问存储帐户的图。

  1. 在最后一个负责任的时刻,客户端向轻量、可扩展到零的 Azure Function 托管的 API 进行身份验证,以请求访问。

  2. API 验证请求,然后获取并返回时间范围受限的 SaS 令牌。

    API生成的令牌对客户端的限制如下:

    • 要使用的存储帐户。 也就是说,客户不需要提前知道这些信息。
    • 要使用的特定容器和文件名;确保令牌最多只能用于一个文件。
    • 短暂的操作窗口,如三分钟。 这个短时间段确保令牌的 TTL 不会超过其效用。
    • 只允许创建 blob 的权限;不能下载、更新或删除。
  3. 然后,客户端在短暂的时间范围内使用该令牌将文件直接上传到存储帐户。

API 根据 API 自己的 Microsoft Entra ID 托管标识,使用用户委托密钥向授权客户端生成这些令牌。 在存储帐户和令牌生成 API 上启用日志记录,允许令牌请求和令牌使用之间的关联。 API 可以使用客户端身份验证信息或其他可用数据来决定使用哪个存储帐户或容器,例如在多租户情况下。

GitHub 上的附属密钥模式示例提供了完整的示例。 以下代码片段改编自该示例。 第一个示例显示 Azure Function(位于 ValetKey.Web 中)如何使用 Azure Function 自己的托管标识生成用户委托的共享访问签名令牌。

[Function("FileServices")]
public async Task<StorageEntitySas> GenerateTokenAsync([HttpTrigger(...)] HttpRequestData req, ..., 
                                                        CancellationToken cancellationToken)
{
  // Authorize the caller, select a blob storage account, container, and file name.
  // Authenticate to the storage account with the Azure Function's managed identity.
  ...

  return await GetSharedAccessReferenceForUploadAsync(blobContainerClient, blobName, cancellationToken);
}

/// <summary>
/// Return an access key that allows the caller to upload a blob to this
/// specific destination for about three minutes.
/// </summary>
private async Task<StorageEntitySas> GetSharedAccessReferenceForUploadAsync(BlobContainerClient blobContainerClient, 
                                                                            string blobName,
                                                                            CancellationToken cancellationToken)
{
  var blobServiceClient = blobContainerClient.GetParentBlobServiceClient();
  var blobClient = blobContainerClient.GetBlockBlobClient(blobName);

  // Allows generating a SaS token that is evaluated as the union of the RBAC permissions on the managed identity
  // (for example, Blob Data Contributor) and then narrowed further by the specific permissions in the SaS token.
  var userDelegationKey = await blobServiceClient.GetUserDelegationKeyAsync(DateTimeOffset.UtcNow.AddMinutes(-3),
                                                                            DateTimeOffset.UtcNow.AddMinutes(3),
                                                                            cancellationToken);

  // Limit the scope of this SaS token to the following:
  var blobSasBuilder = new BlobSasBuilder
  {
      BlobContainerName = blobContainerClient.Name,     // - Specific container
      BlobName = blobClient.Name,                       // - Specific filename
      Resource = "b",                                   // - Blob only
      StartsOn = DateTimeOffset.UtcNow.AddMinutes(-3),  // - For about three minutes (+/- for clock drift)
      ExpiresOn = DateTimeOffset.UtcNow.AddMinutes(3),  // - For about three minutes (+/- for clock drift)
      Protocol = SasProtocol.Https                      // - Over HTTPS
  };
  blobSasBuilder.SetPermissions(BlobSasPermissions.Create);

  return new StorageEntitySas
  {
      BlobUri = blobClient.Uri,
      Signature = blobSasBuilder.ToSasQueryParameters(userDelegationKey, blobServiceClient.AccountName).ToString();
  };
}

以下代码片段是 API 和客户端使用的数据传输对象 (DTO)。

public class StorageEntitySas
{
  public Uri? BlobUri { get; internal set; }
  public string? Signature { get; internal set; }
}

然后,客户端(位于 ValetKey.Client 中)使用从 API 返回的 URI 和令牌来执行上传,而不需要额外的资源并且具有完全的客户端到存储性能。

...

// Get the SaS token (valet key)
var blobSas = await httpClient.GetFromJsonAsync<StorageEntitySas>(tokenServiceEndpoint);
var sasUri = new UriBuilder(blobSas.BlobUri)
{
    Query = blobSas.Signature
};

// Create a blob client using the SaS token as credentials
var blob = new BlobClient(sasUri.Uri);

// Upload the file directly to blob storage
using (var stream = await GetFileToUploadAsync(cancellationToken))
{
    await blob.UploadAsync(stream, cancellationToken);
}

...

后续步骤

实现此模式时,以下指南可能比较有用:

实现此模式时,以下模式也可能有用:

  • 守护程序模式。 此模式可以与附属密钥模式结合使用,通过充当客户端与应用程序或服务之间中转站的专用主机实例,来保护应用程序和服务。 守护程序可验证和清理请求,以及在客户端与应用程序之间传递请求和数据。 可以额外提供一层安全,并减小系统的攻击面。
  • 静态内容托管模式。 介绍如何将静态资源部署到基于云的存储服务,而该服务可以将这些资源直接提供给客户端以减少对成本高昂的计算实例的需求。 如果不打算公开提供资源,则可以使用附属密钥模式来保护它们。