本篇我們介紹一下代理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調用的項目。
- 在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 }
);
}
}
- 在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}");
}
}
- 在Controllers目錄下,將CalendarController類替換爲以下代碼。
public class CalendarController : BaseController
{
// GET: Calendar
[Authorize]
public ActionResult Index()
{
return View();
}
}
- 繼續添加一個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);
}
}
- 在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();
}
- 刪除Models文件夾中的CachedUser.cs。
- 在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。
- 編輯Views\Calendar\下的index.cshtml,改爲如下內容。
<h1>Calendar</h1>
<mgt-agenda group-by-day></mgt-agenda>
- 編輯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>
- 最後,編輯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文件中的信息是我們實際應用的值即可。