设置包含资源数据的更改通知

Microsoft Graph 允许应用通过 不同的传递渠道订阅和接收资源的更改通知。 你可以设置订阅,以将更改后的资源数据(例如 Microsoft Teams 聊天消息的内容或 Microsoft Teams 状态信息)包括在更改通知中。 包含资源更改数据的更改通知称为“丰富通知”。 应用可以使用丰富的通知来运行业务逻辑,而无需进行单独的 API 调用来提取更改的资源。

本文将指导你完成在应用程序中设置丰富通知的过程。

支持的资源

丰富通知可用于以下资源。

注意

使用星号 (*) 标记的终结点订阅的丰富通知仅在终结点上 /beta 可用。

资源 支持的资源路径 限制
Outlook 事件 对用户邮箱中所有事件的更改: /users/{id}/events $select需要仅返回丰富通知中的一部分属性。 有关详细信息,请参阅 Outlook 资源的更改通知
Outlook 邮件 对用户邮箱中所有邮件的更改: /users/{id}/messages

对用户收件箱中邮件的更改: /users/{id}/mailFolders/{id}/messages
$select需要仅返回丰富通知中的一部分属性。 有关详细信息,请参阅 Outlook 资源的更改通知
Outlook 个人联系人 对用户邮箱中所有个人联系人的更改: /users/{id}/contacts

对用户 contactFolder 中所有个人联系人的更改: /users/{id}/contactFolders/{id}/contacts
$select需要仅返回丰富通知中的一部分属性。 有关详细信息,请参阅 Outlook 资源的更改通知
Teams callRecording 组织中的所有录制内容: communications/onlineMeetings/getAllRecordings

特定会议的所有录制内容: communications/onlineMeetings/{onlineMeetingId}/recordings

在由特定用户组织的会议中可用的通话记录: users/{id}/onlineMeetings/getAllRecordings

在安装了特定 Teams 应用的会议中可用的通话记录: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllRecordings *
最大订阅配额:
  • 每个应用和联机会议组合:1
  • 每个应用和用户组合:1
  • 每个用户 (,用于跟踪由用户组织的所有 onlineMeeting 中的记录的订阅) :10 个订阅。
  • 每个组织:总共 10,000 个订阅。
  • Teams callTranscript 组织中的所有脚本: communications/onlineMeetings/getAllTranscripts

    特定会议的所有脚本: communications/onlineMeetings/{onlineMeetingId}/transcripts

    在由特定用户组织的会议中可用的通话记录: users/{id}/onlineMeetings/getAllTranscripts

    在安装了特定 Teams 应用的会议中可用的通话记录: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllTrancripts *
    最大订阅配额:
  • 每个应用和联机会议组合:1
  • 每个应用和用户组合:1
  • 按用户 (订阅跟踪由用户组织的所有 onlineMeeting 中的脚本) :10 个订阅。
  • 每个组织:总共 10,000 个订阅。
  • Teams 频道 更改所有团队中的频道: /teams/getAllChannels

    对特定团队中的频道所做的更改: /teams/{id}/channels
    -
    Teams 聊天 对租户中任何聊天的更改: /chats

    对特定聊天的更改: /chats/{id}
    -
    Teams chatMessage 对所有团队所有频道中聊天消息的更改: /teams/getAllMessages

    对特定频道中的聊天消息的更改: /teams/{id}/channels/{id}/messages

    更改所有聊天中的聊天消息: /chats/getAllMessages

    对特定聊天中聊天消息的更改: /chats/{id}/messages

    对特定用户的所有聊天中聊天消息的更改是以下部分的一部分: /users/{id}/chats/getAllMessages
    不支持使用 $select 仅返回所选属性。 丰富通知包含已更改实例的所有属性。
    Teams conversationMember 对特定团队中成员身份的更改: /teams/{id}/members



    对特定聊天中成员身份的更改: /chats/{id}/members
    -
    Teams onlineMeeting * 对联机会议的更改: /communications/onlineMeetings/?$filter=JoinWebUrl eq '{joinWebUrl} * 不支持使用 $select 仅返回所选属性。 丰富通知包含已更改实例的所有属性。
    Teams 状态 对单个用户状态的更改: /communications/presences/{id} 不支持使用 $select 仅返回所选属性。 丰富通知包含已更改实例的所有属性。
    Teams 团队 对租户中任何团队的更改: /teams

    对特定团队的更改: /teams/{id}
    -

    通知负载中的资源数据

    通常,这种类型的更改通知在有效负载中包含以下资源数据:

    • resourceData属性中返回的已更改资源实例的ID和类型。
    • 按照订阅中规定内容加密、在 encryptedContent 属性中返回的资源实例的所有属性值。
    • 或者,具体取决于资源、resourceData属性中返回的特定属性。 若要仅获取特定属性,请使用 $select 参数,将其指定为订阅中的资源URL 的一部分。

    创建订阅

    丰富通知的设置方式与 基本更改通知相同。

    为了安全起想,Microsoft Graph 会对在丰富通知中返回的资源数据进行加密。 在创建订阅过程中,必须提供公共加密密钥。 有关创建和管理加密密钥的详细信息,请参阅 解密更改通知中的资源数据

    若要创建包含丰富通知的订阅, 必须 指定以下属性:

    • includeResourceData,应设置为 true 以明确请求资源数据。
    • encryptionCertificate ,它仅包含 Microsoft Graph 用于加密返回到应用的资源数据的公钥。
    • encryptionCertificateId,是证书的自有标识符。 使用此 ID 在各更改通知中匹配用于解密的证书。

    请注意下列事项:

    • 通知终结点验证中所述,验证两个终结点。 如果选择对两个终结点使用相同的 URL,则会收到并应响应两个验证请求。

    订阅请求示例

    以下示例为订阅 Microsoft Teams 中创建或更新的频道消息。

    POST https://graph.microsoft.com/v1.0/subscriptions
    Content-Type: application/json
    {
      "changeType": "created,updated",
      "notificationUrl": "https://webhook.azurewebsites.net/api/resourceNotifications",
      "resource": "/teams/{id}/channels/{id}/messages",
      "includeResourceData": true,
      "encryptionCertificate": "{base64encodedCertificate}",
      "encryptionCertificateId": "{customId}",
      "expirationDateTime": "2019-09-19T11:00:00.0000000Z",
      "clientState": "{secretClientState}"
    }
    

    订阅响应

    HTTP/1.1 201 Created
    Content-Type: application/json
    
    {
      "changeType": "created,updated",
      "notificationUrl": "https://webhook.azurewebsites.net/api/resourceNotifications",
      "resource": "/teams/{id}/channels/{id}/messages",
      "includeResourceData": true,
      "encryptionCertificateId": "{custom ID}",
      "expirationDateTime": "2019-09-19T11:00:00.0000000Z",
      "clientState": "{secret client state}"
    }
    

    订阅生命周期通知

    某些事件可能会干扰现有订阅中的更改通知流。 订阅生命周期通知将通知你要采取的操作,以保持流不中断。 与通知资源实例更改的资源更改通知不同,生命周期通知是关于订阅本身及其在生命周期中的当前状态。

    有关如何接收和响应生命周期通知的详细信息,请参阅 减少缺少的订阅和更改通知

    验证通知的真实性

    应用通常根据更改通知中包含的资源数据运行业务逻辑。 首先验证每个更改通知的真实性非常重要。 否则,第三方可能会以错误的更改通知欺骗你的应用,使其错误地运行其业务逻辑并导致安全事件。

    对于不包含资源数据的基本更改通知,只需根据处理更改通知中所述的 clientState 值对其进行验证。 这是可以接受的,因为可以进行后续调用受信任的 Microsoft Graph 来访问资源数据,因此,任何尝试欺骗的影响都是有限的。

    对于传递资源数据的更改通知,请在处理数据之前执行更全面的验证。

    本节内容:

    更改通知中的验证令牌

    包含资源数据的更改通知包含一个附加属性 validationTokens,该属性包含 Microsoft Graph 生成的 JSON Web 令牌 (JWT) 数组。 Microsoft Graph 为每个不同的应用和租户对生成单个令牌, 值数组中有 一个项。 请记住,更改通知可能包含使用同一 notificationUrl 订阅的各种应用和租户的混合项。

    注意:如果要设置通过 Azure 事件中心传递的更改通知,Microsoft Graph将不会发送验证令牌。 Microsoft Graph不需要验证notificationUrl

    在以下示例中,更改通知包含同一应用和两个不同租户的两个项目,因此 validationTokens 数组包含两个需要验证的令牌。

    {
        "value": [
            {
                "subscriptionId": "76619225-ff6b-4489-96ca-4ef547e78b22",
                "tenantId": "84bd8158-6d4d-4958-8b9f-9d6445542f95",
                "changeType": "created",
                ...
            }
        ],
        "validationTokens": [
            "eyJ0eXAiOiJKV1QiLCJhb...",
            "cGlkYWNyIjoiMiIsImlkc..."
        ]
    }
    

    注意:有关传递更改通知时发送的数据的完整说明,请参阅 changeNotificationCollection

    如何验证

    使用 MSAL 帮助你处理令牌验证,或使用其他平台的第三方库。

    注意以下事项:

    • 确保始终发送 HTTP 202 Accepted 状态代码作为更改通知响应的一部分。
    • 可在验证更改通知前(例如,将更改通知存储在队列中以供后续处理)或在验证更改通知后(如果即时处理了通知)进行响应,即使验证失败也是如此。
    • 接受更改通知可以防止不必要的传递重试,还可以防止任何潜在的恶意行为者查明他们是否通过了验证。 接受无效的更改通知后,你始终可以选择忽略它。

    具体而言,针对 validationTokens 集合中的各个 JWT 令牌进行验证。 如果任何令牌失败,请考虑更改通知可疑并进一步调查。

    使用下列步骤验证令牌和生成令牌的应用程序:

    1. 验证令牌是否未过期。

    2. 验证令牌是否未被篡改且是否由预期的颁发机构颁发,Microsoft 标识平台:

      • 从公用配置终结点获取签名密钥:https://login.microsoftonline.com/common/.well-known/openid-configuration。 此配置由应用缓存一段时间。 请注意,由于签名密钥每日都会轮换,因此配置会经常更新。
      • 使用这些密钥验证 JWT 令牌的签名。

      不接受任何其他机构颁发的令牌。

    3. 验证口令已为订阅更改通知的应用程序颁发。

      下列步骤是 JWT 令牌库中标准验证逻辑的一部分,通常可作为单个函数调用执行。

      • 在与应用程序ID匹配的令牌中验证“受众”。
      • 如果有多个应用收到更改通知,请务必检查是否有多个 ID。
    4. 关键:验证生成令牌的应用程序是否代表着 Microsoft Graph 更改通知的发布者。

      • 在与 0bf30f3b-4a52-48df-9a82-234910c4a086期望值匹配的令牌中检查 appid 属性。
      • 这可确保更改通知不会由不是 Microsoft Graph 的其他应用发送。

    JWT 令牌示例

    下面是验证所需的 JWT 令牌中所含属性的示例。

    {
      // aud is your app's id 
      "aud": "8e460676-ae3f-4b1e-8790-ee0fb5d6148f",                           
      "iss": "https://sts.windows.net/84bd8158-6d4d-4958-8b9f-9d6445542f95/",
      "iat": 1565046813,
      "nbf": 1565046813,
      // Expiration date 
      "exp": 1565075913,                                                        
      "aio": "42FgYKhZ+uOZrHa7p+7tfruauq1HAA==",
      // appid represents the notification publisher and must always be the same value of 0bf30f3b-4a52-48df-9a82-234910c4a086 
      "appid": "0bf30f3b-4a52-48df-9a82-234910c4a086",                          
      "appidacr": "2",
      "idp": "https://sts.windows.net/84bd8158-6d4d-4958-8b9f-9d6445542f95/",
      "tid": "84bd8158-6d4d-4958-8b9f-9d6445542f95",
      "uti": "-KoJHevhgEGnN4kwuixpAA",
      "ver": "1.0"
    }
    

    示例:对验证令牌进行验证

    // add Microsoft.IdentityModel.Protocols.OpenIdConnect and System.IdentityModel.Tokens.Jwt nuget packages to your project
    public async Task<bool> ValidateToken(string token, string tenantId, IEnumerable<string> appIds)
    {
        var configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
        var openIdConfig = await configurationManager.GetConfigurationAsync();
        var handler = new JwtSecurityTokenHandler();
        try
        {
        handler.ValidateToken(token, new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateIssuerSigningKey = true,
            ValidateLifetime = true,
            ValidIssuer = $"https://sts.windows.net/{tenantId}/",
            ValidAudiences = appIds,
            IssuerSigningKeys = openIdConfig.SigningKeys
        }, out _);
        return true;
        }
        catch (Exception ex)
        {
        Trace.TraceError($"{ex.Message}:{ex.StackTrace}");
        return false;
        }
    }
    

    解密更改通知资源数据

    更改通知的 resourceData 属性仅包含资源实例的基本 ID 和类型信息。 encryptedData属性包含由 Microsoft Graph 使用订阅中所提供密钥解密的完整资源数据。 此属性还含有验证和解密所需的数值。 这样做是为了提高通过更改通知访问的客户数据的安全性。 你有责任保护私钥,以确保第三方无法解密客户数据,即使他们设法截获了原始更改通知。

    本节内容:

    管理加密密钥

    1. 使用非对称密钥对获取证书。

      • 你可以对证书进行自签名,因为 Microsoft Graph 不会验证证书颁发者,并且仅使用公钥进行加密。

      • 使用Azure 密钥存储库作为创建、轮换和安全管理证书的解决方案。 确保密钥符合下列条件:

        • 密钥必须属于类型 RSA
        • 密钥大小必须介于 2,048 位和 4,096 位之间
    2. 采用base64编码X.509格式导出证书,且仅包括公钥

    3. 创建订阅时:

      • 使用导入证书的base64基编码内容,在encryptionCertificate属性中提供证书。

      • encryptionCertificateId 属性中提供自己的标识符。

        此标识符能够将你的证书与接收的更改通知匹配,并从证书存储中检索证书。 标识符最长 128 个字符。

    4. 安全地管理私钥,以便更改通知处理代码可以访问私钥来解密资源数据。

    轮换密钥

    若要将私钥泄露的风险降至最低,请定期更改非对称密钥。 请按照以下步骤介绍一对新密钥:

    1. 使用新非对称密钥对获取新证书。 将其用于创建的所有新订阅。

    2. 使用新的证书密钥更新现有订阅。

      • 作为定期续订订阅的一部分执行此操作。
      • 或者,枚举所有订阅并提供密钥。 使用订阅修补程序操作并更新encryptionCertificateencryptionCertificateId属性。
    3. 请记住下列事项:

      • 一段时间后,旧证书仍可能用于加密。 应用程序必须具有访问新旧证书的权限,以能够对内容进行解密。
      • 使用各更改通知中的 encryptionCertificateId 属性来确定要使用的正确密钥。
      • 只有在没有看到最近任何更改通知引用旧证书时,才可以丢弃旧证书。

    解密资源数据

    为优化性能,Microsoft Graph 使用两步加密过程:

    • 它会生成一个使用对称密钥,并用来加密资源数据。
    • 它使用公共非对称密钥(订阅时提供)加密对称密钥,并将之包含在订阅的各更改通知中。

    始终假设更改通知中各项的对称密钥不同。

    若要对资源数据进行解密,应用应使用各更改通知 encryptedContent 下的属性执行反向操作:

    1. 使用 encryptionCertificateId 属性标识要使用的证书。

    2. 使用私钥初始化 RSA 加密组件(如 .NET RSACryptoServiceProvider)。

    3. 解密更改通知中各项的 dataKey 属性中提供的对称密钥。

      使用适用于解密算法的最佳非对称加密填充(OAEP)。

    4. 使用对称密钥计算数据中数值的 HMAC-SHA256 签名。

      将其与 dataSignature中的值进行比较。 如果不匹配,则假定有效负载已被篡改,并且不对其进行解密。

    5. 将对称密钥与高级加密标准(AES)(例如 .NET AesCryptoServiceProvider)结合使用,解密 数据中的内容。

      • 将以下解密参数用于 AES 算法:

        • 填充: PKCS7
        • 密码模式: CBC
      • 通过复制用于解密的对称密钥的前16个字节来设置 "初始化向量"。

    6. 解密值是一个 JSON 字符串,表示更改通知中的资源实例。

    示例:使用加密资源数据解密通知

    下面是一个示例更改通知,其中包含频道消息中 chatMessage 实例的解密属性值。 实例由 @odata.id 值指定。

    {
        "value": [
            {
                "subscriptionId": "76222963-cc7b-42d2-882d-8aaa69cb2ba3",
                "changeType": "created",
                // Other properties typical in a resource change notification
                "resource": "teams('d29828b8-c04d-4e2a-b2f6-07da6982f0f0')/channels('19:f127a8c55ad949d1a238464d22f0f99e@thread.skype')/messages('1565045424600')/replies('1565047490246')",
                "resourceData": {
                    "id": "1565293727947",
                    "@odata.type": "#Microsoft.Graph.ChatMessage",
                    "@odata.id": "teams('88cbc8fc-164b-44f0-b6a6-b59b4a1559d3')/channels('19:8d9da062ec7647d4bb1976126e788b47@thread.tacv2')/messages('1565293727947')/replies('1565293727947')"
                },
                "encryptedContent": {
                    "data": "{encrypted data that produces a full resource}",
            "dataSignature": "<HMAC-SHA256 hash>",
                    "dataKey": "{encrypted symmetric key from Microsoft Graph}",
                    "encryptionCertificateId": "MySelfSignedCert/DDC9651A-D7BC-4D74-86BC-A8923584B0AB",
                    "encryptionCertificateThumbprint": "07293748CC064953A3052FB978C735FB89E61C3D"
                }
            }
        ],
        "validationTokens": [
            "eyJ0eXAiOiJKV1QiLCJhbGciOiJSU..."
        ]
    }
    

    注意:有关传递更改通知时发送的数据的完整说明,请参阅 changeNotificationCollection

    本节包含一些有用的代码片段,它们针对解密的各个阶段使用C# 和NET。

    解密对称密钥

    // Initialize with the private key that matches the encryptionCertificateId.
    RSACryptoServiceProvider rsaProvider = ...;        
    byte[] encryptedSymmetricKey = Convert.FromBase64String(<value from dataKey property>);
    
    // Decrypt using OAEP padding.
    byte[] decryptedSymmetricKey = rsaProvider.Decrypt(encryptedSymmetricKey, fOAEP: true);
    
    // Can now use decryptedSymmetricKey with the AES algorithm.
    

    使用 HMAC-SHA256 比较数据签名

    byte[] decryptedSymmetricKey = <the aes key decrypted in the previous step>;
    byte[] encryptedPayload = <the value from the data property, still encrypted>;
    byte[] expectedSignature = <the value from the dataSignature property>;
    byte[] actualSignature;
    
    using (HMACSHA256 hmac = new HMACSHA256(decryptedSymmetricKey))
    {
        actualSignature = hmac.ComputeHash(encryptedPayload);
    }
    if (actualSignature.SequenceEqual(expectedSignature))
    {
        // Continue with decryption of the encryptedPayload.
    }
    else
    {
        // Do not attempt to decrypt encryptedPayload. Assume notification payload has been tampered with and investigate.
    }
    

    解密资源数据内容

    AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider();
    aesProvider.Key = decryptedSymmetricKey;
    aesProvider.Padding = PaddingMode.PKCS7;
    aesProvider.Mode = CipherMode.CBC;
    
    // Obtain the intialization vector from the symmetric key itself.
    int vectorSize = 16;
    byte[] iv = new byte[vectorSize];
    Array.Copy(decryptedSymmetricKey, iv, vectorSize);
    aesProvider.IV = iv;
    
    byte[] encryptedPayload = Convert.FromBase64String(<value from data property>);
    
    string decryptedResourceData;
    // Decrypt the resource data content.
    using (var decryptor = aesProvider.CreateDecryptor())
    {
      using (MemoryStream msDecrypt = new MemoryStream(encryptedPayload))
      {
          using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
          {
              using (StreamReader srDecrypt = new StreamReader(csDecrypt))
              {
                  decryptedResourceData = srDecrypt.ReadToEnd();
              }
          }
      }
    }
    
    // decryptedResourceData now contains a JSON string that represents the resource.