通过


使用资源数据设置Microsoft Graph 更改通知

Microsoft Graph 使应用能够订阅和接收有关资源更改的通知。 本文介绍如何设置 丰富通知,这些通知直接包括通知有效负载中的资源数据。

丰富的通知无需额外的 API 调用即可获取更新的资源,从而更快、更轻松地运行业务逻辑。

支持的资源

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

注意

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

资源 支持的资源路径 限制
Copilot aiInteraction 特定用户所属的 Copilot AI 交互: copilot/users/{userId}/interactionHistory/getAllEnterpriseInteractions

在组织中协作处理 AI 交互: copilot/interactionHistory/getAllEnterpriseInteractions
最大订阅配额:
  • 每个应用和租户组合 (,用于跟踪跨租户的 AI 交互的订阅) :1
  • 每个应用和用户组合 (用于跟踪 AI 交互的订阅,特定用户是) 的一部分: 1
  • 跟踪 AI 交互的订阅的每个用户 (特定用户是) 的一部分:10 个订阅。
  • 每个组织:总共 10,000 个订阅。
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

对租户中所有团队成员身份的更改: /teams/getAllMembers

更改特定团队下的所有频道的成员身份: /teams/{id}/channels/getAllMembers

对整个租户中所有通道的成员身份的更改: /teams/getAllChannels/getAllMembers

对特定聊天中成员身份的更改: /chats/{id}/members

更改所有 Teams 聊天的成员身份: /chats/getAllMembers
不支持使用 $select 仅返回所选属性。 丰富通知包含已更改实例的所有属性。
Teams onlineMeeting * 对联机会议的更改: /communications/onlineMeetings(joinWebUrl='{encodedJoinWebUrl}')/meetingCallEvents * 不支持使用 $select 仅返回所选属性。 丰富通知包含已更改实例的所有属性。 每个联机会议每个应用程序允许一个订阅。 有关详细信息,请参阅 获取Microsoft Teams 会议呼叫事件更新的更改通知
Teams 状态 对单个用户状态的更改: /communications/presences/{id}

对多个用户状态的更改: /communications/presences?$filter=id in ({id},{id}...)
多用户状态的订阅限制为 650 个不同的用户。 不支持使用 $select 仅返回所选属性。 丰富通知包含已更改实例的所有属性。 每个委派用户允许每个应用程序一个订阅。 有关详细信息,请参阅 在 Microsoft Teams 中获取状态更新的更改通知
Teams 团队 对租户中任何团队的更改: /teams

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

通知负载中的资源数据

丰富通知包括包含以下详细信息的资源数据:

  • 已更改的资源实例的 ID 和类型,位于 resourceData 属性中。
  • 资源实例的所有属性值(在订阅中指定的加密)在 encryptedContent 属性中找到。
  • 资源的特定属性,具体取决于资源,或者( $select 如果使用订阅 的资源 URL 中的参数请求)。

通知的应用程序配置

在创建包含资源数据的订阅之前,请通过设置 appRoleAssignmentRequired 属性,为表示租户-应用对的服务主体对象配置应用程序访问权限,如下所示:

如果未满足这两个条件,则通知有效负载将包含 null验证令牌

创建订阅

若要设置丰富通知,请遵循与 基本更改通知相同的步骤,但包含以下必需属性:

  • includeResourceData:将此设置为 true 以请求资源数据。
  • encryptionCertificate:提供 graph Microsoft用于加密资源数据的公钥。 有关详细信息,请参阅 从更改通知解密资源数据
  • encryptionCertificateId:提供证书的标识符,以将通知与正确的解密密钥匹配。

通知终结点验证中所述,验证两个终结点。 如果对两个终结点使用相同的 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": "{customId}",
  "expirationDateTime": "2019-09-19T11:00:00.0000000Z",
  "clientState": "{secretClientState}"
}

订阅生命周期通知

事件可能会中断订阅中的更改通知流。 生命周期通知告知你要采取哪些操作来保持流不中断。 与资源更改通知不同,生命周期通知侧重于订阅的状态。

若要了解详细信息,请参阅 减少缺少的订阅和更改通知

验证通知的真实性

在处理更改通知之前,请始终验证其真实性。 这可以防止应用使用来自第三方的虚假通知触发不正确的业务逻辑。

对于基本通知,请使用 clientState 值验证它们,如 处理更改通知中所述。 对于丰富的通知,请执行其他验证步骤。

更改通知中的验证令牌

丰富通知包括 validationTokens 属性,该属性包含 JSON Web 令牌 数组 (JWT) 。 每个令牌对于应用和租户对是唯一的。 更改通知可能包含使用同一 notificationUrl 订阅的各种应用和租户的混合项。

注意

Microsoft Graph 不会为通过 Azure 事件中心 传递的更改通知发送验证令牌,因为订阅服务不需要验证事件中心的 notificationUrl

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

提示

null validationTokens 的值指示由于应用配置不正确,Microsoft Graph 无法加密资源数据。 查看 “通知的应用程序配置 ”部分以解决此问题。

{
    "value": [
        {
            "subscriptionId": "76619225-ff6b-4489-96ca-4ef547e78b22",
            "tenantId": "aaaabbbb-0000-cccc-1111-dddd2222eeee",
            "changeType": "created",
            ...
        },
        {
            "subscriptionId": "5cfe2387-163c-4006-81bb-1b5e1e060afe",
            "tenantId": "bbbbcccc-1111-dddd-2222-eeee3333ffff",
            "changeType": "created",
            ...
        }
    ],
    "validationTokens": [
        "eyJ0eXAiOiJKV1QiLCJhb...",
        "cGlkYWNyIjoiMiIsImlkc..."
    ]
}

更改通知对象位于 changeNotificationCollection 资源类型的结构中。

如何验证

使用 Microsoft身份验证库 (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。 用于表示调用方应用程序的声明取决于令牌的版本:

    • 对于 v1.0 令牌 (ver = "1.0") ,调用方标识由 appid 声明表示。
    • 对于) (ver = "2.0" v2.0 令牌,调用方标识由 azp 声明表示。

    重要

    未能验证适当的声明可能会导致接受来自不受信任的发布者的通知。

JWT 令牌示例

以下示例显示了验证所需的 v2.0 JWT 令牌中的属性。

{
  // aud is your app's id
  "aud": "925bff9f-f6e2-4a69-b858-f71ea2b9b6d0",
  "iss": "https://login.microsoftonline.com/9f4ebab6-520d-49c0-85cc-7b25c78d4a93/v2.0",
  "iat": 1624649764,
  "nbf": 1624649764,
  "exp": 1624736464,
  "aio": "E2ZgYGjnuFglnX7mtjJzwR5lYaWvAA==",
  // azp represents the notification publisher and must always be the same value of 0bf30f3b-4a52-48df-9a82-234910c4a086
  "azp": "0bf30f3b-4a52-48df-9a82-234910c4a086",
  "azpacr": "2",
  "oid": "1e7d79fa-7893-4d50-bdde-164260d9c5ba",
  "rh": "0.AX0AtrpOnw1SwEmFzHslx41KkzsP8wtSSt9ImoIjSRDEoIZ9AAA.",
  "sub": "1e7d79fa-7893-4d50-bdde-164260d9c5ba",
  "tid": "9f4ebab6-520d-49c0-85cc-7b25c78d4a93",
  "uti": "mIB4QKCeZE6hK71XUHJ3AA",
  "ver": "2.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 加密组件。 初始化 RSA 组件的一种简单方法是将 RSACertificateExtensions.GetRSAPrivateKey (X509Certificate2) 方法X509Certificate2 实例结合使用,该实例包含 管理加密密钥中所述的私钥。

  3. 使用私钥解密更改通知中每个项的 dataKey 属性中的对称密钥。 使用最佳非对称加密填充 (OAEP) 作为解密算法。

  4. 使用对称密钥计算 数据中值的 HMAC-SHA256 签名。 将其与 dataSignature中的值进行比较。 如果不匹配,则假定有效负载被篡改,并且不要解密它。

  5. 使用具有高级加密Standard (AES) (例如 .NET Aes)的对称密钥解密数据属性。

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

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

解密的数据将是表示资源的 JSON 字符串。

示例:解密资源数据

以下 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.
X509Certificate2 certificate = <instance of X509Certificate2 matching the encryptionCertificateId property>;
RSA rsa = certificate.GetRSAPrivateKey();
byte[] encryptedSymmetricKey = Convert.FromBase64String(<value from dataKey property>);

// Decrypt using OAEP padding.
byte[] decryptedSymmetricKey = rsa.Decrypt(encryptedSymmetricKey, RSAEncryptionPadding.OaepSHA1);

// 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.
}

解密资源数据内容

Aes aesProvider = Aes.Create();
aesProvider.Key = decryptedSymmetricKey;
aesProvider.Padding = PaddingMode.PKCS7;
aesProvider.Mode = CipherMode.CBC;

// Obtain the initialization 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.