创建使用单一登录的 Node.js Office 加载项

用户可以登录 Office,Office Web 加载项能够利用此登录进程,授权用户访问加载项和 Microsoft Graph,而无需要求用户再登录一次。 有关概述,请参阅在 Office 加载项中启用 SSO

本文将指导你完成在加载项中启用单一登录 (SSO) 的过程。 创建的示例外接程序包含两个部分:在 Microsoft Excel 中加载的任务窗格,以及处理任务窗格对 Microsoft Graph 的调用的中间层服务器。 中间层服务器使用 Node.js 和 Express 构建, /getuserfilenames并公开单个 REST API ,该 API 返回用户的 OneDrive 文件夹中的前 10 个文件名的列表。 任务窗格使用 getAccessToken() 方法获取已登录用户到中间层服务器的访问令牌。 中间层服务器使用代表流 (OBO) 将访问令牌交换为有权访问 Microsoft Graph 的新令牌。 可以扩展此模式以访问任何 Microsoft Graph 数据。 任务窗格始终调用中间层 REST API, (需要 Microsoft Graph 服务时) 传递访问令牌。 中间层使用通过 OBO 获取的令牌调用 Microsoft Graph 服务并将结果返回到任务窗格。

本文使用使用 Node.js 和 Express 的加载项。 有关与此类似的 ASP.NET 加载项文章,请参阅创建使用单一登录的 ASP.NET Office 加载项

先决条件

  • Node.js(最新的 LTS 版本)

  • Git Bash(或其他 git 客户端)

  • 代码编辑器 - 建议Visual Studio Code

  • Microsoft 365 订阅中至少存储在OneDrive for Business上的一些文件和文件夹

  • 支持 IdentityAPI 1.3 要求集 的 Microsoft 365 内部版本。 你可能有资格通过 Microsoft 365 开发人员计划获得Microsoft 365 E5开发人员订阅,其中包括开发人员沙盒;有关详细信息,请参阅常见问题解答开发人员沙盒包含 Microsoft Azure 订阅,可在本文后面的步骤中使用该订阅进行应用注册。 如果需要,可以使用单独的 Microsoft Azure 订阅进行应用注册。 在 Microsoft Azure 获取试用版订阅。

设置初学者项目

  1. 克隆或下载 Office 外接程序 NodeJS SSO 中的存储库。

    注意

    示例项目有两个版本:

    • Begin 文件夹是初学者项目。 未直接连接到 SSO 或授权的外接程序的 UI 和其他方面已经完成。 本文后续章节将引导你完成此过程。
    • Complete 文件夹包含相同的示例,已完成本文中的所有编码步骤。 若要使用已完成的版本,只需按照本文中的说明进行操作,但将“Begin”替换为“Complete”,并跳过编写 客户端代码编写中间层服务器端 代码部分。
  2. Begin 文件夹中打开命令提示符。

  3. 在该控制台中输入 npm install 以安装 package.json 文件中列出明细的所有依赖项。

  4. 运行命令 npm run install-dev-certs。 为安装证书的提示选择“”。

将以下值用于后续应用注册步骤的占位符。

占位符
<add-in-name> Office-Add-in-NodeJS-SSO
<fully-qualified-domain-name> localhost:3000
Microsoft Graph 权限 profile、openid、Files.Read

使用 Microsoft 标识平台 注册加载项

需要在 Azure 中创建表示 Web 服务器的应用注册。 这将启用身份验证支持,以便可以在 JavaScript 中向客户端代码颁发适当的访问令牌。 此注册既支持客户端中的 SSO,也支持使用 Microsoft 身份验证库 (MSAL) 进行回退身份验证。

  1. 使用 Microsoft 365 租户的管理员凭据登录到 Azure 门户。 例如,MyName@contoso.onmicrosoft.com

  2. 选择“应用注册”。 如果未看到图标,请在搜索栏中搜索“应用注册”。

    Azure 门户主页。

    将显示 应用注册 页。

  3. 选择“新注册”

    “应用注册”窗格中的新注册。

    将显示注册应用程序页。

  4. 在“注册应用”页上,按如下方式设置值。

    • 将“名称”设置为“<add-in-name>”。
    • 将“ 支持的帐户类型 ”设置为 “任何组织目录中的帐户 (任何 Azure AD 目录 - 多租户) 和个人 Microsoft 帐户 (,例如 Skype、Xbox)
    • 重定向 URI 设置为使用平台 单页应用程序 (SPA) ,并将 URI 设置为 https://<fully-qualified-domain-name>/dialog.html

    注册一个应用程序窗格,其中完成了名称和支持的帐户。

  5. 选择“注册”。 显示一条消息,指出已创建应用程序注册。

    指示已创建应用程序注册的消息。

  6. 复制并保存 应用程序 (客户端) ID目录 (租户) ID 的值。 你将在后面的过程中使用它们。

    显示客户端 ID 和目录 ID 的 Contoso 的应用注册窗格。

添加客户端密码

有时称为 应用程序密码,客户端密码是一个字符串值,你的应用可以使用它来代替证书来标识自身。

  1. 在左窗格中,选择“ 证书 & 机密”。 然后在“ 客户端机密 ”选项卡上,选择“ 新建客户端密码”。

    “证书 & 机密”窗格。

    此时会显示 “添加客户端机密 ”窗格。

  2. 添加客户端密码的说明。

  3. 选择机密的过期时间或指定自定义生存期。

    • 客户端机密生存期限制为两年 (24 个月) 或更短。 不能指定超过 24 个月的自定义生存期。
    • Microsoft 建议将过期值设置为小于 12 个月。

    添加客户端密码窗格,说明和过期已完成。

  4. 选择“添加”。 将创建新机密并临时显示值。

重要

记录要在 客户端应用程序代码中使用的机密值。 离开此窗格后 ,永远不会再次显示 此机密值。

公开 Web API

  1. 在左窗格中,选择“ 公开 API”。

    此时会显示 “公开 API ”窗格。

    应用注册的“公开 API”窗格。

  2. 选择“ 设置 ”以生成应用程序 ID URI。

    应用注册的“公开 API”窗格中的“设置”按钮。

    将显示用于设置应用程序 ID URI 的部分,其中以 格式 api://<app-id>显示生成的应用程序 ID URI。

  3. 将应用程序 ID URI 更新为 api://<fully-qualified-domain-name>/<app-id>

    编辑“应用 ID URI”窗格,并将 localhost 端口设置为 44355。

    • 应用程序 ID URI 以格式 api://<app-id> 预填充应用 ID (GUID)。
    • 应用程序 ID URI 格式应为: api://<fully-qualified-domain-name>/<app-id>
    • 插入 介于 fully-qualified-domain-nameapi://<app-id> (这是 GUID) 。 例如,api://contoso.com/<app-id>
    • 如果使用 localhost,则格式应为 api://localhost:<port>/<app-id>。 例如,api://localhost:3000/c6c1f32b-5e55-4997-881a-753cc1d563b7

    有关其他应用程序 ID URI 的详细信息,请参阅 应用程序清单 identifierUris 属性

    注意

    如果收到一条错误,指出域已有所有者,但你拥有该域,请按照快速入门: 将自定义域名添加到 Azure Active Directory 中的步骤进行操作来注册该域,然后重复此步骤。 (如果未使用 Microsoft 365 租户中管理员的凭据登录,也可能会出现此错误。请参阅步骤 2。注销并使用管理员凭据再次登录,并重复步骤 3.)

添加范围

  1. “公开 API ”页上,选择“ 添加范围”。

    选择“添加范围”按钮。

    此时会打开 “添加范围 ”窗格。

  2. “添加范围 ”窗格中,指定作用域的属性。 下表显示了 需要 、、 openidFiles.ReadWriteMail.Read 权限的 profile和 Outlook 外接程序的示例值。 修改文本以匹配外接程序所需的权限。

    字段 说明
    范围名称 范围的名称。 常见的范围命名约定是 resource.operation.constraint 对于 SSO,必须将其设置为 access_as_user
    谁可以同意 确定是否需要管理员同意,或者用户是否可以在未经管理员批准的情况下同意。 为了学习 SSO 和示例,建议将其设置为 管理员和用户

    对于更高特权的权限,请选择“ 仅管理员 ”。
    管理员同意显示名称 仅对管理员可见的范围用途的简短说明。 Read/write permissions to user files. Read permissions to user mail and profiles.
    管理员同意说明 由范围授予的权限的更详细说明,仅供管理员查看。 Allow Office to have read/write permissions to all user files and read permissions to all user mail. Office can call the app's web APIs as the current user.
    用户同意显示名称 范围用途的简短说明。 仅当将 “谁可以同意 ”设置为 “管理员和用户”时,才会向用户显示。 Read/write permissions to your files. Read permissions to your mail and profile.
    用户同意说明 范围授予的权限的更详细说明。 仅当将 “谁可以同意 ”设置为 “管理员和用户”时,才会向用户显示。 Allow Office to have read/write permissions to your files, and read permissions to your mail and profile.
  3. “状态 ”设置为 “已启用”,然后选择“ 添加范围”。

    将状态设置为“启用”,然后选择“添加范围”按钮。

    定义的新范围将显示在窗格中。

    “公开 API”窗格上显示的新范围。

    注意

    显示在文本字段正下方的“作用域名称”的域部分应自动与上一步骤中设置的“应用 ID URI”匹配,并将 /access_as_user 附加到末尾;例如,api://localhost:6789/c6c1f32b-5e55-4997-881a-753cc1d563b7/access_as_user

  4. 选择添加客户端应用程序

    选择“添加客户端应用程序”。

    此时会显示 “添加客户端应用程序 ”窗格。

  5. “客户端 ID” 中输入 ea5a67f6-b6f3-4338-b240-c655ddc3cc8e。 此值预授权所有 Microsoft Office 应用程序终结点。 如果还希望在 Microsoft Teams 中使用 Office 时预先授权 Office,请添加 1fec8e78-bce4-4aaf-ab1b-5451cc387264 (Microsoft Teams 桌面和 Teams 移动) ,并在 5e3ce6c0-2b1f-4285-8d4b-75ee78787346 Web) 上 (Teams。

    注意

    ea5a67f6-b6f3-4338-b240-c655ddc3cc8e ID 在以下所有平台上预授权 Office。 或者,如果出于任何原因想要在某些平台上拒绝对 Office 的授权,则可以输入以下 ID 的正确子集。 如果这样做,请省略要从中扣留授权的平台的 ID。 这些平台上加载项的用户将无法调用 Web API,但外接程序中的其他功能仍可正常工作。

    • d3590ed6-52b3-4102-aeff-aad2292ab01c (Microsoft Office)
    • 93d53678-613d-4013-afc1-62e9e444a0a5(Office 网页版)
    • bc59ab01-8403-45c6-8796-ac3ef710b3e3(Outlook 网页版)
  6. “授权范围”中,选中复选框 api://<fully-qualified-domain-name>/<app-id>/access_as_user

  7. 选择“添加应用程序”

    “添加客户端应用程序”窗格。

添加 Microsoft Graph 权限

  1. 在左窗格中,选择“ API 权限”。

    “API 权限”窗格。

    API 权限 ”窗格随即打开。

  2. 选择“添加权限”。

    在“API 权限”窗格中添加权限。

    此时会打开 “请求 API 权限 ”窗格。

  3. 选择 Microsoft Graph

    带有 Microsoft Graph 按钮的“请求 API 权限”窗格。

  4. 选择“委托的权限”。

    具有委托权限按钮的“请求 API 权限”窗格。

  5. “选择权限” 搜索框中,搜索外接程序所需的权限。 例如,对于 Outlook 外接程序,可以使用 profileopenidFiles.ReadWriteMail.Read

    注意

    User.Read 权限可能已默认列出。 最好只请求所需的权限,因此,如果加载项实际上不需要此权限,我们建议取消选中此权限框。

  6. 选中每个权限显示的复选框。 请注意,在选择每个权限时,这些权限不会在列表中保持可见。 选择加载项所需的权限后,选择“ 添加权限”。

    “请求 API 权限”窗格,其中选择了一些权限。

  7. 选择“ 授予管理员同意[租户名称]”。 对于显示的确认,请选择“ ”。

配置访问令牌版本

必须定义应用可接受的访问令牌版本。 此配置是在 Azure Active Directory 应用程序清单中进行的。

定义访问令牌版本

如果你在任何组织目录中选择了帐户类型以外的帐户类型 (任何 Azure AD 目录 - 多租户) 和个人 Microsoft 帐户 (,例如 Skype、Xbox) ,则访问令牌版本可能会更改。 使用以下步骤确保访问令牌版本适用于 Office SSO 用法。

  1. 在左窗格中,选择“ 清单”。

    选择“Azure 清单”。

    此时会显示 Azure Active Directory 应用程序清单。

  2. 输入 2 作为 accessTokenAcceptedVersion 属性的值。

    接受访问令牌版本的值。

  3. 选择“保存”。

    浏览器上弹出一条消息,指出清单已成功更新。

    清单更新的消息。

祝贺你! 你已完成应用注册,以便为 Office 加载项启用 SSO。

配置加载项

  1. 在代码编辑器中打开克隆项目中的 \Begin 文件夹。

  2. .ENV打开 文件,并使用之前从 Office-Add-in-NodeJS-SSO 应用注册复制的值。 按如下所示设置值:

    名称
    CLIENT_ID 应用程序注册概述页中的应用程序 (客户端) ID
    CLIENT_SECRET “证书 & 机密”页保存的客户端密码。

    该值能用引号引起来。 完成后,文件应当类似于以下示例:

    CLIENT_ID=8791c036-c035-45eb-8b0b-265f43cc4824
    CLIENT_SECRET=X7szTuPwKNts41:-/fa3p.p@l6zsyI/p
    NODE_ENV=development
    SERVER_SOURCE=<https://localhost:3000>
    
  3. 打开加载项清单文件“manifest\manifest_local.xml”,然后滚动到该文件的底部。 在结束标记的 </VersionOverrides> 正上方,你将找到以下标记。

    <WebApplicationInfo>
      <Id>$app-id-guid$</Id>
      <Resource>api://localhost:3000/$app-id-guid$</Resource>
      <Scopes>
          <Scope>Files.Read</Scope>
          <Scope>profile</Scope>
          <Scope>openid</Scope>
      </Scopes>
    </WebApplicationInfo>
    
  4. 将标记中两个位置的占位符“$app-id-guid$”替换为创建 Office-Add-in-NodeJS-SSO 应用注册时复制的应用程序 ID。 “$”符号不是 ID 的一部分,因此请勿包含它们。 此 ID 与在 中用于CLIENT_ID的 ID 相同。ENV 文件。

    注意

    “资源”<>值是在注册外接程序时设置的应用程序 ID URI。 如果加载项是通过 AppSource 销售的,则 <“作用域> ”部分仅用于生成同意对话框。

  5. 打开 \public\javascripts\fallback-msal\authConfig.js文件。 将占位符“$app-id-guid$”替换为之前创建的 Office-Add-in-NodeJS-SSO 应用注册中保存的应用程序 ID。

  6. 保存对文件所做的更改。

编写客户端代码

调用 Web 服务器 REST API

  1. 在代码编辑器中,打开文件 public\javascripts\ssoAuthES6.js。 它已有确保 Promises 受支持的代码,即使在 Trident (Internet Explorer 11) Webview 控件中,也 Office.onReady 调用了将处理程序分配给外接程序的唯一按钮。

    注意

    顾名思义,ssoAuthES6.js 使用 JavaScript ES6 语法,因为使用 asyncawait 可以最好地显示 SSO API 本质的简单性。 当 localhost 服务器启动时,此文件将转译为 ES5 语法,以便示例支持 Trident。

  2. getFileNameList 函数中,将 TODO 1 替换为下列代码。 关于此代码,请注意以下几点:

    • 当用户在任务窗格上选择“获取 OneDrive 文件名”按钮时,将调用 函数getFileNameList
    • 它调用函数, callWebServerAPI 指定要调用的 REST API。 这将返回包含用户 OneDrive 中文件名列表的 JSON。
    • JSON 将传递给 函数以 writeFileNamesToOfficeDocument 列出文档中的文件名。
    try {
        const jsonResponse = await callWebServerAPI('GET', '/getuserfilenames');
        if (jsonResponse === null) {
            // Null is returned when a message was displayed to the user
            // regarding an authentication error that cannot be resolved.
            return;
        }
        await writeFileNamesToOfficeDocument(jsonResponse);
        showMessage('Your OneDrive filenames are added to the document.');
    } catch (error) {
        console.log(error.message);
        showMessage(error.message);
    }
    
  3. callWebServerAPI 函数中,将 TODO 2 替换为下列代码。 关于此代码,请注意以下几点:

    • 函数调用 getAccessToken 我们自己的函数,根据需要使用 Office SSO 或 MSAL 回退封装以获取令牌。 如果它返回 null 令牌,则会针对无法解决的身份验证错误条件显示一条消息,因此该函数也返回 null。
    • 函数使用 fetch API 调用 Web 服务器,如果成功,则返回 JSON 正文。
    const accessToken = await getAccessToken(authSSO);
    if (accessToken === null) {
        return null;
    }
    const response = await fetch(path, {
        method: method,
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + accessToken,
        },
    });
    
    // Check for success condition: HTTP status code 2xx.
    if (response.ok) {
        return response.json();
    }
    
  4. callWebServerAPI 函数中,将 TODO 3 替换为下列代码。 关于此代码,请注意以下几点:

    • 此代码处理 SSO 令牌过期的情况。 如果是这样,我们需要调用 Office.auth.getAccessToken 来获取刷新的令牌。 最简单的方法是进行递归调用,这将导致对 Office.auth.getAccessToken的新调用。 参数 retryRequest 确保递归调用只尝试一次。
    • 每当 TokenExpiredError 检测到过期的令牌时,该字符串就由 Web 服务器设置。
     // Check for fail condition: Is SSO token expired? If so, retry the call which will get a refreshed token.
    const jsonBody = await response.json();
    if (
        authSSO === true &&
        jsonBody != null &&
        jsonBody.type === 'TokenExpiredError'
    ) {
        if (!retryRequest) {
            return callWebServerAPI(method, path, true); // Try the call again. The underlying call to Office JS getAccessToken will refresh the token.
        } else {
            // Indicates a second call to retry and refresh the token failed.
            authSSO = false;
            return callWebServerAPI(method, path, true); // Try the call again, but now using MSAL fallback auth.
        }
    }
    
  5. callWebServerAPI 函数中,将 TODO 4 替换为下列代码。 关于此代码,请注意以下几点:

    • Microsoft Graph每当 Microsoft Graph 调用失败时,该字符串由 Web 服务器设置。
    // Check for fail condition: Did we get a Microsoft Graph API error, which is returned as bad request (403)?
    if (response.status === 403 && jsonBody.type === 'Microsoft Graph') {
        throw new Error('Microsoft Graph error: ' + jsonBody.errorDetails);
    }
    
  6. callWebServerAPI 函数中,将 TODO 5 替换为下列代码。

    // Handle other errors.
    throw new Error(
        'Unknown error from web server: ' + JSON.stringify(jsonBody)
    );
    
  7. getAccessToken 函数中,将 TODO 6 替换为下列代码。 关于此代码,请注意以下几点:

    • authSSO 跟踪我们使用的是 SSO,还是使用 MSAL 回退。 如果使用 SSO,则函数调用 Office.auth.getAccessToken 并返回令牌。
    • 错误由 函数处理, handleSSOErrors 如果函数切换到回退 MSAL 身份验证,该函数将返回令牌。
    • 回退身份验证使用 MSAL 库来登录用户。 加载项本身是一个 SPA,使用 SPA 应用注册来访问 Web 服务器。
    if (authSSO) {
        try {
            // Get the access token from Office host using SSO.
            // Note that Office.auth.getAccessToken modifies the options parameter. Create a copy of the object
            // to avoid modifying the original object.
            const options = JSON.parse(JSON.stringify(ssoOptions));
            const token = await Office.auth.getAccessToken(options);
            return token;
        } catch (error) {
            console.log(error.message);
            return handleSSOErrors(error);
        }
    } else {
        // Get access token through MSAL fallback.
        try {
            const accessToken = await getAccessTokenMSAL();
            return accessToken;
        } catch (error) {
            console.log(error);
            throw new Error(
                'Cannot get access token. Both SSO and fallback auth failed. ' +
                    error
            );
        }
    }
    
  8. handleSSOErrors 函数中,将 TODO 7 替换为下列代码。 有关这些错误的详细信息,请参阅对 Office 加载项中的 SSO 进行故障排除

    switch (error.code) {
        case 13001:
            // No one is signed into Office. If the add-in cannot be effectively used when no one
            // is logged into Office, then the first call of getAccessToken should pass the
            // `allowSignInPrompt: true` option. Since this sample does that, you should not see
            // this error.
            showMessage(
                'No one is signed into Office. But you can use many of the add-ins functions anyway. If you want to log in, press the Get OneDrive File Names button again.'
            );
            break;
        case 13002:
            // The user aborted the consent prompt. If the add-in cannot be effectively used when consent
            // has not been granted, then the first call of getAccessToken should pass the `allowConsentPrompt: true` option.
            showMessage(
                'You can use many of the add-ins functions even though you have not granted consent. If you want to grant consent, press the Get OneDrive File Names button again.'
            );
            break;
        case 13006:
            // Only seen in Office on the web.
            showMessage(
                'Office on the web is experiencing a problem. Please sign out of Office, close the browser, and then start again.'
            );
            break;
        case 13008:
            // Only seen in Office on the web.
            showMessage(
                'Office is still working on the last operation. When it completes, try this operation again.'
            );
            break;
        case 13010:
            // Only seen in Office on the web.
            showMessage(
                "Follow the instructions to change your browser's zone configuration."
            );
            break;
    
  9. TODO 8 替换为下面的代码。 对于无法处理的任何错误,代码将使用 MSAL 切换到回退身份验证。

    default: //recursive call.
            // For all other errors, including 13000, 13003, 13005, 13007, 13012, and 50001, fall back
            // to MSAL sign-in.
            showMessage('SSO failed. Trying fallback auth.');
            authSSO = false;
            return getAccessToken(false);
    }
    return null; // Return null for errors that show a message to the user.
    

编写 Web 服务器 REST API 代码

Web 服务器提供 REST API 供客户端调用。 例如,REST API /getuserfilenames 从用户的 OneDrive 文件夹中获取文件名列表。 每个 REST API 调用都需要客户端提供访问令牌,以确保正确的客户端正在访问其数据。 访问令牌通过代表流 (OBO) 交换 Microsoft Graph 令牌。 新的 Microsoft Graph 令牌由 MSAL 库缓存,用于后续 API 调用。 它永远不会发送到 Web 服务器外部。 有关详细信息,请参阅 中间层访问令牌请求

创建路由并实现代理流

  1. 打开 文件 routes\getFilesRoute.js ,并将 替换为 TODO 9 以下代码。 关于此代码,请注意以下几点:

    • 它调用 authHelper.validateJwt。 这可确保访问令牌有效且未被篡改。
    • 有关详细信息,请参阅 验证令牌
    router.get(
     "/getuserfilenames",
     authHelper.validateJwt,
     async function (req, res) {
       // TODO 10: Exchange the access token for a Microsoft Graph token
       //          by using the OBO flow.
     }
    );
    
  2. TODO 10 替换为下面的代码。 关于此代码,请注意以下几点:

    • 它仅请求所需的最小范围,例如 files.read
    • 它使用 MSAL authHelper 在调用 acquireTokenOnBehalfOf中执行 OBO 流。
    try {
      const authHeader = req.headers.authorization;
      let oboRequest = {
        oboAssertion: authHeader.split(' ')[1],
        scopes: ["files.read"],
      };
    
      // The Scope claim tells you what permissions the client application has in the service.
      // In this case we look for a scope value of access_as_user, or full access to the service as the user.
      const tokenScopes = jwt.decode(oboRequest.oboAssertion).scp.split(' ');
      const accessAsUserScope = tokenScopes.find(
        (scope) => scope === 'access_as_user'
      );
      if (!accessAsUserScope) {
        res.status(401).send({ type: "Missing access_as_user" });
        return;
      }
      const cca = authHelper.getConfidentialClientApplication();
      const response = await cca.acquireTokenOnBehalfOf(oboRequest);
      // TODO 11: Call Microsoft Graph to get list of filenames.
    } catch (err) {
      // TODO 12: Handle any errors.
    }
    
  3. TODO 11 替换为下面的代码。 关于此代码,请注意以下几点:

    • 它构造 Microsoft 图形 API 调用的 URL,然后通过 getGraphData 函数进行调用。
    • 它通过发送 HTTP 500 响应以及详细信息来返回错误。
    • 成功后,它会将包含文件名列表的 JSON 返回到客户端。
    // Minimize the data that must come from MS Graph by specifying only the property we need ("name")
    // and only the top 10 folder or file names.
    const rootUrl = '/me/drive/root/children';
    
    // Note that the last parameter, for queryParamsSegment, is hardcoded. If you reuse this code in
    // a production add-in and any part of queryParamsSegment comes from user input, be sure that it is
    // sanitized so that it cannot be used in a Response header injection attack.
    const params = '?$select=name&$top=10';
    
    const graphData = await getGraphData(
      response.accessToken,
      rootUrl,
      params
    );
    
    // If Microsoft Graph returns an error, such as invalid or expired token,
    // there will be a code property in the returned object set to a HTTP status (e.g. 401).
    // Return it to the client. On client side it will get handled in the fail callback of `makeWebServerApiCall`.
    if (graphData.code) {
      res
        .status(403)
        .send({
          type: "Microsoft Graph",
          errorDetails:
            "An error occurred while calling the Microsoft Graph API.\n" +
            graphData,
        });
    } else {
      // MS Graph data includes OData metadata and eTags that we don't need.
      // Send only what is actually needed to the client: the item names.
      const itemNames = [];
      const oneDriveItems = graphData["value"];
      for (let item of oneDriveItems) {
        itemNames.push(item["name"]);
      }
    
      res.status(200).send(itemNames);
    }
    // TODO 12: Check for expired token.
    
  4. TODO 12 替换为下面的代码。 此代码专门检查令牌是否过期,因为客户端可以请求新令牌并再次调用。

    } catch (err) {
       // On rare occasions the SSO access token is unexpired when Office validates it,
       // but expires by the time it is used in the OBO flow. Microsoft identity platform will respond
       // with "The provided value for the 'assertion' is not valid. The assertion has expired."
       // Construct an error message to return to the client so it can refresh the SSO token.
       if (err.errorMessage.indexOf('AADSTS500133') !== -1) {
         res.status(401).send({ type: "TokenExpiredError", errorDetails: err });
       } else {
         res.status(403).send({ type: "Unknown", errorDetails: err });
       }
    }
    

该示例必须处理通过 MSAL 的回退身份验证和通过 Office 的 SSO 身份验证。 该示例将首先尝试 SSO,文件 authSSO 顶部的布尔值将跟踪示例是否使用 SSO 或已切换到回退身份验证。

运行项目

  1. 请确保 OneDrive 中有一些文件,以便可以验证结果。

  2. \Begin 文件夹的根目录中打开命令提示符。

  3. 运行 命令 npm install 以安装所有包依赖项。

  4. 运行 命令 npm start 以启动中间层服务器。

  5. 需要将加载项旁加载到 Office 应用程序(Excel、Word 或 PowerPoint),以便对其进行测试。 说明取决于你的平台。 在旁加载 Office 加载项以供测试中有指向说明的链接。

  6. 在 Office 应用程序的“主页”功能区上,选择“SSO Node.js”组中的“显示加载项”按钮以打开任务窗格加载项。

  7. 单击“获取 OneDrive 文件名”按钮。 如果使用Microsoft 365 教育版或工作帐户或 Microsoft 帐户登录 Office,并且 SSO 按预期工作,则OneDrive for Business中前 10 个文件和文件夹名称将插入到文档中。 (第一次可能需要 15 秒的时间。) 如果未登录,或者你处于不支持 SSO 的方案中,或者 SSO 由于任何原因无法正常工作,系统会提示你登录。 登录后,将显示文件和文件夹名称。

注意

如果先前使用其他 ID 登录过 Office,并且当时打开的一些 Office 应用现在仍处于打开状态,Office 可能无法可靠地更改 ID,即使看似已更改过,也不例外。 在这种情况下,可能无法调用 Microsoft Graph,或者可能返回以前 ID 的数据。 为了防止发生这种情况,请务必先关闭其他所有 Office 应用程序,然后再按“获取 OneDrive 文件名”。

安全说明

  • 中的/getuserfilenamesgetFilesroute.js路由使用文本字符串来编写对 Microsoft Graph 的调用。 如果更改调用以便字符串的任何部分来自用户输入,请清理输入,使其不能用于响应标头注入攻击。

  • app.js 以下内容中,脚本的安全策略已到位。 你可能希望根据加载项的安全需求指定其他限制。

    "Content-Security-Policy": "script-src https://appsforoffice.microsoft.com https://ajax.aspnetcdn.com https://alcdn.msauth.net " + process.env.SERVER_SOURCE,

始终遵循Microsoft 标识平台文档中的安全最佳做法。