Microsoft Graph 使应用能够订阅和接收有关资源更改的通知。 本文介绍如何设置 丰富通知,这些通知直接包括通知有效负载中的资源数据。
丰富的通知无需额外的 API 调用即可获取更新的资源,从而更快、更轻松地运行业务逻辑。
支持的资源
丰富通知可用于以下资源。
注意
使用星号 (*) 标记的终结点订阅的丰富通知仅在终结点上 /beta
可用。
资源 | 支持的资源路径 | 限制 |
---|---|---|
Copilot aiInteraction | 特定用户所属的 Copilot AI 交互: copilot/users/{userId}/interactionHistory/getAllEnterpriseInteractions 在组织中协作处理 AI 交互: copilot/interactionHistory/getAllEnterpriseInteractions |
最大订阅配额: |
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 * |
最大订阅配额: |
Teams callTranscript | 组织中的所有脚本: communications/onlineMeetings/getAllTranscripts 特定会议的所有脚本: communications/onlineMeetings/{onlineMeetingId}/transcripts 在由特定用户组织的会议中可用的通话记录: users/{id}/onlineMeetings/getAllTranscripts 在安装了特定 Teams 应用的会议中可用的通话记录: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllTrancripts * |
最大订阅配额: |
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 中的参数请求)。
创建订阅
若要设置丰富通知,请遵循与 基本更改通知相同的步骤,但包含以下必需属性:
-
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 数组包含两个需要验证的令牌。
{
"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 令牌进行验证。 如果任何令牌失败,请考虑更改通知可疑并进一步调查。
按照以下步骤验证令牌以及生成令牌的应用:
验证令牌是否未过期。
验证Microsoft 标识平台是否颁发了令牌,并且令牌未被篡改。
- 从公用配置终结点获取签名密钥:
https://login.microsoftonline.com/common/.well-known/openid-configuration
。 应用可以缓存此配置一段时间。 该配置会频繁更新,因为签名密钥是每天轮换的。 - 使用这些密钥验证 JWT 令牌的签名。
不接受任何其他机构颁发的令牌。
- 从公用配置终结点获取签名密钥:
确认已为应用颁发令牌。
下列步骤是 JWT 令牌库中标准验证逻辑的一部分,通常可作为单个函数调用执行。
- 在与应用程序ID匹配的令牌中验证“受众”。
- 如果有多个应用收到更改通知,请务必检查是否有多个 ID。
验证令牌的
azp
属性是否与 的预期值0bf30f3b-4a52-48df-9a82-234910c4a086
匹配,该值代表 Microsoft Graph 更改通知发布者。
JWT 令牌示例
以下示例显示了验证所需的 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 使用订阅中提供的公钥进行加密。 此属性还含有验证和解密所需的数值。 此加密是为了提高通过更改通知访问的客户数据的安全性。 保护私钥,确保第三方无法解密客户数据,即使他们拦截了原始更改通知。
在本部分中,你将了解以下概念:
管理加密密钥
获取包含一对非对称密钥的证书。
可以使用自签名证书,因为 Microsoft Graph 不会验证证书颁发者,并且仅使用公钥进行加密。
使用 Azure 密钥保管库创建、轮换和安全管理证书。 确保密钥符合下列条件:
- 键的类型必须为
RSA
。 - 密钥大小必须介于 2,048 位和 4,096 位之间。
- 键的类型必须为
以 Base64 编码的 X.509 格式导出证书 ,并仅包含公钥。
创建订阅时:
使用导出证书的 Base64 编码内容,在 encryptionCertificate 属性中提供证书。
在 encryptionCertificateId 属性中提供自己的标识符。
此标识符能够将你的证书与接收的更改通知匹配,并从证书存储中检索证书。 标识符最长 128 个字符。
安全地管理私钥,以便更改通知处理代码可以访问私钥来解密资源数据。
轮换密钥
定期更改非对称密钥,以最大程度地降低私钥泄露的风险。 请按照以下步骤介绍一对新密钥:
使用新非对称密钥对获取新证书。 将其用于创建的所有新订阅。
使用新的证书密钥更新现有订阅。
- 使此更新成为定期订阅续订的一部分。
- 或者,枚举所有订阅并提供密钥。 使用订阅修补程序操作并更新encryptionCertificate和encryptionCertificateId属性。
请记住以下原则:
- 旧证书可能仍用于加密一段时间。 应用程序必须具有访问新旧证书的权限,以能够对内容进行解密。
- 使用各更改通知中的 encryptionCertificateId 属性来确定要使用的正确密钥。
- 仅当看不到引用旧证书的最近更改通知时,才放弃旧证书。
解密资源数据
为优化性能,Microsoft Graph 使用两步加密过程:
- 它生成一个一次性对称密钥,并使用它来加密资源数据。
- 它使用公共非对称密钥(订阅时提供)加密对称密钥,并将之包含在订阅的各更改通知中。
假设更改通知中的每个项的对称密钥都不同。
若要对资源数据进行解密,应用应使用各更改通知 encryptedContent 下的属性执行反向操作:
使用 encryptionCertificateId 属性标识正确的证书。
使用私钥初始化 RSA 加密组件。 初始化 RSA 组件的一种简单方法是将 RSACertificateExtensions.GetRSAPrivateKey (X509Certificate2) 方法 与 X509Certificate2 实例结合使用,该实例包含 管理加密密钥中所述的私钥。
使用私钥解密更改通知中每个项的 dataKey 属性中的对称密钥。 使用最佳非对称加密填充 (OAEP) 作为解密算法。
使用对称密钥计算 数据中值的 HMAC-SHA256 签名。 将其与 dataSignature中的值进行比较。 如果不匹配,则假定有效负载被篡改,并且不要解密它。
使用具有高级加密的对称密钥解密数据 proeprty 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.
相关内容
- 配置 订阅资源类型。