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文件中的信息是我們實際應用的值即可。

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