这篇我们介绍Microsoft Graph中的webhooks。
Microsoft Graph for Office 365 - 用例:Webhooks-LMLPHP

概述

Webhooks为应用程序开发者提供了一种可以在Microsoft Graph的数据更新时得到提醒的方法。它允许第三方应用程序跟Microsoft服务中的数据进行连接,提供了更丰富,更具吸引力的用户体验。例如,一个webhook可以配置为监听某个销售人员的收件箱中的邮件,并通过任何会话在CRM系统中提供完整的帐户信息,进而自动在CRM系统中更新联系人。这个过程不需要用户更改上下文,事实上这向更广泛的组织提供了可见性。

另一个Microsoft Graph提供的工具叫做Delta Hooks,它使开发者可以轮询特定资源的更新。而对于我们来说,使用哪个工具取决于我们的使用场景和我们想要监听的资源。

目前应用程序可以订阅以下资源的更新:

  • 消息
  • 事件
  • 联系人
  • 用户
  • 组会话
  • OneDrive上的共享内容,包括跟SharePoint网站关联的网盘
  • 用户个人OneDrive文件夹
  • 安全警报

一个webhook有两个主要组件。一个创建和管理订阅的进程,一个接收和处理通知的进程。这两个组件相互依赖,需要共享一些信息,因为作为这样一个真实的生产系统,它需要一个持久性的系统来确保生产系统能够平稳运行,避免出现如多个活动订阅指向相同资源的问题。

注册一个订阅的流程如下:

  • 创建一个新订阅至少需要提供:
    • 要订阅的资源
    • 要通知的变更类型
    • 用于发送通知POST请求的URL
    • 订阅的过期时间
    • 用于验证收到的后续消息的客户端状态
  • 将订阅POST到Microsoft Graph
  • 如果请求有效,Graph会向通知URL发送一个验证令牌
  • 客户端会在10秒内将验证令牌返回Graph
  • Graph将包含新创建订阅的应答返回到客户端
  • 将订阅保存到一个数据存储来验证收到的通知

示例解决方案

本篇的示例代码是一个ASP.NET Core 2.1的Web API,但是应用程序仍然使用之前注册的那个。为了避免示例过于复杂,我们不使用真实数据库进行持久化存储,只是将当前订阅的通知列表保存在一个内存对象中。

示例中SubscriptionsController的部分代码:

[HttpGet("{upn}")]
public async Task<ActionResult<Subscription>> Get(string upn)
{
    var result = _subscriptionRepository.LoadByUpn(upn);
    if (result != null && result.ExpirationDateTime > DateTime.Now)
    {
        return result;
    }
    string clientState = Guid.NewGuid().ToString("d");
    var request = new Subscription
    {
        ChangeType = "created",
        ExpirationDateTime = DateTime.Now.AddDays(2),
        ClientState = clientState,
        Resource = $"users/{upn}/events",
        NotificationUrl = _notificationUrl.Url
    };
    var response = await _graphClient.PostAsJsonAsync(_subscriptionsResource, request);
    string responseBody = await response.Content.ReadAsStringAsync();

    if (!response.IsSuccessStatusCode)
    {
        Console.WriteLine(response.ReasonPhrase);
        var error = new ObjectResult(responseBody);
        error.StatusCode = 500;
        return error;
    }
    Subscription subscription = JsonConvert.DeserializeObject<Subscription>(responseBody);
    _subscriptionRepository.Save(subscription);
    return subscription;
}

关于代码有一些要注意的地方。当首次收到请求时,会对现有的订阅集合进行查询以验证该用户还没有活动的订阅。对于每个新订阅都会创建一个客户端状态,用于在收到通知时对这些通知是否属于该系统创建的订阅之一进行验证。Graph中成功创建订阅后,订阅会保存到库中。

在接收和处理通知时:

  • Microsoft Graph发送通知对象数组的POST请求到订阅创建时提供的通知URL
    • 每个通知至少包含:
    • 订阅的的Id
    • 触发通知的资源
    • 触发通知的类型
    • 通知的资源类型
    • 订阅的客户端状态
  • 返回HTTP状态码202 - 允许的应答。如果收到非200类的应答,Graph会尝试重新发送通知一些次。

对于每个通知:

  • 加载响应的订阅
  • 验证提供的和保留的客户端状态是否匹配
  • 从Microsoft Graph加载资源
  • 按照自己的用例执行任何自定义逻辑

示例中NotificationsController的部分代码:

[HttpPost]
public async Task<ActionResult> Listen([FromQuery] string validationToken)
{
    if (!string.IsNullOrEmpty(validationToken))
    {
        return Content(validationToken, "plain/text");
    }
    try
    {
        // Read the post body directly as we can't mix optional FromBody and FromQuery parameters
        var postBody = await Request.GetBodyAsync<Notifications>();
        foreach (var item in postBody.value)
        {
            await ProcessEventNotification(item);
        }
    }
    catch (Exception)
    {
        // Just ignore exceptions
    }
    // Send a 202 so MicrosoftGraph knows we processed the notification
    return new StatusCodeResult(202);
}

通知控制器可以处理提供有效令牌的注册请求和通知请求。在处理通知请求时,代码总会返回HTTP状态202应答以防止Graph重试通知请求。通知请求可以一次打包多个通知。基于这点,我们需要考虑处理一个通知请求两次或者失败可能带来的影响。如果在这两种情况下容易出问题,我们应该将收到的通知保存到数据库。这样我们就可以跟踪这些消息触发过程中成功或失败的状态。

权限

需要Calendar.Read应用程序权限。

其他工具

当在本地运行Web API项目时,它会在http://localhost:5000绑定并监听。由于这个URL在Microsoft Graph端不可访问,因此有必要使用代理工具如ngrok。这样可以让本地运行的代码有一个互联网地址。

示例代码的地址如下。
https://github.com/microsoftgraph/dotnetcore-console-sample/tree/master/day28-webhooks

08-09 05:51