使用 Microsoft Microsoft Teams生成应用Graph
本教程指导你如何使用 Microsoft Teams 和 Microsoft ASP.NET Core Graph API 生成 Microsoft Teams 应用,以检索用户的日历信息。
提示
如果只想下载已完成的教程,可以下载或克隆GitHub存储库。 有关使用应用 ID 和密码配置应用的说明,请参阅演示文件夹中的自述文件。
先决条件
在开始本教程之前,应在开发计算机上安装以下内容。
你还应该在启用了自定义应用旁加载的 Microsoft 365 租户中拥有一Teams或学校帐户。 如果你没有 Microsoft 工作或学校帐户,或者你的组织尚未启用自定义 Teams 应用旁加载,你可以注册Microsoft 365 开发人员计划,获取免费的 Office 365 开发人员订阅。
备注
本教程是使用 .NET SDK 版本 5.0.302 编写的。 本指南中的步骤可能与其他版本一起运行,但尚未经过测试。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
创建 ASP.NET Core MVC Web 应用
Microsoft Teams选项卡应用程序有多个选项来对用户进行身份验证并调用 Microsoft Graph。 在此练习中,你将实现一个执行单一登录的选项卡,以在客户端上获取身份验证令牌,然后在服务器上使用代表流交换该令牌,以访问 Microsoft Graph。
有关其他备选方法,请参阅以下内容。
- 使用 Microsoft Microsoft Teams 生成一个"Graph Toolkit"选项卡。 此示例完全为客户端,使用 Microsoft Graph Toolkit处理身份验证和调用 Microsoft Graph。
- Microsoft Teams身份验证示例。 此示例包含多个涉及不同身份验证方案的示例。
创建项目
首先创建一个 ASP.NET Core Web 应用。
在要创建项目的 (CLI) 打开命令行接口。 运行以下命令:
dotnet new webapp -o GraphTutorial
创建项目后,验证其是否正常工作,方法为将当前目录更改到 GraphTu一l 目录,并运行 CLI 中的以下命令。
dotnet run
打开浏览器并浏览到
https://localhost:5001
。 如果一切正常,应该会看到默认 ASP.NET Core页面。
重要
如果收到 localhost 证书不受信任的警告,可以使用 .NET Core CLI 安装和信任开发证书。 有关特定操作系统的说明,请参阅ASP.NET Core中的强制 HTTPS。
添加 Nuget 程序包
在继续之前,请安装一些NuGet包,你稍后会使用它。
- 用于进行身份验证和请求访问令牌的Microsoft.Identity.Web。
- Microsoft.Identity.Web.MicrosoftGraph,用于添加Graph Microsoft.Identity.Web 配置的支持。
- Microsoft。Graph更新 Microsoft.Identity.Web.MicrosoftGraph 安装的此程序包的版本。
- TimeZoneConverter,用于Windows时区标识符转换为 IANA 标识符。
在 CLI 中运行以下命令以安装依赖项。
dotnet add package Microsoft.Identity.Web --version 1.15.2 dotnet add package Microsoft.Identity.Web.MicrosoftGraph --version 1.15.2 dotnet add package Microsoft.Graph --version 4.1.0 dotnet add package TimeZoneConverter
设计应用
在此部分中,你将创建应用程序的基本 UI 结构。
提示
可以使用任何文本编辑器编辑本教程的源文件。 但是,Visual Studio Code提供其他功能,如调试和 Intellisense。
打开 ./Pages/Shared/_Layout.cshtml, 并将其全部内容替换为以下代码以更新应用程序的全局布局。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - GraphTutorial</title> <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/11.0.0/css/fabric.min.css" /> <link rel="stylesheet" href="~/css/site.css" /> </head> <body class="ms-Fabric"> <div class="container"> <main role="main"> @RenderBody() </main> </div> <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="https://statics.teams.cdn.office.net/sdk/v1.7.0/js/MicrosoftTeams.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> @RenderSection("Scripts", required: false) </body> </html>
这会将 Bootstrap替换为 Fluent UI,添加Microsoft Teams SDK,并简化布局。
打开 ./wwwroot/js/site.js 并添加以下代码。
(function () { // Support Teams themes microsoftTeams.initialize(); // On load, match the current theme microsoftTeams.getContext((context) => { if(context.theme !== 'default') { // For Dark and High contrast, set text to white document.body.style.color = '#fff'; document.body.style.setProperty('--border-style', 'solid'); } }); // Register event listener for theme change microsoftTeams.registerOnThemeChangeHandler((theme)=> { if(theme !== 'default') { document.body.style.color = '#fff'; document.body.style.setProperty('--border-style', 'solid'); } else { // For default theme, remove inline style document.body.style.color = ''; document.body.style.setProperty('--border-style', 'none'); } }); })();
这将添加一个简单的主题更改处理程序,以更改深色和高对比度主题的默认文本颜色。
打开 ./wwwroot/css/site.css, 并将其内容替换为以下内容。
:root { --border-style: none; } .tab-title { margin-bottom: .5em; } .event-card { margin: .5em; padding: 1em; border-style: var(--border-style); border-width: 1px; border-color: #fff; } .event-card div { margin-bottom: .25em; } .event-card .ms-Icon { margin-right: 10px; float: left; position: relative; top: 3px; } .event-card .ms-Icon--MapPin { top: 2px; } .form-container { max-width: 720px; } .form-label { display: block; margin-bottom: .25em; } .form-input { width: 100%; margin-bottom: .25em; padding: .5em; box-sizing: border-box; } .form-button { padding: .5em; } .result-panel { display: none; padding: 1em; margin: 1em; } .error-msg { color: red; } .success-msg { color: green; }
打开 ./Pages/Index.cshtml, 并将其内容替换为以下代码。
@page @model IndexModel @{ ViewData["Title"] = "Home page"; } <div id="tab-container"> <h1 class="ms-fontSize-24 ms-fontWeight-semibold">Loading...</h1> </div> @section Scripts { <script> </script> }
打开 ./Startup.cs 并删除
app.UseHttpsRedirection();
方法中的Configure
行。 这是 ngrok 隧道工作所必需的。
运行 ngrok
Microsoft Teams不支持应用的本地托管。 托管应用的服务器必须使用 HTTPS 终结点从云中提供。 对于本地调试,可以使用 ngrok 为本地托管的项目创建公用 URL。
打开 CLI 并运行以下命令以启动 ngrok。
ngrok http 5000
ngrok 启动后,复制 HTTPS 转发 URL。 它应类似
https://50153897dd4d.ngrok.io
。 在稍后的步骤中,将需要此值。
重要
如果您使用的是 ngrok 的免费版本,则每次重新启动 ngrok 时,转发 URL 都会更改。 建议您保持 ngrok 运行,直到完成本教程以保持相同的 URL。 如果必须重新启动 ngrok,则需要在所使用的任何地方更新 URL,并重新安装Microsoft Teams。
在门户中注册该应用
在此练习中,你将使用管理中心创建新的 Azure AD Azure Active Directory注册。
打开浏览器,并转到 Azure Active Directory 管理中心。 使用 个人帐户(亦称为“Microsoft 帐户”)或 工作或学校帐户 登录。
选择左侧导航栏中的“Azure Active Directory”,再选择“管理”下的“应用注册”。
选择“新注册”。 在 "注册应用程序" 页上,按如下所示设置值,其中 是上一节中复制的
YOUR_NGROK_URL
ngrok 转发 URL。- 将“名称”设置为“
Teams Graph Tutorial
”。 - 将“受支持的帐户类型”设置为“任何组织目录中的帐户和个人 Microsoft 帐户”。
- 在“重定向 URI”下,将第一个下拉列表设置为“
Web
”,并将值设置为“YOUR_NGROK_URL/authcomplete
”。
- 将“名称”设置为“
选择“注册”。 在Teams Graph 教程"页上,复制"应用程序 (客户端) ID"的值并 保存它,下一步中将需要该值。
选择“管理”下的“身份验证”。 找到 隐式授予 部分并启用 访问令牌 和 ID 令牌。 选择“保存”。
选择“管理”下的“证书和密码”。 选择“新客户端密码”按钮。 在“说明”中输入值,并选择“过期”下的一个选项,再选择“添加”。
离开此页前,先复制客户端密码值。 将在下一步中用到它。
重要
此客户端密码不会再次显示,所以请务必现在就复制它。
选择 "管理"下的"API 权限", 然后选择"添加权限"。
选择 "Microsoft Graph", 然后选择"委派权限"。
选择以下权限,然后选择"添加权限"。
- Calendars.ReadWrite - 这将允许应用读取和写入用户的日历。
- MailboxSettings.Read - 这将允许应用从用户的邮箱设置获取用户的时区、日期格式和时间格式。
配置Teams单一登录
在此部分中,你将更新应用注册以支持Teams。
选择 "公开 API"。 选择" 应用程序 ID URI"旁边的"设置"链接。 插入 ngrok 转发 URL 域名, (双正斜杠和 GUID 之间的末尾附加一个正斜杠) "/"。 整个 ID 应类似于
api://50153897dd4d.ngrok.io/ae7d8088-3422-4c8c-a351-6ded0f21d615
:。在"此 API 定义的范围"部分,选择"添加范围"。 按如下所示填写字段,然后选择"添加范围"。
- 范围名称:
access_as_user
- Who同意?:管理员和用户
- 管理员同意显示名称:
Access the app as the user
- 管理员同意说明:
Allows Teams to call the app's web APIs as the current user.
- 用户同意显示名称:
Access the app as you
- 用户同意说明:
Allows Teams to call the app's web APIs as you.
- 状态:已启用
- 范围名称:
在"授权客户端应用程序" 部分,选择 "添加客户端应用程序"。 从以下列表中输入客户端 ID,在"授权范围"下启用范围,然后选择"添加应用程序"。 对列表中的每个客户端 ID 重复此过程。
1fec8e78-bce4-4aaf-ab1b-5451cc387264
(Teams移动/桌面应用程序)5e3ce6c0-2b1f-4285-8d4b-75ee78787346
(Teams Web 应用程序)
创建应用清单
应用清单描述应用如何与Microsoft Teams集成,并且安装应用是必需的。 在此部分中,你将使用 Microsoft Teams 客户端中的 App Studio 生成清单。
如果你尚未在应用程序中安装 App Studio,Teams立即安装它。
在 Microsoft Teams 启动 App Studio,然后选择 清单编辑器。
选择 创建新应用。
在" 应用详细信息 "页上,填写必填字段。
备注
可以使用"品牌打造"部分中的默认 图标 或上传你自己的图标。
在左侧菜单上,选择功能下的****选项卡。
选择 "添加****个人"选项卡下的"添加"。
按如下所示填写字段,
YOUR_NGROK_URL
其中 是上一节中复制的转发 URL。 完成后选择 保存。- 名称:
Create event
- 实体 ID:
createEventTab
- 内容 URL:
YOUR_NGROK_URL/newevent
- 名称:
选择 "添加****个人"选项卡下的"添加"。
按如下所示填写字段,
YOUR_NGROK_URL
其中 是上一节中复制的转发 URL。 完成后选择 保存。- 名称:
Graph calendar
- 实体 ID:
calendarTab
- 内容 URL:
YOUR_NGROK_URL
- 名称:
在左侧菜单上,选择完成 下的域和****权限。
通过应用 注册将 AAD 应用 ID 设置为应用程序 ID。
通过 应用注册将 "单一登录"字段设置为应用程序 ID URI。
在左侧菜单上,选择"完成 "下的"测试和****分发"。 选择 下载。
在名为 Manifest 的项目的根目录中新建 一个目录。 将下载的 ZIP 文件的内容提取到此目录。
添加 Azure AD 身份验证
在此练习中,你将从上一练习中扩展应用程序,以支持使用 Azure AD 的单一登录身份验证。 需要获得所需的 OAuth 访问令牌才能调用 Microsoft Graph API。 在此步骤中,您将配置 Microsoft.Identity.Web 库。
重要
为了避免在源中存储应用程序 ID 和密码,你将使用 .NET 密码管理器 存储这些值。 密码管理器仅供开发使用,生产应用应该使用受信任的密码管理器来存储密码。
打开 "appsettings.js", 并将其内容替换为以下内容。
{ "AzureAd": { "Instance": "https://login.microsoftonline.com/", "TenantId": "common" }, "Graph": { "Scopes": "https://graph.microsoft.com/.default" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*" }
在 GraphTu一l.csproj 所在的目录中打开 CLI,然后运行以下命令,用 Azure 门户中的应用程序 ID 和应用程序密码进行代用。
YOUR_APP_ID
YOUR_APP_SECRET
dotnet user-secrets init dotnet user-secrets set "AzureAd:ClientId" "YOUR_APP_ID" dotnet user-secrets set "AzureAd:ClientSecret" "YOUR_APP_SECRET"
实施登录
首先,在应用的 JavaScript 代码中实现单一登录。 你将使用Microsoft Teams JavaScript SDK获取访问令牌,该令牌允许运行在 Teams 客户端中的 JavaScript 代码对您将稍后实现的 Web API 进行 AJAX 调用。
打开 ./Pages/Index.cshtml, 在 标记内添加以下
<script>
代码。(function () { if (microsoftTeams) { microsoftTeams.initialize(); microsoftTeams.authentication.getAuthToken({ successCallback: (token) => { // TEMPORARY: Display the access token for debugging $('#tab-container').empty(); $('<code/>', { text: token, style: 'word-break: break-all;' }).appendTo('#tab-container'); }, failureCallback: (error) => { renderError(error); } }); } })(); function renderError(error) { $('#tab-container').empty(); $('<h1/>', { text: 'Error' }).appendTo('#tab-container'); $('<code/>', { text: JSON.stringify(error, Object.getOwnPropertyNames(error)), style: 'word-break: break-all;' }).appendTo('#tab-container'); }
这将调用
microsoftTeams.authentication.getAuthToken
以静默方式作为登录用户进行身份验证Teams。 通常不会涉及到任何 UI 提示,除非用户必须同意。 然后,代码将在选项卡中显示令牌。在 CLI 中运行以下命令,保存更改并启动应用程序。
dotnet run
重要
如果已重新启动 ngrok 并且 ngrok URL 已更改,请务必在测试之前在下列位置更新 ngrok值。
- 应用注册中的重定向 URI
- 应用注册中的应用程序 ID URI
contentUrl
in manifest.jsonvalidDomains
in manifest.jsonresource
in manifest.json
创建 ZIP 文件 ,manifest.js、color.png 和 outline.png。
In Microsoft Teams, select Apps in the left-hand bar, select Upload a custom app, then select Upload for me or my teams.
浏览到之前创建的 ZIP 文件,然后选择"打开 "。
查看应用程序信息,然后选择"添加 "。
应用程序将在 Teams中打开并显示访问令牌。
如果复制令牌,可以将其粘贴到 jwt.ms 。 验证声明 (访问群体) 应用程序 ID,声明 (的唯一) 是创建的 aud
scp
API access_as_user
范围。 这意味着此令牌不会向 Microsoft Graph! 相反,您即将实现的 Web API 将需要使用代表流交换此令牌,以获得将与 Microsoft Graph一起使用的令牌。
配置 ASP.NET Core 应用中的身份验证
首先,将 Microsoft Identity 平台服务添加到应用程序。
打开 ./Startup.cs 文件,将以下语句
using
添加到文件顶部。using Microsoft.Identity.Web;
在 函数中的 行之前
app.UseAuthorization();
添加以下Configure
行。app.UseAuthentication();
在 函数中的 行之后
endpoints.MapRazorPages();
添加以下Configure
行。endpoints.MapControllers();
将现有的
ConfigureServices
函数替换为以下内容。public void ConfigureServices(IServiceCollection services) { // Use Web API authentication (default JWT bearer token scheme) services.AddMicrosoftIdentityWebApiAuthentication(Configuration) // Enable token acquisition via on-behalf-of flow .EnableTokenAcquisitionToCallDownstreamApi() // Specify that the down-stream API is Graph .AddMicrosoftGraph(Configuration.GetSection("Graph")) // Use in-memory token cache // See https://github.com/AzureAD/microsoft-identity-web/wiki/token-cache-serialization .AddInMemoryTokenCaches(); services.AddRazorPages(); services.AddControllers().AddNewtonsoftJson(); }
此代码将应用程序配置为允许对 Web API 的调用基于标头中的 JWT bearer 令牌
Authorization
进行身份验证。 它还添加了令牌获取服务,这些服务可以通过代表流交换该令牌。
创建 Web API 控制器
在名为 Controllers 的项目的根目录中新建 一个目录。
在名为 CalendarController.cs 的 ./Controllers 目录中创建新文件并添加以下代码。
using System; using System.Collections.Generic; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Identity.Web; using Microsoft.Identity.Web.Resource; using Microsoft.Graph; using TimeZoneConverter; namespace GraphTutorial.Controllers { [ApiController] [Route("[controller]")] [Authorize] public class CalendarController : ControllerBase { private static readonly string[] apiScopes = new[] { "access_as_user" }; private readonly GraphServiceClient _graphClient; private readonly ITokenAcquisition _tokenAcquisition; private readonly ILogger<CalendarController> _logger; public CalendarController(ITokenAcquisition tokenAcquisition, GraphServiceClient graphClient, ILogger<CalendarController> logger) { _tokenAcquisition = tokenAcquisition; _graphClient = graphClient; _logger = logger; } [HttpGet] public async Task<ActionResult<string>> Get() { // This verifies that the access_as_user scope is // present in the bearer token, throws if not HttpContext.VerifyUserHasAnyAcceptedScope(apiScopes); // To verify that the identity libraries have authenticated // based on the token, log the user's name _logger.LogInformation($"Authenticated user: {User.GetDisplayName()}"); try { // TEMPORARY // Get a Graph token via OBO flow var token = await _tokenAcquisition .GetAccessTokenForUserAsync(new[]{ "User.Read", "MailboxSettings.Read", "Calendars.ReadWrite" }); // Log the token _logger.LogInformation($"Access token for Graph: {token}"); return Ok("{ \"status\": \"OK\" }"); } catch (MicrosoftIdentityWebChallengeUserException ex) { _logger.LogError(ex, "Consent required"); // This exception indicates consent is required. // Return a 403 with "consent_required" in the body // to signal to the tab it needs to prompt for consent return new ContentResult { StatusCode = (int)HttpStatusCode.Forbidden, ContentType = "text/plain", Content = "consent_required" }; } catch (Exception ex) { _logger.LogError(ex, "Error occurred"); throw; } } } }
这将实现一个 Web API ()
GET /calendar
可以从"开始"选项卡Teams调用。现在,它只是尝试交换令牌的Graph令牌。 用户第一次加载选项卡时,这将失败,因为他们尚未同意允许应用代表Graph访问 Microsoft。打开 ./Pages/Index.cshtml, 将
successCallback
函数替换为以下内容。successCallback: (token) => { // TEMPORARY: Call the Web API fetch('/calendar', { headers: { 'Authorization': `Bearer ${token}` } }).then(response => { response.text() .then(body => { $('#tab-container').empty(); $('<code/>', { text: body }).appendTo('#tab-container'); }); }).catch(error => { console.error(error); renderError(error); }); }
这将调用 Web API 并显示响应。
保存更改并重新启动该应用。 刷新选项卡中的Microsoft Teams。 页面应显示
consent_required
。查看 CLI 中的日志输出。 请注意两点。
- 类似 的条目
Authenticated user: MeganB@contoso.com
。 Web API 已基于随 API 请求发送的令牌对用户进行身份验证。 - 类似 的条目
AADSTS65001: The user or administrator has not consented to use the application with ID...
。 这是预期操作,因为尚未提示用户同意请求的 Microsoft Graph权限范围。
- 类似 的条目
实现同意提示
由于 Web API 无法提示用户,Teams选项卡将需要实现提示。 此操作只需为每个用户执行一次。 一旦用户同意,他们无需重新连接,除非他们明确撤销了对您的应用程序的访问权限。
在 ./Pages 目录中创建一个名为 Authenticate.cshtml.cs 的新文件并添加以下代码。
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace GraphTutorial.Pages { public class AuthenticateModel : PageModel { private readonly ILogger<IndexModel> _logger; public string ApplicationId { get; private set; } public string State { get; private set; } public string Nonce { get; private set; } public AuthenticateModel(IConfiguration configuration, ILogger<IndexModel> logger) { _logger = logger; // Read the application ID from the // configuration. This is used to build // the authorization URL for the consent prompt ApplicationId = configuration .GetSection("AzureAd") .GetValue<string>("ClientId"); // Generate a GUID for state and nonce State = System.Guid.NewGuid().ToString(); Nonce = System.Guid.NewGuid().ToString(); } } }
在 ./Pages 目录中创建一个名为 Authenticate.cshtml 的新文件,并添加以下代码。
@page <!-- Copyright (c) Microsoft Corporation. Licensed under the MIT License. --> @model AuthenticateModel @section Scripts { <script> (function () { microsoftTeams.initialize(); // Save the state so it can be verified in // AuthComplete.cshtml localStorage.setItem('auth-state', '@Model.State'); // Get the context for tenant ID and login hint microsoftTeams.getContext((context) => { // Set all of the query parameters for an // authorization request const queryParams = { client_id: '@Model.ApplicationId', response_type: 'id_token token', response_mode: 'fragment', scope: 'https://graph.microsoft.com/.default openid', redirect_uri: `${window.location.origin}/authcomplete`, nonce: '@Model.Nonce', state: '@Model.State', login_hint: context.loginHint, }; // Generate the URL const authEndpoint = `https://login.microsoftonline.com/${context.tid}/oauth2/v2.0/authorize?${toQueryString(queryParams)}`; // Browse to the URL window.location.assign(authEndpoint); }); })(); // Helper function to build a query string from an object function toQueryString(queryParams) { let encodedQueryParams = []; for (let key in queryParams) { encodedQueryParams.push(key + '=' + encodeURIComponent(queryParams[key])); } return encodedQueryParams.join('&'); } </script> }
在 ./Pages 目录中创建一个名为 AuthComplete.cshtml 的新文件并添加以下代码。
@page <!-- Copyright (c) Microsoft Corporation. Licensed under the MIT License. --> @section Scripts { <script> (function () { microsoftTeams.initialize(); const hashParams = getHashParameters(); if (hashParams['error']) { microsoftTeams.authentication.notifyFailure(hashParams['error']); } else if (hashParams['access_token']) { // Check the state parameter const expectedState = localStorage.getItem('auth-state'); if (expectedState !== hashParams['state']) { microsoftTeams.authentication.notifyFailure('StateDoesNotMatch'); } else { // State parameter matches, report success localStorage.removeItem('auth-state'); microsoftTeams.authentication.notifySuccess('Success'); } } else { microsoftTeams.authentication.notifyFailure('NoTokenInResponse'); } })(); // Helper function to generate a hash from // a query string function getHashParameters() { let hashParams = {}; location.hash.substr(1).split('&').forEach(function(item) { let s = item.split('='), k = s[0], v = s[1] && decodeURIComponent(s[1]); hashParams[k] = v; }); return hashParams; } </script> }
打开 ./Pages/Index.cshtml, 在 标记中添加以下
<script>
函数。function loadUserCalendar(token, callback) { // Call the API fetch('/calendar', { headers: { 'Authorization': `Bearer ${token}` } }).then(response => { if (response.ok) { // Get the JSON payload response.json() .then(events => { callback(events); }); } else if (response.status === 403) { response.text() .then(body => { // If the API sent 'consent_required' // we need to prompt the user if (body === 'consent_required') { promptForConsent((error) => { if (error) { renderError(error); } else { // Retry API call loadUserCalendar(token, callback); } }); } }); } }).catch(error => { renderError(error); }); } function promptForConsent(callback) { // Cause Teams to popup a window for consent microsoftTeams.authentication.authenticate({ url: `${window.location.origin}/authenticate`, width: 600, height: 535, successCallback: (result) => { callback(null); }, failureCallback: (error) => { callback(error); } }); }
在 标记中添加以下
<script>
函数,以显示 Web API 的成功结果。function renderCalendar(events) { $('#tab-container').empty(); $('<pre/>').append($('<code/>', { text: JSON.stringify(events, null, 2), style: 'word-break: break-all;' })).appendTo('#tab-container'); }
将 现有的
successCallback
替换为以下代码。successCallback: (token) => { loadUserCalendar(token, (events) => { renderCalendar(events); }); }
保存更改并重新启动该应用。 刷新选项卡中的Microsoft Teams。 你应该收到一个弹出窗口,要求同意 Microsoft Graph权限范围。 接受后,选项卡应显示
{ "status": "OK" }
。备注
如果选项卡显示
"FailedToOpenWindow"
,请在浏览器中禁用弹出窗口阻止程序并重新加载页面。查看日志输出。 应该会看到
Access token for Graph
条目。 如果分析该令牌,你会注意到它包含在 上Graph Microsoftappsettings.js 作用域。
存储和刷新令牌
此时,应用程序具有访问令牌,该令牌在 API 调用 Authorization
标头中发送。 该令牌允许应用程序代表用户访问 Microsoft Graph。
但是,此令牌期很短。 令牌在颁发后一小时过期。 此时刷新令牌将变得有效。 刷新令牌允许应用程序请求新的访问令牌,而无需用户再次登录。
由于应用使用的是 Microsoft.Identity.Web 库,因此不需要实现任何令牌存储或刷新逻辑。
应用使用内存中令牌缓存,这足以供应用在重新启动时无需保留令牌使用。 生产应用可能会改为使用 Microsoft.Identity.Web 库中 的分布式缓存选项。
GetAccessTokenForUserAsync
方法将处理令牌过期并刷新。 它首先检查缓存的令牌,如果令牌未过期,则返回它。 如果已过期,它将使用缓存的刷新令牌获取新的刷新令牌。
控制器通过依赖关系注入获取的 GraphServiceClient 已使用适用于你的身份验证提供程序 GetAccessTokenForUserAsync
进行预配置。
获取日历视图
在此部分中,你将 Microsoft Graph应用程序。 对于此应用程序,你将使用适用于 .NET 的 Microsoft Graph客户端库调用 Microsoft Graph。
获取日历视图
日历视图是两个时间点之间发生的用户日历中的一组事件。 你将使用它获取本周的用户事件。
打开 ./Controllers/CalendarController.cs, 将以下函数添加到 CalendarController 类。
private DateTime GetUtcStartOfWeekInTimeZone(DateTime today, string timeZoneId) { // Time zone returned by Graph could be Windows or IANA style // TimeZoneConverter can take either TimeZoneInfo userTimeZone = TZConvert.GetTimeZoneInfo(timeZoneId); // Assumes Sunday as first day of week int diff = System.DayOfWeek.Sunday - today.DayOfWeek; // create date as unspecified kind var unspecifiedStart = DateTime.SpecifyKind(today.AddDays(diff), DateTimeKind.Unspecified); // convert to UTC return TimeZoneInfo.ConvertTimeToUtc(unspecifiedStart, userTimeZone); }
添加以下函数以处理从 Microsoft Graph返回的异常。
private async Task HandleGraphException(Exception exception) { if (exception is MicrosoftIdentityWebChallengeUserException) { _logger.LogError(exception, "Consent required"); // This exception indicates consent is required. // Return a 403 with "consent_required" in the body // to signal to the tab it needs to prompt for consent HttpContext.Response.ContentType = "text/plain"; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; await HttpContext.Response.WriteAsync("consent_required"); } else if (exception is ServiceException) { var serviceException = exception as ServiceException; _logger.LogError(serviceException, "Graph service error occurred"); HttpContext.Response.ContentType = "text/plain"; HttpContext.Response.StatusCode = (int)serviceException.StatusCode; await HttpContext.Response.WriteAsync(serviceException.Error.ToString()); } else { _logger.LogError(exception, "Error occurred"); HttpContext.Response.ContentType = "text/plain"; HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; await HttpContext.Response.WriteAsync(exception.ToString()); } }
将现有的
Get
函数替换为以下内容。[HttpGet] public async Task<IEnumerable<Event>> Get() { // This verifies that the access_as_user scope is // present in the bearer token, throws if not HttpContext.VerifyUserHasAnyAcceptedScope(apiScopes); // To verify that the identity libraries have authenticated // based on the token, log the user's name _logger.LogInformation($"Authenticated user: {User.GetDisplayName()}"); try { // Get the user's mailbox settings var me = await _graphClient.Me .Request() .Select(u => new { u.MailboxSettings }) .GetAsync(); // Get the start and end of week in user's time // zone var startOfWeek = GetUtcStartOfWeekInTimeZone( DateTime.Today, me.MailboxSettings.TimeZone); var endOfWeek = startOfWeek.AddDays(7); // Set the start and end of the view var viewOptions = new List<QueryOption> { new QueryOption("startDateTime", startOfWeek.ToString("o")), new QueryOption("endDateTime", endOfWeek.ToString("o")) }; // Get the user's calendar view var results = await _graphClient.Me .CalendarView .Request(viewOptions) // Send user time zone in request so date/time in // response will be in preferred time zone .Header("Prefer", $"outlook.timezone=\"{me.MailboxSettings.TimeZone}\"") // Get max 50 per request .Top(50) // Only return fields app will use .Select(e => new { e.Subject, e.Organizer, e.Start, e.End, e.Location }) // Order results chronologically .OrderBy("start/dateTime") .GetAsync(); return results.CurrentPage; } catch (Exception ex) { await HandleGraphException(ex); return null; } }
查看更改。 此函数的新版本:
- 返回
IEnumerable<Event>
,而不是string
。 - 使用 Microsoft 邮件获取用户的邮箱Graph。
- 使用用户的时区计算本周的开始时间和结束时间。
- 获取日历视图
- 使用 函数包含标头,这将导致返回的事件的开始时间和结束时间转换为
.Header()
Prefer: outlook.timezone
用户的时区。 - 使用
.Top()
函数请求最多 50 个事件。 - 使用
.Select()
函数仅请求应用使用的字段。 - 使用
OrderBy()
函数按开始时间对结果进行排序。
- 使用 函数包含标头,这将导致返回的事件的开始时间和结束时间转换为
- 返回
保存更改并重新启动该应用。 刷新选项卡中的Microsoft Teams。 应用显示事件的 JSON 列表。
显示结果
现在,你可以以更用户友好的方式显示事件列表。
打开 ./Pages/Index.cshtml, 在 标记中添加以下
<script>
函数。function renderSubject(subject) { if (!subject || subject.length <= 0) { subject = '<No subject>'; } return $('<div/>', { class: 'ms-fontSize-18 ms-fontWeight-bold', text: subject }); } function renderOrganizer(organizer) { return $('<div/>', { class: 'ms-fontSize-14 ms-fontWeight-semilight', text: organizer.emailAddress.name }).append($('<i/>', { class: 'ms-Icon ms-Icon--PartyLeader', style: 'margin-right: 10px;' })); } function renderTimeSpan(start, end) { return $('<div/>', { class: 'ms-fontSize-14 ms-fontWeight-semilight', text: `${formatDateTime(start.dateTime)} - ${formatDateTime(end.dateTime)}` }).append($('<i/>', { class: 'ms-Icon ms-Icon--DateTime2', style: 'margin-right: 10px;' })); } function formatDateTime(dateTime) { const date = new Date(dateTime); // Format like 10/14/2020 4:00 PM let hours = date.getHours(); const minutes = date.getMinutes(); const ampm = hours >= 12 ? 'PM' : 'AM'; hours = hours % 12; hours = hours ? hours : 12; const minStr = minutes < 10 ? `0${minutes}` : minutes; return `${date.getMonth()+1}/${date.getDate()}/${date.getFullYear()} ${hours}:${minStr} ${ampm}`; } function renderLocation(location) { if (!location || location.displayName.length <= 0) { return null; } return $('<div/>', { class: 'ms-fontSize-14 ms-fontWeight-semilight', text: location.displayName }).append($('<i/>', { class: 'ms-Icon ms-Icon--MapPin', style: 'margin-right: 10px;' })); }
将现有的
renderCalendar
函数替换为以下内容。function renderCalendar(events) { $('#tab-container').empty(); // Add title $('<div/>', { class: 'tab-title ms-fontSize-42', text: 'Week at a glance' }).appendTo('#tab-container'); // Render each event events.map(event => { const eventCard = $('<div/>', { class: 'event-card ms-depth-4', }); eventCard.append(renderSubject(event.subject)); eventCard.append(renderOrganizer(event.organizer)); eventCard.append(renderTimeSpan(event.start, event.end)); const location = renderLocation(event.location); if (location) { eventCard.append(location); } eventCard.appendTo('#tab-container'); }); }
保存更改并重新启动该应用。 刷新选项卡中的Microsoft Teams。 应用在用户日历上显示事件。
创建新事件
在此部分中,您将添加在用户日历上创建事件的能力。
新建事件选项卡
在 ./Pages 目录中新建一个名为 NewEvent.cshtml 的文件并添加以下代码。
@page <!-- Copyright (c) Microsoft Corporation. Licensed under the MIT License. --> @{ ViewData["Title"] = "New event"; } <div class="form-container"> <form id="newEventForm"> <div class="ms-Grid" dir="ltr"> <div class="ms-Grid-row"> <div class="ms-Grid-col ms-sm12"> <label class="ms-fontWeight-semibold form-label" for="subject">Subject</label> <input class="form-input" type="text" id="subject" name="subject" /> </div> </div> <div class="ms-Grid-row"> <div class="ms-Grid-col ms-sm12"> <label class="ms-fontWeight-semibold form-label" for="attendees">Attendees</label> <input class="form-input" type="text" id="attendees" name="attendees" placeholder="Enter email addresses of attendees. Separate multiple with ';'. Leave blank for no attendees." /> </div> </div> <div class="ms-Grid-row"> <div class="ms-Grid-col ms-sm6"> <label class="ms-fontWeight-semibold form-label" for="start">Start</label> <input class="form-input" type="datetime-local" id="start" name="start" /> </div> <div class="ms-Grid-col ms-sm6"> <label class="ms-fontWeight-semibold form-label" for="end">End</label> <input class="form-input" type="datetime-local" id="end" name="end" /> </div> </div> <div class="ms-Grid-row"> <div class="ms-Grid-col ms-sm12"> <label class="ms-fontWeight-semibold form-label" for="body">Body</label> <textarea class="form-input" id="body" name="body" rows="4"></textarea> </div> </div> <input class="form-button" type="submit" value="Create"/> </div> </form> <div class="ms-depth-16 result-panel"></div> </div> @section Scripts { <script> (function () { if (microsoftTeams) { microsoftTeams.initialize(); } $('#newEventForm').on('submit', async (e) => { e.preventDefault(); $('.result-panel').empty(); $('.result-panel').hide(); const formData = new FormData(newEventForm); // Basic validation // Require subject, start, and end const subject = formData.get('subject'); const start = formData.get('start'); const end = formData.get('end'); if (subject.length <= 0 || start.length <= 0 || end.length <= 0) { $('<div/>', { class: 'error-msg', text: 'Subject, Start, and End are required.' }).appendTo('.result-panel'); $('.result-panel').show(); return; } // Get the auth token from Teams microsoftTeams.authentication.getAuthToken({ successCallback: (token) => { createEvent(token, formData); }, failureCallback: (error) => { $('<div/>', { class: 'error-msg', text: `Error getting token: ${error}` }).appendTo('.result-panel'); $('.result-panel').show(); } }); }); })(); async function createEvent(token, formData) { // Convert the form to a JSON payload jsonFormData = formDataToJson(); // Post the payload to the web API const response = await fetch('/calendar', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, method: 'POST', body: jsonFormData }); if (response.ok) { $('<div/>', { class: 'success-msg', text: 'Event added to your calendar' }).appendTo('.result-panel'); $('.result-panel').show(); } else { const error = await response.text(); $('<div/>', { class: 'error-msg', text: `Error creating event: ${error}` }).appendTo('.result-panel'); $('.result-panel').show(); } } // Helper method to serialize the form fields // as JSON function formDataToJson() { const array = $('#newEventForm').serializeArray(); const jsonObj = {}; array.forEach((kvp) => { jsonObj[kvp.name] = kvp.value; }); return JSON.stringify(jsonObj); } </script> }
这将实现一个简单的表单,并添加 JavaScript 以将表单数据张贴到 Web API。
实现 Web API
在项目的根目录中新建一个名为 Models 的目录。
在 ./Models 目录中新建一个名为 NewEvent.cs 的文件 并添加以下代码。
namespace GraphTutorial.Models { public class NewEvent { public string Subject { get; set; } public string Attendees { get; set; } public string Start { get; set; } public string End { get; set; } public string Body { get; set; } } }
打开 ./Controllers/CalendarController.cs, 在文件顶部
using
添加以下语句。using GraphTutorial.Models;
将以下函数添加到 CalendarController 类。
[HttpPost] public async Task<string> Post(NewEvent newEvent) { HttpContext.VerifyUserHasAnyAcceptedScope(apiScopes); try { // Get the user's mailbox settings var me = await _graphClient.Me .Request() .Select(u => new { u.MailboxSettings }) .GetAsync(); // Create a Graph Event var graphEvent = new Event { Subject = newEvent.Subject, Start = new DateTimeTimeZone { DateTime = newEvent.Start, TimeZone = me.MailboxSettings.TimeZone }, End = new DateTimeTimeZone { DateTime = newEvent.End, TimeZone = me.MailboxSettings.TimeZone } }; // If there are attendees, add them if (!string.IsNullOrEmpty(newEvent.Attendees)) { var attendees = new List<Attendee>(); var emailArray = newEvent.Attendees.Split(';'); foreach (var email in emailArray) { attendees.Add(new Attendee { Type = AttendeeType.Required, EmailAddress = new EmailAddress { Address = email } }); } graphEvent.Attendees = attendees; } // If there is a body, add it if (!string.IsNullOrEmpty(newEvent.Body)) { graphEvent.Body = new ItemBody { ContentType = BodyType.Text, Content = newEvent.Body }; } // Create the event await _graphClient.Me .Events .Request() .AddAsync(graphEvent); return "success"; } catch (Exception ex) { await HandleGraphException(ex); return null; } }
这允许对包含表单字段的 Web API 进行 HTTP POST。
保存所有更改并重新启动应用程序。 在 Microsoft Teams 中刷新应用,然后选择" 创建事件" 选项卡。填写表单并选择" 创建", 将事件添加到用户的日历。
恭喜!
你已完成 Microsoft Microsoft Teams 应用Graph教程。 现在,你已经拥有一个调用 Microsoft Graph,你可以试验和添加新功能。 请访问Microsoft Graph概述,查看可以使用 Microsoft Graph 访问的所有数据。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。