65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Java 构建 Microsoft Teams 选项卡应用,第 1 部分:创建带 SSO 的个人选项卡应用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2021 年 11 月 12 日

CPOL

13分钟阅读

viewsIcon

6044

在本系列文章中,我们将演示如何使用 Java 构建 Microsoft Teams 应用。

说服团队采用其他应用程序可能具有挑战性。Microsoft Teams for Java Developers 系列介绍了在 Microsoft Teams 中构建应用程序作为解决此常见障碍的方法。为什么不使用人们每天都在使用的平台构建新工具呢?

由于您的同事已经在 Teams 中协作,因此很容易让他们采用新功能并提高工作效率。他们只需选择所需的选项卡,即可在 iframe 中启动应用程序,而无需离开 Teams。在这个新的三部分系列中,我将演示如何使用 Microsoft 的 Teams 应用程序示例库作为构建自定义 Java 应用程序的框架。

在本系列中,我们将使用 C# 示例构建所有应用程序。开发人员可以使用 Visual Studio 创建这些示例,并将其发布到 Azure App Service。我将向您展示如何使用 Spring Boot 将它们重新实现为 Java。

我们将使用 Eclipse 集成开发环境(IDE),但您也可以使用 IntelliJ 或任何其他您选择的 IDE。我想同时运行 C# 和 Java 版本,因此我将把这两个版本都托管在 Azure App Service 上。请注意,本文不依赖 ngrok。Azure App Service 服务器的应用程序运行方式与 Spring Boot 包的 Tomcat 服务器不同,这为拓宽您的经验提供了绝佳的机会。

让我们从 Microsoft Teams for Java Developers 系列中创建的个人待办事项列表应用程序入手。最好能保护此列表,因此在本教程中,我将构建一个个人选项卡应用程序的框架,并通过 Azure Active Directory (Azure AD) 进行单点登录 (SSO) 来保护它。然后,您可以将待办事项列表代码添加到此框架中,或使用它来托管您自己的应用程序。

在 Teams 应用中实现 SSO

我们使用 OAuth 2 来实现 Teams 中的单点登录 (SSO)。OAuth 2 身份验证过程很复杂,但我已提供了处理它所需的所有代码。如果您不熟悉 OAuth 2,应该查看此 Microsoft 文章。

此应用程序借助 Spring Boot OAuth2 Client 和 Spring Boot Azure Active Directory 库,通过三部分流程实现 OAuth 2。首先,它对用户进行身份验证,然后对应用程序进行身份验证,最后获取令牌以访问 Teams 资源。

Microsoft 指出,Teams 应用程序应首先尝试“静默”获取访问权限,因为 Teams 可以从多个设备对用户进行身份验证。例如,用户可能在手机上具有活动会话,然后在平板电脑上打开 Teams 选项卡。如果存在身份验证问题,应用程序需要提供一种方法让用户启动身份验证过程。

当 Teams 在 iframe 中打开我的应用程序时,应用程序使用 Teams SDK 检查身份验证。请注意,Teams 可能会通过向用户显示身份验证页面来响应应用程序的身份验证令牌请求。

此过程为我们提供了用于建立用户身份的令牌,但它不提供访问 Teams 资源的权限。应用程序必须发送请求以访问 Teams 资源。应用程序将我们的身份令牌包含在该请求中,Teams 使用它来查看用户是否具有对所请求资源的访问权限。如果有,则 Teams 返回一个我们可以用于请求资源的令牌。大部分代码位于 auth.js 中,并得到 SsoAuthHelper 类的代码支持。

注册应用程序并创建终结点

我们的应用程序在没有访问 Teams 的情况下无法向 Teams 发送访问请求。我们通过在 Azure Active Directory 应用注册门户上注册我们的应用程序并创建一个应用程序用于向 Azure 进行身份验证的密钥来实现这一点。此注册还必须授予 Teams 访问权限,以便将数据返回到应用程序终结点。此过程的分步说明在 Teams Tab SSO Authentication README 中。您可以使用相同的过程来注册您的 Java 应用程序。

请注意,注册过程会询问 Web 应用程序的 URL 和主机域。不幸的是,该 URL 和域尚不存在,因为我们尚未将应用程序部署到 Azure App Service。但这不成问题——我们可以使用占位符 URL 注册应用程序,然后在部署应用程序后更新注册。

使用 Spring Initializr 创建应用程序

现在我们已经注册了应用程序,是时候构建它了。Spring 提供了 Spring Boot Initializr 工具,该工具创建一个 Maven 包,我们可以将其导入 Eclipse。

通过打开浏览器并访问 https://start.spring.io/ 来运行该工具。然后,填写项目详细信息和元数据。包括以下依赖项

  • Spring Web
  • Thymeleaf
  • Azure Active Directory
  • OAuth2 Client
  • Spring Boot DevTools

点击 **生成** 创建一个 ZIP 文件,其中包含完整的 Maven 基础架构和初始运行时类。将资源解压缩到您的项目目录中,然后将该目录作为现有的 Maven 项目导入到 Eclipse 中。

在 Eclipse 中,选择 **文件** > **导入** 以显示此对话框

选择 **现有 Maven 项目** 并单击 **下一步** 以显示此对话框

选择您解压缩从 Spring Initializr 下载的文件所在的目录,勾选项目旁边的复选框,然后单击 **完成**。

导入项目后,通过将此依赖项添加到 pom.xml 文件中来启用 Bootstrap CSS

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>4.0.0-2</version>
</dependency>

完整的项目可在 GitHub 上获得。

准备核心 Java 代码

C# 示例使用 Razor MVC 框架,但我已将其精简为最小实现。此最小版本强调了您进行 SSO 所需的内容,并使您能够添加 Web 应用程序所需的内容。因此,这些类与 C# 文件对齐,支持应用程序

Java 类文件 C# 类源文件
AppConfiguration.java Startup.cs
PersonalTabSsoApplication.java Program.cs
HomeController.java HomeController.cs
AuthController.java AuthController.cs
SSOAuthHelper.java SSOAuthHelper.cs

当 Spring Boot 启动我们的应用程序时,它会在 PersonalTabSsoApplication 类中运行 main。此类使用 Spring Boot 启动器提供的默认类实现。

AppConfiguration 类配置 Spring Boot 引擎。我们必须记住,Teams 在 iframe 中启动我们的应用程序。此外,Azure App Service 使用的 Web 应用程序服务器强制执行跨框架脚本安全,因此我们需要添加代码以允许我们的应用程序运行。整个类如下

package com.contentlab.teams.java.personaltabsso;
 
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 
@Configuration
@Order(value = 0)
public class AppConfiguration extends WebSecurityConfigurerAdapter
{ 
@Override
public void configure(HttpSecurity http) throws Exception
{
http.headers()
.contentSecurityPolicy(
"frame-ancestors 'self' https://*.microsoftonline.com  https://*.microsoft.com;"
);
}
}

Mozilla 指出,content-security-policy 标头已淘汰 X-FRAME-OPTIONS 标头,因此我在本应用程序中实现了较新的方法。

HomeControllerindex 页面以及 SSO 流程所需的 /Auth/StartAuth/End URL 提供映射和处理程序。我使用 @Controller 注释此类,因为响应包含网页。

AuthController 类为 GetUserAccessToken URL 提供映射和处理程序。我使用 @RestController 注释此类,因为它向调用者提供文本响应。auth.js 中的代码在 SSO 序列用用户 ID 令牌交换用户访问令牌时获取此 URL。

SsoAuthHelper 类提供 GetAccessTokenOnBehalfUser,由 AuthController.GetUserAccessToken 调用。此方法负责代表已通过身份验证的用户访问 Teams 资源。它通过传递的 env 参数从 applications.properties 文件中获取几个关键值

String clientId = env.getProperty(ClientIdConfigurationSettingsKey); 
String tenantId = env.getProperty(TenantIdConfigurationSettingsKey); 
String clientSecret = env.getProperty(AppsecretConfigurationSettingsKey); 
String issuerUrl = env.getProperty(AzureAuthUrlConfigurationSettingsKey);
String instancerUrl = env.getProperty(AzureInstanceConfigurationSettingsKey);
String azureScopes = env.getProperty(AzureScopes);

这些值从 Azure 中的多个来源获取

Location
clientId 在 Azure 中注册您的应用程序时创建
tenantId 由 Azure 分配。您可以在 Azure Active Directory 服务中的默认目录概述页面上找到它
clientSecret 在 Azure 中注册您的应用程序时创建
issuerUrl 硬编码/oauth2/v2.0/token
instancerUrl 硬编码https://login.microsoftonline.com
azureScopes 硬编码https://graph.microsoft.com/User.Read

稍后在介绍更新 application.properties 文件时,您将看到所有这些值。最后三个值与 Microsoft Graph API 定义的服务相关。

获取令牌的过程开始于构建 POST 请求的 URL

URL url = new URL(instancerUrl + "/" + tenantId + issuerUrl);

这会解析为

https://login.microsoftonline.com/<<your-tenant-ID>>/oauth2/v2.0/token

请求的正文是以下表单编码数据

String body = "assertion=" + idToken 
+ "&requested_token_use=on_behalf_of"
+ "&grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" 
+ "&client_id=" + clientId + "@" + tenantId 
+ "&client_secret=" + clientSecret 
+ "&scope=" + azureScopes;

为了了解 GetAccessTokenOnBehalfUser 如何处理对 POST 请求的响应,我们需要检查调用函数是如何工作的。这是 auth.js 中处理响应的相关代码片段

.then((responseJson) => {
                    if (IsValidJSONString(responseJson)) {
                        console.log("valid responseJson: " + responseJson);
                    	var parseResult = JSON.parse(responseJson);
                        if (JSON.parse(responseJson).error)
                           reject(JSON.parse(responseJson).error);
                    } else if (responseJson) {
                        accessToken = responseJson;
                        console.log("Exchanged token: " + accessToken);
                        getUserInfo(context.userPrincipalName);
                        getPhotoAsync(accessToken);
                    }

如下所示,当 Teams 以 HTTP 200 状态响应时,GetAccessTokenOnBehalfUser 会从响应中提取 JSON Web 令牌 (JWT) 并返回令牌。返回的值无效 JSON(它是有效字符串,但不是有效 JSON)。

if (http.getResponseCode() == HttpURLConnection.HTTP_OK)
{
InputStream is = http.getInputStream();
String responseBody = new String(is.readAllBytes(),
 StandardCharsets.UTF_8);
is.close();

ObjectMapper mapper = new ObjectMapper();
JsonNode actualObj = mapper.readTree(responseBody);
 
JsonNode accessToken = actualObj.get("access_token");
retval = accessToken.asText();
}
else
{
try
{
InputStream is = http.getInputStream();
retval = new String(is.readAllBytes(), StandardCharsets.UTF_8);
is.close();
}
catch(Exception e)
{
try
{
InputStream errIs = http.getErrorStream();
retval = new String(errIs.readAllBytes(),
 StandardCharsets.UTF_8);
errIs.close();
}
catch(Exception ex)
{
retval += " Can't read either input or error stream: "
 + ex.getMessage();					
}					
}

throw new Exception(retval);
}

通常,如上面的 else 情况所示,如果 Teams 返回错误,请检查 input 流。如果为空,则会通过抛出异常而失败,因此请检查 error 流。

具体来说,Teams 返回 HTTP 400 “无效请求”错误状态以指示身份验证问题。在这种情况下,输入流将为空,并且错误流将包含指示“无效授权”错误的有效 JSON 数据。

GetAccessTokenOnBehalfUser 仅在发生错误时才返回有效的 JSON 对象。在上面的 auth.js 代码片段中,这会导致 reject 行执行

reject(JSON.parse(responseJson).error)

反过来,这会导致应用程序显示一个“身份验证”按钮,使用户能够登录。否则,当返回令牌时,该令牌不是有效的 JSON,并且 auth.js 调用 getUserInfogetPhotoAsync

准备网页

将客户端代码实现为一个名为 auth.js 的 JavaScript 文件和三个 HTML 文件,它们对应于以下 .cshtml 文件

Java 应用程序 HTML 文件 C# HTML 源文件
index.html Home/index.cshtml
/Auth/Start.html Auth/Start.cshtml
/Auth/End.html Auth/End.cshtml

index.html 网页包含指向 auth.js 的链接以及一些 HTML 来显示有关已通过身份验证的用户的信息。我们提供此信息以验证身份验证是否已发生。index 页面还提供一个按钮,用户可以在需要时单击它来启动身份验证过程。如果当前身份验证时间已过期,用户可能需要执行此操作。

Auth/Start.htmlAuth/End.html 文件提供启动 OAuth 2 流程的代码。Start.html 调用 https://login.microsoftonline.com/common/oauth2/authorize 并将 /Auth/End.html 的 URL 作为回调。此调用会触发 OAuth 2 流程,完成后,/Auth/End.html 将收到错误或访问令牌。

添加支持 JavaScript

auth.js 文件处理 OAuth 2 过程。此文件直接从 Microsoft C# 示例中提取,以 jQuery onready 处理程序开始,该处理程序在页面加载开始后尽快运行。

此处理程序初始化 Microsoft Teams SDK 并调用 getClientSideToken。如果该调用成功,并且返回了身份验证令牌,getClientSideToken 将调用 getServerSideToken

如果该操作成功,getServerSideToken 将调用 getUserInfogetPhotoAsync,它们会在 index 页面上提供用户信息。您的应用程序需要修改 getServerSideToken 以提供您的初始表示。

部署应用程序

Maven 提供了创建 Azure App Service 计划并将其部署到那里的工具。要使用这些工具,首先,打开一个 MS-DOS 命令提示符(cmd)窗口。将目录(cd)更改为包含 pom.xml 文件的项目目录。然后,执行以下命令

  • az login --tenant 26e17e8f-8a99-44f3-a027-54de7b19f3af
  • mvn com.microsoft.azure:azure-webapp-maven-plugin:2.2.0:config
  • mvn package azure-webapp:deploy

az login 命令将您登录到 Azure,以便 Maven 可以创建资源。成功的结果如下所示

Z:\...\PersonalTabSSO>az login --tenant <<Your tenancy ID goes here>>
The default web browser has been opened at https://login.microsoftonline.com/26...af/oauth2/authorize. Please continue the login in the web browser. If no web browser is available or if the web browser fails to open, use device code flow with `az login --use-device-code`.
You have logged in. Now let us find all the subscriptions to which you have access...
[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "26...af",
    "id": "a3...2e",
    "isDefault": true,
    "managedByTenants": [],
    "name": "AzureJava",
    "state": "Enabled",
    "tenantId": "26...af",
    "user": {
      "name": "j<"your user ID appears here,
      "type": "user"
    }
  }
]

第一个 mvn 命令捕获创建 Azure App Service 计划所需的信息,并将其存储在 pom.xml 中。成功响应如下所示

Z:\...\PersonalTabSSO>mvn com.microsoft.azure:azure-webapp-maven-plugin:2.2.0:config
[INFO] Scanning for projects...
[INFO]
[INFO] --------------< com.contentlab.teams.java:PersonalTabSSO >--------------
[INFO] Building PersonalTabSSO 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- azure-webapp-maven-plugin:2.2.0:config (default-cli) @ PersonalTabSSO ---
Auth type: AZURE_CLI
Default subscription: AzureJava(a3...2e)
Username: <<your user ID>>
[INFO] Subscription: AzureJava(a3...2e)
[INFO] It may take a few minutes to load all Java Web Apps, please be patient.
Java SE Web Apps in subscription AzureJava:
* 1: <create>
Please choose a Java SE Web App [<create>]:
Define value for OS [Linux]:
  1: Windows
* 2: Linux
  3: Docker
Enter your choice: 2
Define value for javaVersion [Java 8]:
* 1: Java 8
  2: Java 11
Enter your choice: 2
Define value for pricingTier [P1v2]:
   1: B1
   2: B2
   3: B3
   4: D1
   5: EP1
   6: EP2
   7: EP3
   8: F1
*  9: P1v2
  10: P1v3
  11: P2v2
  12: P2v3
  13: P3v2
  14: P3v3
  15: S1
  16: S2
  17: S3
  18: Y1
Enter your choice: 8
Please confirm webapp properties
Subscription Id : a3...2e
AppName : PersonalTabSSO-1635023142497
ResourceGroup : PersonalTabSSO-1635023142497-rg
Region : westus
PricingTier : F1
OS : Linux
Java : Java 11
Web server stack: Java SE
Deploy to slot : false
Confirm (Y/N) [Y]: y
[INFO] Saving configuration to pom.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 52.517 s
[INFO] Finished at: 2021-10-23T17:05:59-04:00
[INFO] ------------------------------------------------------------------------

第二个 mvn 命令将应用程序部署到此计划。成功的结果如下所示

Z:\...\PersonalTabSSO>mvn package azure-webapp:deploy
[INFO] Scanning for projects...
[INFO]
[INFO] --------------< com.contentlab.teams.java:PersonalTabSSO >--------------
[INFO] Building PersonalTabSSO 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ PersonalTabSSO ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 4 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ PersonalTabSSO ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ PersonalTabSSO ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] skip non existing resourceDirectory Z:\...\PersonalTabSSO\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ PersonalTabSSO ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ PersonalTabSSO ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests
18:04:17.121 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating CacheAwareContextLoaderDelegate from class [org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate]
18:04:17.475 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating BootstrapContext using constructor [public org.springframework.test.context.support.DefaultBootstrapContext(java.lang.Class,org.springframework.test.context.CacheAwareContextLoaderDelegate)]
18:04:17.889 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating TestContextBootstrapper for test class [com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests] from class [org.springframework.boot.test.context.SpringBootTestContextBootstrapper]
18:04:18.073 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests], using SpringBootContextLoader
18:04:18.113 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests]: class path resource [com/contentlab/teams/java/PersonalTabSSO/PersonalTabSsoApplicationTests-context.xml] does not exist
18:04:18.128 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests]: class path resource [com/contentlab/teams/java/PersonalTabSSO/PersonalTabSsoApplicationTestsContext.groovy] does not exist
18:04:18.128 [main] INFO org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests]: no resource found for suffixes {-context.xml, Context.groovy}.
18:04:18.128 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils - Could not detect default configuration classes for test class [com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests]: PersonalTabSsoApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
18:04:18.812 [main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests]
18:04:19.650 [main] DEBUG org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider - Identified candidate component class: file [Z:\...\PersonalTabSSO\target\classes\com\contentlab\teams\java\PersonalTabSSO\PersonalTabSsoApplication.class]
18:04:19.663 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Found @SpringBootConfiguration com.contentlab.teams.java.personaltabsso.PersonalTabSsoApplication for test class com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests
18:04:21.312 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - @TestExecutionListeners is not present for class [com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests]: using defaults.
18:04:21.312 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.event.ApplicationEventsTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener]
18:04:21.641 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/TransactionDefinition]
18:04:21.657 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttribute]
18:04:21.657 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@a8e6492, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@1c7fd41f, org.springframework.test.context.event.ApplicationEventsTestExecutionListener@3b77a04f, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@7b324585, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@2e11485, org.springframework.test.context.support.DirtiesContextTestExecutionListener@60dce7ea, org.springframework.test.context.event.EventPublishingTestExecutionListener@662f5666, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@fd8294b, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@5974109, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@27305e6, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@1ef3efa8, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@502f1f4c, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener@6f8f9349]
18:04:21.782 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: context [DefaultTestContext@51cd7ffc testClass = PersonalTabSsoApplicationTests, testInstance = [null], testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@30d4b288 testClass = PersonalTabSsoApplicationTests, locations = '{}', classes = '{class com.contentlab.teams.java.personaltabsso.PersonalTabSsoApplication}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@16b2bb0c, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@c7045b9, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@11f0a5a1, org.springframework.boot.test.web.reactive.server.WebTestClientContextCustomizer@732d0d24, org.springframework.boot.test.autoconfigure.actuate.metrics.MetricsExportContextCustomizerFactory$DisableMetricExportContextCustomizer@13d4992d, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@2141a12, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@384ad17b], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true]], class annotated with @DirtiesContext [false] with mode [null].
18:04:22.399 [main] DEBUG org.springframework.test.context.support.TestPropertySourceUtils - Adding inlined properties to environment: {spring.jmx.enabled=false, org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}
 
  .   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/

:: Spring Boot ::                (v2.5.5)
 
2021-10-23 18:04:26.007  INFO 99040 --- [           main] c.c.t.j.P.PersonalTabSsoApplicationTests : Starting PersonalTabSsoApplicationTests using Java 11.0.3 on us-jgriffith2 with PID 99040 (started by jgriffith in Z:\...\PersonalTabSSO)
2021-10-23 18:04:26.022  INFO 99040 --- [           main] c.c.t.j.P.PersonalTabSsoApplicationTests : No active profile set, falling back to default profiles: default
2021-10-23 18:04:46.624  INFO 99040 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page template: index
2021-10-23 18:04:48.575  INFO 99040 --- [           main] c.a.s.a.aad.AADAuthenticationProperties  : AzureADJwtTokenFilter Constructor.
2021-10-23 18:04:51.349  INFO 99040 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@760f1081, org.springframework.security.web.context.SecurityContextPersistenceFilter@2baac4a7, org.springframework.security.web.header.HeaderWriterFilter@e0d1dc4, org.springframework.security.web.csrf.CsrfFilter@5fe46d52, org.springframework.security.web.authentication.logout.LogoutFilter@59328218, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@25f0c5e7, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5aea8994, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@52621501, org.springframework.security.web.session.SessionManagementFilter@3db6dd52, org.springframework.security.web.access.ExceptionTranslationFilter@1d60059f]
2021-10-23 18:04:53.236  INFO 99040 --- [           main] c.c.t.j.P.PersonalTabSsoApplicationTests : Started PersonalTabSsoApplicationTests in 30.741 seconds (JVM running for 39.552)
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 42.505 s - in com.contentlab.teams.java.PersonalTabSSO.PersonalTabSsoApplicationTests
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ PersonalTabSSO ---
[INFO]
[INFO] --- spring-boot-maven-plugin:2.5.5:repackage (repackage) @ PersonalTabSSO ---
[INFO] Replacing main artifact with repackaged archive
[INFO]
[INFO] --- azure-webapp-maven-plugin:2.2.0:deploy (default-cli) @ PersonalTabSSO ---
Auth type: AZURE_CLI
Default subscription: AzureJava(a3100fd0-81b9-46d0-b470-f2318b4f452e)
Username: jeff.griffith@gmail.com
[INFO] Subscription: AzureJava(a3100fd0-81b9-46d0-b470-f2318b4f452e)
[INFO] Creating web app PersonalTabSSO-1635023142497...
[INFO] Creating app service plan asp-PersonalTabSSO-1635023142497...
[INFO] Successfully created app service plan asp-PersonalTabSSO-1635023142497.
[INFO] Successfully created Web App PersonalTabSSO-1635023142497.
[INFO] Trying to deploy external resources to PersonalTabSSO-1635023142497...
[INFO] Successfully deployed the resources to PersonalTabSSO-1635023142497
[INFO] Trying to deploy artifact to PersonalTabSSO-1635023142497...
[INFO] Deploying (Z:\...\PersonalTabSSO\target\PersonalTabSSO-0.0.1-SNAPSHOT.jar)[jar]  ...
[INFO] Successfully deployed the artifact to https://personaltabsso-1635023142497.azurewebsites.net
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 03:00 min
[INFO] Finished at: 2021-10-23T18:07:08-04:00
[INFO] ------------------------------------------------------------------------

修复应用注册和 application.properties 文件

现在我们已经部署了应用程序,我们需要返回 Azure Active Directory 应用注册门户并更新应用注册——请记住,我们之前在那里放入了占位符信息。此过程有四个部分:捕获应用服务 URL、更新应用注册、更新 application.properties 文件以及重新部署应用程序。

部署代码将显示应用服务 URL

[INFO] Successfully deployed the artifact to https://personaltabsso-1635023142497.azurewebsites.net

要更新应用注册中的 URL,请登录 Azure 门户并导航到您之前创建的应用注册。单击 **重定向 URL** 链接。

更新 **重定向 URL**,确保在 **隐式授权和混合流** 部分勾选两个复选框,然后单击 **保存**。

您还需要在 **公开 API** 刀片上更新 **应用程序 ID URI**,如下面的屏幕截图所示

现在,设置 applications.properties 文件,以便应用程序拥有这些信息

# Specifies your Active Directory ID:
azure.activedirectory.tenant-id=<< Your Tenant ID >>
# Specifies your App Registration's Application ID:
azure.activedirectory.client-id=<< Your Application ID >>
# Specifies your App Registration's secret key:
azure.activedirectory.client-secret=<< Your application secret >>
azure.auth.url=/oauth2/v2.0/token
azure.instance=https://login.microsoftonline.com
azure.api=api://personaltabsso-1635023142497.azurewebsites.net/<<YourApplicationID>>
azure.scopes=https://graph.microsoft.com/User.Read
 
spring.thymeleaf.prefix=classpath:/templates/

您需要将 azure.api 设置中的主机名更改为与 Azure App Service 中的 URL 主机名匹配。另外,请确保设置 spring.thymeleaf.prefix 设置,以便 Thymeleaf 可以找到网页。

现在,保存更新并重新部署应用程序。

在 Teams 中测试应用程序

要运行此应用程序,我创建一个清单,然后使用此清单将应用程序导入 Teams。Teams 提供 App Studio 工具来构建清单,但由于 Microsoft 将在 2022 年弃用此工具,我使用 Microsoft 示例中的 appPackage/manifest.json 文件作为起点来生成我的 manifest.json。Microsoft 在其网站上记录了此清单的格式。

{
    "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.11/MicrosoftTeams.schema.json",
    "manifestVersion": "1.11",
    "version": "1.0.0",
    "id": "5e443522-1974-404b-a67b-3cc939681be1",
    "packageName": "com.contentlab.teams.java.personaltabsso",
    "developer": {
    "name": "Microsoft",
    "websiteUrl": "https://www.microsoft.com",
    "privacyUrl": "https://www.microsoft.com/privacy",
    "termsOfUseUrl": "https://www.microsoft.com/termsofuse"
    },
    "name": {
        "short": "Java Personal Tab SSO",
        "full": "Java Personal Tab SSO"
    },
    "description": {
        "short": "Java Personal Tab SSO",
        "full": "Java Personal Tab SSO"
    },
    "icons": {
        "color": "color.png",
        "outline": "outline.png"
    },
    "accentColor": "#FFFFFF",
    "staticTabs": [
        {
            "entityId": "Java Personal Tab SSO",
            "name": "Java Personal Tab SSO",
            "contentUrl": "https://personaltabsso-1635023142497.azurewebsites.net",
            "scopes": [ "personal" ]
        },
        {
            "entityId": "about",
            "scopes": [ "personal" ]
        }
    ],
    "permissions": [ "identity", "messageTeamMembers" ],
    "validDomains": [
        "personaltabsso-1635023142497.azurewebsites.net",
        "*.onmicrosoft.com",
        "*.azurewebsites.net"
    ],
    "webApplicationInfo": {
        "id": "2ffed052-5f43-42dd-988c-0072b07b6da9",
        "resource": "api://personaltabsso-1635023142497.azurewebsites.net/2ffed052-5f43-42dd-988c-0072b07b6da9"
  }
}

staticTabs 对象的范围设置为 personal。此设置至关重要,因为它表明此应用程序仅供您使用,并且不会共享。我将在下一篇文章中演示一个组应用程序,您将在其中看到这些值会发生变化。

请注意,Azure App Service URL 中的主机名会出现在此文件中的多个位置。此外,webApplicationInfo 对象中 id 字段的值来自应用程序注册,并且它位于 application.properties 文件中的 azure.activedirectory.client-id 值。

准备好 manifest.json 文件后,创建清单包。此包是一个 ZIP 文件,其中包含 manifest.json 文件以及彩色和轮廓图标

所有文件都在 ZIP 文件的根目录中。请勿在此文件中包含任何文件夹。

现在,我们将 manifest.zip 文件上传到 Teams 并添加应用程序。

首先,在 Teams 中,单击工具栏(左下角)上的 **应用** 图标,向下滚动以显示 **上传自定义应用** 链接,然后单击它。接下来,单击 **为我的组织上传** 链接。然后,选择 manifest.zip 文件

此操作将上传 ZIP 文件,使应用在 Teams 中可用。

接下来单击应用,然后单击 **添加**。

当您单击 **添加** 时,可能需要等待 Azure App Service 启动。此过程可能需要几分钟,并且应用程序可能看起来已失败。您可以通过检查左下角的状态消息来确保它仍在工作

当应用程序准备就绪时,您将看到以下屏幕

后续步骤

就是这样。您现在拥有一个可正常运行的安全的个人选项卡应用程序的框架。由于 Teams 应用程序只是 iframe 中的一个 Web 应用程序,因此您的个人选项卡应用程序可以做任何您想做的事情。您所要做的就是替换服务器端应用程序。

您当前的 Java 技能和工具在构建这些应用程序时非常有用。Microsoft 提供了与 Teams 交互所需的所有库的 Java 版本,其余的部署是标准的 Web 应用程序开发。

当您注册此应用程序时,您创建了一个密钥。该密钥是创建应用程序身份的一种方式。本教程中的应用程序使用该密钥在交换客户端令牌以获取 Teams 令牌时登录 Azure。您可以使用此方法来保护对所有 Azure 资源的访问,例如数据库和密钥保管库。因此,您可以从托管 Teams 的任何设备保护和访问整个应用程序链。

我将在本系列的后续两篇文章中使用此过程来重新实现 C# 示例为 Java。此过程提供了对整个库的访问。继续阅读本系列文章的第二篇,以探索使用 SSO 创建频道或组选项卡。

© . All rights reserved.