Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Microsoft Graph позволяет приложениям подписываться на ресурсы и получать уведомления об изменениях в ресурсах. В этой статье объясняется, как настроить расширенные уведомления, которые включают данные ресурсов непосредственно в полезные данные уведомления.
Расширенные уведомления устраняют необходимость в дополнительных вызовах API для получения обновленных ресурсов, что ускоряет и упрощает выполнение бизнес-логики.
Поддерживаемые ресурсы
Расширенные уведомления доступны для следующих ресурсов.
Примечание.
Расширенные уведомления для подписок на конечные точки, помеченные звездочкой (*), доступны только в конечной точке /beta .
| Ресурс | Поддерживаемые пути к ресурсам | Ограничения |
|---|---|---|
| Copilot aiInteraction | Взаимодействие с ИИ Copilot, частью чего является конкретный пользователь: copilot/users/{userId}/interactionHistory/getAllEnterpriseInteractions Взаимодействие Copilot 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. |
| Вызовы TeamsRecording | Все записи в организации: communications/onlineMeetings/getAllRecordings Все записи для определенного собрания: communications/onlineMeetings/{onlineMeetingId}/recordings Запись звонка, которая становится доступной на собрании, организованном определенным пользователем: users/{id}/onlineMeetings/getAllRecordings Запись звонка, которая становится доступной на собрании, где установлено определенное приложение Teams: appCatalogs/teamsApps/{id}/installedToOnlineMeetings/getAllRecordings * |
Квоты максимальной подписки: |
| Вызов TeamsTranscript | Все расшифровки в организации: 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} |
- |
| chatMessage Teams | Изменения в сообщениях чата во всех каналах во всех командах: /teams/getAllMessagesИзменения в сообщениях чата в определенном канале: /teams/{id}/channels/{id}/messagesИзменения в сообщениях чата во всех чатах: /chats/getAllMessagesИзменения в сообщениях чата в определенном чате: /chats/{id}/messagesИзменения в сообщениях чата во всех чатах, в которые входит конкретный пользователь: /users/{id}/chats/getAllMessages |
Не поддерживает использование $select для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра. |
| conversationMember в Teams | Изменения членства в определенной команде: /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. |
| presence в Teams | Изменения в присутствии одного пользователя: /communications/presences/{id} Изменения в присутствии нескольких пользователей: /communications/presences?$filter=id in ({id},{id}...) |
Подписка на присутствие нескольких пользователей ограничена 650 отдельными пользователями. Не поддерживает использование $select для возврата только выбранных свойств. Расширенное уведомление состоит из всех свойств измененного экземпляра. Допускается одна подписка на одно приложение на каждого делегированного пользователя. Дополнительные сведения см. в разделе Получение уведомлений об изменениях для обновлений присутствия в Microsoft Teams. |
| Команда Teams | Изменения в любой команде в клиенте: /teamsИзменения в конкретной команде: /teams/{id} |
- |
Сведения ресурсов в полезных данных уведомлений
Расширенные уведомления включают данные ресурсов со следующими сведениями:
- Идентификатор и тип измененного экземпляра ресурса, найденные в свойстве resourceData .
- Все значения свойств экземпляра ресурса, зашифрованные, как указано в подписке, находятся в свойстве encryptedContent .
- Конкретные свойства ресурса в зависимости от ресурса или запроса с помощью
$selectпараметра в URL-адресе ресурса подписки.
Создание подписки
Чтобы настроить расширенные уведомления, выполните те же действия, что и базовые уведомления об изменениях, но укажите следующие обязательные свойства:
-
includeResourceData: задайте значение для
trueзапроса данных ресурсов. - encryptionCertificate. Укажите открытый ключ, который Microsoft Graph использует для шифрования данных ресурса. Дополнительные сведения см. в статье Расшифровка данных ресурсов из уведомлений об изменениях.
- 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 (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.
Способ проверки
Используйте библиотеку проверки подлинности Майкрософт (MSAL) или стороннюю библиотеку для проверки маркеров. Выполните следующие действия:
Помните о следующих принципах:
- Немедленно ответьте на уведомление кодом
HTTP 202 Acceptedсостояния. - Ответьте перед проверкой уведомления об изменениях, даже если проверка завершается неудачей позже. Отвечайте сразу после получения уведомления об изменениях, независимо от того, храните ли вы уведомления в очередях для последующей обработки или обработки на лету.
- Принятие уведомления об изменениях и реагирование на них предотвращает ненужные повторные попытки доставки и скрывает результаты проверки от потенциальных злоумышленников. Вы всегда можете игнорировать уведомление о недопустимом изменении после его получения.
В частности, выполняйте проверку каждого маркера JWT в коллекции validationTokens. Если любой из маркеров не прошел проверку, считайте уведомление об изменении подозрительным и выполните дальнейшее исследование.
Выполните следующие действия, чтобы проверить маркеры и приложения, которые их создают:
Убедитесь, что срок действия маркера не истек.
Убедитесь, что платформа удостоверений Майкрософт выдал маркер и что он не был изменен.
- Получите ключи подписи от общей конечной точки конфигурации:
https://login.microsoftonline.com/common/.well-known/openid-configuration. Приложение может кэшировать эту конфигурацию в течение некоторого времени. Конфигурация часто обновляется, так как ключи подписывания сменяются ежедневно. - Проверьте подпись маркера JWT, использующего эти ключи.
Не принимайте маркеры, выданные каким-либо другим центром.
- Получите ключи подписи от общей конечной точки конфигурации:
Убедитесь, что маркер был выдан для вашего приложения.
Следующие действия являются частью стандартной логики проверки в библиотеках маркеров JWT, и обычно их можно выполнять в виде вызова одной функции.
- Убедитесь, что "аудитория" в маркере совпадает с идентификатором вашего приложения.
- Если уведомления об изменениях получают несколько приложений, выполните проверку по нескольким идентификаторам.
Убедитесь, что свойство маркера
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 в уведомлении об изменениях содержит базовые сведения об идентификаторе и типе экземпляра ресурса. Свойство encryptedData содержит полные данные ресурса, зашифрованные Microsoft Graph с открытым ключом, указанным в подписке. Это свойство также содержит значения, необходимые для проверки и расшифровки. Это шифрование выполняется для повышения безопасности данных клиентов, к которые обращаются через уведомления об изменениях. Защитите закрытый ключ, чтобы третья сторона не могла расшифровать данные клиента, даже если она перехватывает исходные уведомления об изменениях.
В этом разделе вы узнаете о следующих понятиях:
Управление ключами шифрования
Получите сертификат с парой асимметричных ключей.
Вы можете использовать самозаверяющий сертификат, так как Microsoft Graph не проверяет издателя сертификата и использует открытый ключ только для шифрования.
Используйте Azure Key Vault для создания, смены сертификатов и безопасного управления ими. Убедитесь, что ключи удовлетворяют следующим условиям:
- Ключ должен иметь тип
RSA. - Размер ключа должен находиться в диапазоне от 2048 до 4096 бит.
- Ключ должен иметь тип
Экспортируйте сертификат в формате X.509 в кодировке Base64 и включите только открытый ключ.
При создании подписки:
Укажите сертификат в свойстве encryptionCertificate , используя содержимое в кодировке Base64, в которое был экспортирован сертификат.
Укажите ваш собственный идентификатор в свойстве encryptionCertificateId.
Этот идентификатор позволяет сопоставлять сертификаты с получаемыми уведомлениями об изменениях, а также получать сертификаты из хранилища сертификатов. Длина идентификатора не должна превышать 128 символов.
Обеспечьте защиту закрытого ключа, чтобы ваш код обработки уведомлений об изменениях мог обращаться к закрытому ключу для расшифровки данных ресурсов.
Ротация ключей
Периодически изменяйте асимметричные ключи, чтобы свести к минимуму риск компрометации закрытого ключа. Чтобы добавить новую пару ключей, выполните следующие действия:
Получите новый сертификат с новой парой асимметричных ключей. Используйте его для всех создаваемых подписок.
Обновите существующие подписки с использованием нового ключа сертификата.
- Сделайте это обновление частью регулярного продления подписки.
- Или перечислите все подписки и укажите ключ. Используйте операцию PATCH для подписки и обновите свойства encryptionCertificate и encryptionCertificateId.
Помните о следующих принципах.
- Старый сертификат может по-прежнему использоваться для шифрования в течение некоторого времени. Чтобы расшифровать контент, у вашего приложения должен быть доступ как к старым, так и к новым сертификатам.
- Используйте свойство encryptionCertificateId в каждом уведомлении об изменении, чтобы определить правильный ключ для использования.
- Отмена старого сертификата только в том случае, если вы не видите последних уведомлений об изменениях, ссылающихся на него.
Расшифровка данных ресурсов
Microsoft Graph использует двухэтапный процесс шифрования с целью оптимизации работы:
- Он создает одноразовый симметричный ключ и использует его для шифрования данных ресурсов.
- Используется открытый асимметричный ключ (указанный при подписке), чтобы зашифровать симметричный ключ и добавить его в каждое уведомление об изменении этой подписки.
Предположим, симметричный ключ отличается для каждого элемента в уведомлении об изменении.
Чтобы расшифровать данные ресурсов, приложение должно выполнить обратные действия, используя свойства в разделе encryptedContent в каждом уведомлении об изменении:
Определите правильный сертификат с помощью свойства encryptionCertificateId .
Инициализация криптографического компонента RSA с помощью закрытого ключа. Простой способ инициализации компонента RSA — использовать метод RSACertificateExtensions.GetRSAPrivateKey(X509Certificate2) с экземпляром X509Certificate2 , который содержит закрытый ключ, описанный в разделе Управление ключами шифрования.
Расшифруйте симметричный ключ в свойстве dataKey каждого элемента в уведомлении об изменениях с помощью закрытого ключа. Используйте оптимальное заполнение асимметричного шифрования (OAEP) в качестве алгоритма расшифровки.
Используйте симметричный ключ для вычисления сигнатуры HMAC-SHA256 для значения в данных. Сравните его со значением в объекте dataSignature. Если они не совпадают, предположим, что полезные данные были изменены и не расшифровывайте их.
Расшифруйте свойство данных с помощью симметричного ключа с помощью расширенного Standard шифрования (AES), например Aes .NET.
Используйте следующие параметры расшифровки для алгоритма 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.
Связанные материалы
- Настройте тип ресурса подписки.