本篇我们介绍一下代理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文件中的信息是我们实际应用的值即可。