Microsoft Graph Toolkit 代理 Provider

本篇我们介绍一下代理Provider,即Proxy Provider。

什么是代理Provider?
正如其名,代理Provider使我们能够在Microsoft Graph Toolkit中使用代理API,而不是直接调用Microsoft Graph API,简单来说就是自己封装一下API作为后端进行调用,提供更高的灵活性。例如:
https://graph.microsoft.com/v1.0/me ==> https://YourAPI.com/api/GraphProxy/v1.0/me

构建代理Provider应用程序向导

在Azure注册应用程序那些内容不再赘述了,主要讲解一下如何写代码,本例我们将以一个ASP.NET MVC应用程序作为示例。
新建一个ASP.NET MVC应用程序,首先跟着Microsoft Learn上关于构建第一个Microsoft Graph的ASP.NET应用程序的内容做,得到一个集成了Microsoft Graph调用的项目。

  1. 在App_Start文件夹中新建一个类WebApiConfig。
public static class WebApiConfig {
    public static void Register(HttpConfiguration config)
    {
        config.MapHttpAttributeRoutes();
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}
  1. 在App_Start目录下的Startup.Auth.cs文件中,将函数替换为以下代码。
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification) {
    var idClient = ConfidentialClientApplicationBuilder.Create(appId)
        .WithRedirectUri(redirectUri)
        .WithClientSecret(appSecret)
        .Build();
    var signedInUser = new ClaimsPrincipal(notification.AuthenticationTicket.Identity);
    var tokenStore = new SessionTokenStore(idClient.UserTokenCache, HttpContext.Current, signedInUser);
    try   
    {
        string[] scopes = graphScopes.Split(' ');
        var result = await idClient.AcquireTokenByAuthorizationCode(scopes, notification.Code).ExecuteAsync();
        var userDetails = await GraphHelper.GetUserDetailsAsync(result.AccessToken);
        var cachedUser = new CachedUser()
        {
            DisplayName = userDetails.DisplayName,
            Email = string.IsNullOrEmpty(userDetails.Mail) ?
            userDetails.UserPrincipalName : userDetails.Mail,
            Avatar = string.Empty
        };
        tokenStore.SaveUserDetails(cachedUser);
    }
    catch (MsalException ex)
    {
        string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
        notification.HandleResponse();
        notification.Response.Redirect($"/Home/Error?message={message}&debug={ex.Message}");
    }
    catch (Microsoft.Graph.ServiceException ex)
    {
        string message = "GetUserDetailsAsync threw an exception";
        notification.HandleResponse();
        notification.Response.Redirect($"/Home/Error?message={message}&debug={ex.Message}");
    }
}
  1. 在Controllers目录下,将CalendarController类替换为以下代码。
public class CalendarController : BaseController    
{
    // GET: Calendar
    [Authorize]
    public ActionResult Index()
    {
        return View();
    }
}
  1. 继续添加一个GraphProxyController,用于管理HTTP请求、执行认证、返回结果。
[RoutePrefix("api/GraphProxy")]
public class GraphProxyController : ApiController {
    [HttpGet]
    [Route("{*all}")]
    public async Task<HttpResponseMessage> GetAsync(string all)
    {
        return await ProcessRequestAsync("GET", all, null).ConfigureAwait(false);
    }

    private async Task<HttpResponseMessage> ProcessRequestAsync(string method, string all, object content)
    {
        var graphClient = GraphHelper.GetAuthenticatedClient();
        var request = new BaseRequest(GetURL(all, graphClient), graphClient, null)
        {
            Method = method,
            ContentType = HttpContext.Current.Request.ContentType,
        };
        var neededHeaders = Request.Headers.Where(h => h.Key.ToLower() == "if-match").ToList();
        if (neededHeaders.Count() > 0)
        {
            foreach (var header in neededHeaders)
            {
                request.Headers.Add(new HeaderOption(header.Key, string.Join(",", header.Value)));
            }
        }

        var contentType = "application/json";
        try
        {
            using (var response = await request.SendRequestAsync(content, CancellationToken.None, HttpCompletionOption.ResponseContentRead).ConfigureAwait(false))
            {
                response.Content.Headers.TryGetValues("content-type", out var contentTypes);
                contentType = contentTypes?.FirstOrDefault() ?? contentType;
                var byteArrayContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
                return ReturnHttpResponseMessage(HttpStatusCode.OK, contentType, new ByteArrayContent(byteArrayContent));
            }
        }
        catch (ServiceException ex)
        {
            return ReturnHttpResponseMessage(ex.StatusCode, contentType, new StringContent(ex.Error.ToString()));
        }
    }

    private static HttpResponseMessage ReturnHttpResponseMessage(HttpStatusCode httpStatusCode, string contentType, HttpContent httpContent)
    {
        var httpResponseMessage = new HttpResponseMessage(httpStatusCode)
        {
            Content = httpContent
        };
        try
        {
            httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
        }
        catch
        {
            httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
        }

        return httpResponseMessage;
    }

    private string GetURL(string all, GraphServiceClient graphClient)
    {
        var urlStringBuilder = new StringBuilder();
        var qs = HttpContext.Current.Request.QueryString;
        if (qs.Count > 0)
        {
            foreach (string key in qs.Keys)
            {
                if (string.IsNullOrWhiteSpace(key)) continue;
                string[] values = qs.GetValues(key);
                if (values == null) continue;
                foreach (string value in values)
                {
                    urlStringBuilder.Append(urlStringBuilder.Length == 0 ? "?" : "&");
                    urlStringBuilder.AppendFormat("{0}={1}", Uri.EscapeDataString(key), Uri.EscapeDataString(value));
                }
            }
            urlStringBuilder.Insert(0, "?");
        }
        urlStringBuilder.Insert(0, $"{GetBaseUrlWithoutVersion(graphClient)}/{all}");
        return urlStringBuilder.ToString();
    }

    private string GetBaseUrlWithoutVersion(GraphServiceClient graphClient)
    {
        var baseUrl = graphClient.BaseUrl;
        var index = baseUrl.LastIndexOf('/');
        return baseUrl.Substring(0, index);
    }
}
  1. 在Helpers文件夹下的GraphHelper类中。
public static async Task<Microsoft.Graph.User> GetUserDetailsAsync(string accessToken)
{
    var graphClient = new GraphServiceClient(
       new DelegateAuthenticationProvider(
          async (requestMessage) =>
          {
              requestMessage.Headers.Authorization =
              new AuthenticationHeaderValue("Bearer", accessToken);
          }));
    return await graphClient.Me.Request().GetAsync();
}
  1. 删除Models文件夹中的CachedUser.cs。
  2. 在TokenStorage文件夹的SessionTokenStore.cs文件中,创建一个CachedUser类用来将用户存储到缓存中。
public class CachedUser {
    public string DisplayName { get; set; }
    public string Email { get; set; }
    public string Avatar { get; set; }
}

至此后端代码就搞定了,接下来我们对前台做一些改变,添加Microsoft Graph Toolkit组件并调用代理API。

  1. 编辑Views\Calendar\下的index.cshtml,改为如下内容。
<h1>Calendar</h1>
<mgt-agenda group-by-day></mgt-agenda>
  1. 编辑Views\Home\下的index.cshtml,改为如下内容。
@{
    ViewBag.Current = "Home";
}

<div class="jumbotron">
    <h1>ASP.NET Microsoft Graph Toolkit Tutorial</h1>
    <p class="lead">This sample app shows how to use the Microsoft Graph Toolkit with ASP.NET</p>
    @if (Request.IsAuthenticated)
    {
        <h4>Welcome @ViewBag.User.DisplayName!</h4>
        <p>Use the navigation bar at the top of the page to get started.</p>
        <h4>People</h4>
        <p>These are the people you work with the most</p>
        <mgt-people>
            <template>
                <div data-for="person in people">
                     <mgt-person person-details="{{person}}" view="twolines" fetch-image person-card="hover">
                </div>
            </template>
        </mgt-people>
    }
    else
    {
        @Html.ActionLink("Click here to sign in", "SignIn", "Account", new { area = "" }, new { @class = "btn btn-primary btn-large" })
    }
</div>
  1. 最后,编辑Views/Shared/下的_Layout.cshtml,改为以下内容:
    在head标签部分,添加以下内容:
<script src="https://unpkg.com/@Html.Raw("@")microsoft/mgt/dist/bundle/mgt-loader.js"></script>
<script>
    const provider = new mgt.ProxyProvider("/api/GraphProxy");
    provider.login = () => window.location.href = '@Url.Action("SignIn", "Account")';
    provider.logout = () => window.location.href = '@Url.Action("SignOut", "Account")';
    mgt.Providers.globalProvider = provider;       
</script>
<style>
    mgt-login {
        --color: white;
        --padding: 8px;
        --background-color--hover: transparent;
    }
</style>

在body标签部分,替换为以下内容:

<body>
    <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
        <div class="container">
            @Html.ActionLink("ASP.NET Microsoft Graph Toolkit", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse"
                    aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarCollapse">
                <ul class="navbar-nav mr-auto">
                    <li class="nav-item">
                        @Html.ActionLink("Home", "Index", "Home", new { area = "" },
                            new { @class = ViewBag.Current == "Home" ? "nav-link active" : "nav-link" })
                    </li>
                    @if (Request.IsAuthenticated)
                    {
                        <li class="nav-item" data-turbolinks="false">
                            @Html.ActionLink("Calendar", "Index", "Calendar", new { area = "" },
                                new { @class = ViewBag.Current == "Calendar" ? "nav-link active" : "nav-link" })
                        </li>      
                    }
                </ul>
                <ul class="navbar-nav justify-content-end">
                    <li class="nav-item">
                        <a class="nav-link" href="https://aka.ms/mgt-docs" target="_blank">
                            <i class="fas fa-external-link-alt mr-1"></i>Docs
                        </a>
                    </li>
                    <li class="nav-item">
                        <mgt-login></mgt-login>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
    <main role="main" class="container">
        @foreach (var alert in alerts)
        {
            <div class="alert alert-danger" role="alert">
                <p class="mb-3">@alert.Message</p>
                @if (!string.IsNullOrEmpty(alert.Debug))
                {
                    <pre class="alert-pre border bg-light p-2"><code>@alert.Debug</code></pre>
                }
            </div>
        }
        @RenderBody()
    </main>
    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>

最后的最后,确保PrivateSettings.config文件中的信息是我们实际应用的值即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章