使用共享访问签名 (SAS) 对事件中心资源访问进行身份验证

使用共享访问签名 (SAS) 可以精细控制向客户端授予的访问权限类型。 下面是可以在 SAS 中设置的一些控制措施:

  • SAS 处于有效状态的时间间隔,包括开始时间和到期时间。
  • SAS 授予的权限。 例如,事件中心命名空间的 SAS 可以授予侦听权限,但不能授予发送权限。
  • 只有可提供有效凭据的客户端才能将数据发送到事件中心。
  • 一个客户端无法模拟另一个客户端。
  • 可以阻止恶意客户端向事件中心发送数据。

本文介绍如何使用 SAS 对事件中心资源访问进行身份验证。 若要了解如何使用 SAS 授权对事件中心资源的访问,请参阅此文

注意

作为安全最佳做法,Microsoft 建议尽可能地使用 Microsoft Entra 凭据,而不要使用更容易泄密的共享访问签名。 尽管你可以继续使用共享访问签名 (SAS) 授予对事件中心资源的精细访问权限,但 Microsoft Entra ID 提供了类似的功能,并且不需要管理 SAS 令牌,也不需要担心吊销已泄密的 SAS。

有关 Azure 事件中心内 Microsoft Entra 集成的详细信息,请参阅使用 Microsoft Entra ID 授权访问事件中心

配置 SAS 身份验证

可以在事件中心命名空间或实体(事件中心实例或事件中心内的 Kafka 主题)中配置 SAS 规则。 目前不支持在使用者组中配置 SAS 规则,但你可以使用命名空间或实体中配置的规则来保护对使用者组的访问。

下图显示了如何对示例实体应用授权规则。

Configure authorization rule

在此示例中,示例事件中心命名空间 (ExampleNamespace) 有两个实体:eh1 和 Kafka topic1。 授权规则是同时在实体级别和命名空间级别定义的。

manageRuleNS、sendRuleNS 和 listenRuleNS 授权规则将同时应用到 eh1 和 t1。 listenRule-eh 和 sendRule-eh 授权规则仅应用到 eh1,sendRuleT 授权规则仅应用到 topic1。

使用 sendRuleNS 授权规则时,客户端应用程序可以同时向 eh1 和 topic1 发送消息。 使用 sendRuleT 授权规则时,会强制要求仅对 topic1 进行粒度访问,因此,使用此规则进行访问的客户端应用程序现在无法向 eh1 发送消息,而只能向 topic1 发送消息。

生成共享访问签名令牌

有权访问授权规则的名称及其某个签名密钥的任何客户端都可以生成 SAS 令牌。 令牌是通过采用以下格式编写一个字符串而生成的:

  • se - 令牌即时过期时间。 一个整数,反映令牌过期距 1970 年 1 月 1 日 00:00:00 UTC 纪元时间(UNIX 纪元)的秒数
  • skn - 授权规则的名称,即 SAS 密钥名称。
  • sr - 正在访问的资源的 URI。
  • sig - 签名。

signature-string 是基于资源 URI 计算的 SHA-256 哈希(上一部分中所述的范围),以及令牌即时过期时间的字符串表示形式,以 CRLF 分隔。 哈希计算方式如以下虚拟代码所示,返回 256 位/32 字节哈希值。

SHA-256('https://<yournamespace>.servicebus.chinacloudapi.cn/'+'\n'+ 1438205742)

令牌包含非哈希值,使接收方可以使用相同的参数重新计算哈希,并验证颁发者是否拥有有效的签名密钥。

资源 URI 是向其声明访问权限的服务总线资源的完整 URI。 例如,http://<namespace>.servicebus.chinacloudapi.cn/<entityPath>sb://<namespace>.servicebus.chinacloudapi.cn/<entityPath>,即 http://contoso.servicebus.chinacloudapi.cn/eh1

URI 必须采用百分比编码格式。

用于签名的 SAS 规则必须在此 URI 指定的实体上,或由其分层父级之一进行配置。 例如,之前示例中的 http://contoso.servicebus.chinacloudapi.cn/eh1http://contoso.servicebus.chinacloudapi.cn

SAS 令牌对于以 signature-string 中使用的 <resourceURI> 为前缀的所有资源有效。

注意

使用共享访问策略为事件中心生成访问令牌。 有关详细信息,请参阅共享访问授权策略

从策略生成签名(令牌)

以下部分介绍如何使用共享访问签名策略生成 SAS 令牌。

NodeJS

function createSharedAccessToken(uri, saName, saKey) { 
  if (!uri || !saName || !saKey) { 
          throw "Missing required parameter"; 
      } 
  var encoded = encodeURIComponent(uri); 
  var now = new Date(); 
  var week = 60*60*24*7;
  var ttl = Math.round(now.getTime() / 1000) + week;
  var signature = encoded + '\n' + ttl; 
  var hash = crypto.createHmac('sha256', saKey).update(signature, 'utf8').digest('base64'); 
  return 'SharedAccessSignature sr=' + encoded + '&sig=' +  
      encodeURIComponent(hash) + '&se=' + ttl + '&skn=' + saName; 
}

若要使用策略名称和键值连接到事件中心,请使用采用 AzureNamedKeyCredential 参数的 EventHubProducerClient 构造函数。

const producer = new EventHubProducerClient("NAMESPACE NAME.servicebus.chinacloudapi.cn", eventHubName, new AzureNamedKeyCredential("POLICYNAME", "KEYVALUE"));

你将需要添加对 AzureNamedKeyCredential 的引用。

const { AzureNamedKeyCredential } = require("@azure/core-auth");

若要使用通过上述代码生成的 SAS 令牌,请使用采用 AzureSASCredential 参数的 EventHubProducerClient 构造函数。

var token = createSharedAccessToken("https://NAMESPACENAME.servicebus.chinacloudapi.cn", "POLICYNAME", "KEYVALUE");
const producer = new EventHubProducerClient("NAMESPACENAME.servicebus.chinacloudapi.cn", eventHubName, new AzureSASCredential(token));

你将需要添加对 AzureSASCredential 的引用。

const { AzureSASCredential } = require("@azure/core-auth");

Java

private static String GetSASToken(String resourceUri, String keyName, String key)
  {
      long epoch = System.currentTimeMillis()/1000L;
      int week = 60*60*24*7;
      String expiry = Long.toString(epoch + week);

      String sasToken = null;
      try {
          String stringToSign = URLEncoder.encode(resourceUri, "UTF-8") + "\n" + expiry;
          String signature = getHMAC256(key, stringToSign);
          sasToken = "SharedAccessSignature sr=" + URLEncoder.encode(resourceUri, "UTF-8") +"&sig=" +
                  URLEncoder.encode(signature, "UTF-8") + "&se=" + expiry + "&skn=" + keyName;
      } catch (UnsupportedEncodingException e) {

          e.printStackTrace();
      }

      return sasToken;
  }

public static String getHMAC256(String key, String input) {
    Mac sha256_HMAC = null;
    String hash = null;
    try {
        sha256_HMAC = Mac.getInstance("HmacSHA256");
        SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(), "HmacSHA256");
        sha256_HMAC.init(secret_key);
        Encoder encoder = Base64.getEncoder();

        hash = new String(encoder.encode(sha256_HMAC.doFinal(input.getBytes("UTF-8"))));

    } catch (InvalidKeyException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
   } catch (IllegalStateException e) {
        e.printStackTrace();
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }

    return hash;
}

PHP

function generateSasToken($uri, $sasKeyName, $sasKeyValue) 
{ 
    $targetUri = strtolower(rawurlencode(strtolower($uri))); 
    $expires = time(); 	
    $expiresInMins = 60; 
    $week = 60*60*24*7;
    $expires = $expires + $week; 
    $toSign = $targetUri . "\n" . $expires; 
    $signature = rawurlencode(base64_encode(hash_hmac('sha256', 			
     $toSign, $sasKeyValue, TRUE))); 

    $token = "SharedAccessSignature sr=" . $targetUri . "&sig=" . $signature . "&se=" . $expires . 		"&skn=" . $sasKeyName; 
    return $token; 
}

C#

private static string createToken(string resourceUri, string keyName, string key)
{
    TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
    var week = 60 * 60 * 24 * 7;
    var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week);
    string stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
    using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
    {
        var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
        var sasToken = String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry, keyName);
        return sasToken;
    }
}

PowerShell

[Reflection.Assembly]::LoadWithPartialName("System.Web")| out-null
$URI="myNamespace.servicebus.chinacloudapi.cn/myEventHub/"
$Access_Policy_Name="RootManageSharedAccessKey"
$Access_Policy_Key="myPrimaryKey"
#Token expires now+300
$Expires=([DateTimeOffset]::Now.ToUnixTimeSeconds())+300
$SignatureString=[System.Web.HttpUtility]::UrlEncode($URI)+ "`n" + [string]$Expires
$HMAC = New-Object System.Security.Cryptography.HMACSHA256
$HMAC.key = [Text.Encoding]::ASCII.GetBytes($Access_Policy_Key)
$Signature = $HMAC.ComputeHash([Text.Encoding]::ASCII.GetBytes($SignatureString))
$Signature = [Convert]::ToBase64String($Signature)
$SASToken = "SharedAccessSignature sr=" + [System.Web.HttpUtility]::UrlEncode($URI) + "&sig=" + [System.Web.HttpUtility]::UrlEncode($Signature) + "&se=" + $Expires + "&skn=" + $Access_Policy_Name
$SASToken

BASH

get_sas_token() {
    local EVENTHUB_URI=$1
    local SHARED_ACCESS_KEY_NAME=$2
    local SHARED_ACCESS_KEY=$3
    local EXPIRY=${EXPIRY:=$((60 * 60 * 24))} # Default token expiry is 1 day

    local ENCODED_URI=$(echo -n $EVENTHUB_URI | jq -s -R -r @uri)
    local TTL=$(($(date +%s) + $EXPIRY))
    local UTF8_SIGNATURE=$(printf "%s\n%s" $ENCODED_URI $TTL | iconv -t utf8)

    local HASH=$(echo -n "$UTF8_SIGNATURE" | openssl sha256 -hmac $SHARED_ACCESS_KEY -binary | base64)
    local ENCODED_HASH=$(echo -n $HASH | jq -s -R -r @uri)

    echo -n "SharedAccessSignature sr=$ENCODED_URI&sig=$ENCODED_HASH&se=$TTL&skn=$SHARED_ACCESS_KEY_NAME"
}

使用 SAS 对事件中心发布者进行身份验证

事件发布者定义事件中心的虚拟终结点。 发布者只可用于将消息发送到事件中心,而不可用于接收消息。

通常,事件中心为每个客户端使用一个发布者。 发送到事件中心的任何发布者的所有消息都会在该事件中心内排队。 发布者可以实现精细访问控制。

为每个事件中心客户端分配一个唯一令牌,该令牌将上传到客户端。 生成令牌后,每个唯一令牌授予对不同唯一发布者的访问权限。 持有令牌的客户端只能向一个发布者发送消息,但不能向其他发布者发送消息。 如果多个客户端共享同一令牌,则其中每个客户端都会共享该发布者。

为所有令牌分配 SAS 密钥。 通常,所有令牌使用同一密钥进行签名。 客户端不知道密钥,这可以防止客户端制造令牌。 客户端以相同的令牌运行,直到令牌过期。

例如,若要定义范围限定为向事件中心发送/发布消息的授权规则,需要定义发送授权规则。 可以在命名空间级别执行此操作,或者为特定的实体(事件中心实例或主题)分配更精细的范围。 具有此类粒度访问范围的客户端或应用程序称为“事件中心发布者”。 为此,请执行以下步骤:

  1. 在要发布的实体中创建一个 SAS 密钥,以在其上分配发送范围。 有关详细信息,请参阅共享访问授权策略

  2. 使用在步骤 1 中生成的密钥为特定的发布者生成具有过期时间的 SAS 令牌。 有关示例代码,请参阅从策略生成签名(令牌)

  3. 向发布者客户端提供令牌,该客户端只能将消息发送到令牌授予了访问权限的实体和发布者。

    令牌过期后,客户端将失去向该实体发送/发布消息的访问权限。

注意

可为设备配置令牌,用于授予对事件中心或命名空间的访问权限,但不建议这样做。 持有此令牌的任何设备都可以直接将消息发送到该事件中心。 此外,无法将设备列入阻止列表,使其无法向该事件中心发送消息。

我们始终建议指定具体的精细范围。

重要

创建令牌后,会为每个客户端设置其自身唯一的令牌。

当客户端向事件中心发送数据时,客户端会使用令牌标记其请求。 为了防止攻击者窃听和盗取令牌,客户端与事件中心之间的通信必须通过加密通道进行。

如果令牌被攻击者盗取,攻击者可能会模拟令牌已被盗的客户端。 将发布者列入阻止列表会导致客户端不可用,直至该客户端收到使用其他发布者的新令牌。

使用 SAS 对事件中心使用者进行身份验证

若要对使用事件中心生成者生成的数据的后端应用程序进行身份验证,事件中心令牌身份验证要求其客户端对其事件中心命名空间或者事件中心实例或主题拥有管理权限或侦听权限。 通过使用者组使用事件中心内的数据。 尽管 SAS 策略可以提供精细范围,但此范围仅仅是在实体级别定义的,而不是在使用者级别定义的。 这意味着,在命名空间级别或者事件中心实例或主题级别定义的特权将应用到该实体的使用者组。

禁用本地/SAS 密钥身份验证

对于某些组织安全要求,你可能需要完全禁用本地/SAS 密钥身份验证,并依赖基于 Microsoft Entra ID 的身份验证,这是与 Azure 事件中心连接的推荐方法。 可以使用 Azure 门户或 Azure 资源管理器模板在事件中心命名空间级别禁用本地/SAS 密钥身份验证。

通过门户禁用本地/SAS 密钥身份验证

可以使用 Azure 门户禁用给定事件中心命名空间的本地/SAS 密钥身份验证。

如下图所示,在命名空间概述部分中,选择“本地身份验证”。

Namespace overview for disabling local auth

然后选择“禁用”选项,然后选择“确定”,如下所示。 Disabling local auth

使用模板禁用本地/SAS 密钥身份验证

可以通过将 disableLocalAuth 属性设置为 true 来禁用给定事件中心命名空间的本地身份验证,如以下 Azure 资源管理器模板(ARM 模板)中所示。

"resources":[
      {
         "apiVersion":"[variables('ehVersion')]",
         "name":"[parameters('eventHubNamespaceName')]",
         "type":"Microsoft.EventHub/Namespaces",
         "location":"[variables('location')]",
         "sku":{
            "name":"Standard",
            "tier":"Standard"
         },
         "resources": [
    {
      "apiVersion": "2017-04-01",
      "name": "[parameters('eventHubNamespaceName')]",
      "type": "Microsoft.EventHub/Namespaces",
      "location": "[resourceGroup().location]",
      "sku": {
        "name": "Standard"
      },
      "properties": {
        "isAutoInflateEnabled": "true",
        "maximumThroughputUnits": "7", 
        "disableLocalAuth": false
      },
      "resources": [
        {
          "apiVersion": "2017-04-01",
          "name": "[parameters('eventHubName')]",
          "type": "EventHubs",
          "dependsOn": [
            "[concat('Microsoft.EventHub/namespaces/', parameters('eventHubNamespaceName'))]"
          ],
          "properties": {
            "messageRetentionInDays": "[parameters('messageRetentionInDays')]",
            "partitionCount": "[parameters('partitionCount')]"
          }

        }
      ]
    }
  ]

示例

  • 请参阅此 GitHub 位置的 .NET 示例 #6,了解如何使用共享访问凭据或默认 Azure 凭据标识来将事件发布到事件中心。
  • 请参阅此 GitHub 位置的 .NET 示例 #5,了解如何使用共享访问凭据或默认 Azure 凭据标识来使用或处理事件。

后续步骤

请参阅以下文章:

请参阅以下相关文章: