教程:从 Node.js 守护程序应用程序调用 Web API

本教程演示如何使用 开放授权 (OAuth) 2.0 客户端凭据授予流准备 Node.js 守护程序客户端应用,然后将其配置为获取用于调用 Web API 的访问令牌。 你将使用 Node Microsoft 身份验证库(MSAL)生成 Node.js 应用程序,以简化向应用添加授权。

在本教程中;

  • 为 Web API 配置应用角色
  • 向守护程序应用授予权限
  • 在 Visual Studio Code 中创建 Node.js 应用,然后安装依赖项。
  • 启用 Node.js 应用以获取用于调用 Web API 的访问令牌。

先决条件

  • Microsoft Entra 管理中心注册一个新的客户端应用程序,配置为 任何组织目录中的帐户以及个人 Microsoft 帐户。 有关更多详细信息 ,请参阅注册应用程序 。 在应用程序 概述 页中记录以下值供以后使用:
    • 应用程序(客户端)ID
    • 目录(租户)ID
    • 目录(租户)域名(例如 contoso.onmicrosoft.comcontoso.com)。
  • 将客户端密码添加到客户端应用注册。 不要 在生产应用中使用客户端机密。 请改用证书或联合凭据。 有关详细信息,请参阅 向应用程序添加凭据
  • 受保护的 Web API,它正在运行且已准备好接受请求。 确保 Web API 通过 HTTPS 公开以下终结点:
    • GET /api/todolist,用于获取所有待办事项。
    • POST /api/todolist,用于添加待办事项。
  • Node.js
  • 本教程使用了 Visual Studio Code,但可以使用任何支持 React 应用程序的集成开发环境 (IDE)。

配置应用角色

API 需要为应用程序发布至少一个应用角色(也称为 应用程序权限),客户端应用才能自行获取访问令牌。 应用程序权限是 API 想要使客户端应用程序能够成功地以自己的身份进行身份验证(无需让用户登录)时应发布的权限类型。 若要发布应用程序权限,请执行下列步骤:

  1. 从“应用注册”页中,选择创建的应用程序(例如 ciam-ToDoList-api)以打开其“概述”页。

  2. 在“管理”下,选择“应用角色”。

  3. 选择“创建应用角色”,输入以下值,然后选择“应用”以保存更改:

    财产 价值
    显示名称 ToDoList.Read.All
    允许的成员类型 应用程序
    价值 ToDoList.Read.All
    DESCRIPTION 允许应用使用“TodoListApi”读写每个用户的待办事项列表
    要启用此应用角色吗? 保持选中状态
  4. 再次选择“创建应用角色”,为第二个应用角色输入以下值,然后选择“应用”以保存更改:

    财产 价值
    显示名称 ToDoList.ReadWrite.All
    允许的成员类型 应用程序
    价值 ToDoList.ReadWrite.All
    DESCRIPTION 允许应用使用“ToDoListApi”读写每个用户的 ToDo 列表
    要启用此应用角色吗? 保持选中状态

配置 idtyp 令牌声明

可以添加 idtyp 可选声明,以帮助 Web API 确定令牌是 应用 令牌还是 应用 + 用户 令牌。 尽管可以将 scp 和 roles 声明的组合用于同一目的,但使用 idtyp 声明仍是区分应用令牌和应用 + 用户令牌的最简单方法。 例如,当令牌为仅限应用的令牌时,此声明的值为 app

向守护程序应用授予 API 权限

  1. 从“应用注册”页中,选择创建的应用程序(例如 ciam-client-app)。

  2. 在“管理”下,选择 API 权限

  3. 在“已配置权限”下,选择“添加权限”。

  4. 选择“我的组织使用的 API”选项卡。

  5. 在 API 列表中,选择 API(例如 ciam-ToDoList-api)。

  6. 选择“应用程序权限”选项。 我们选择此选项是因为应用以自身身份登录,而不是以用户身份登录。

  7. 从权限列表中,选择“TodoList.Read.All”、“ToDoList.ReadWrite.All”(必要时使用搜索框)。

  8. 选择“添加权限”按钮

  9. 此时,你已正确分配了权限。 但是,由于守护程序应用不允许用户与之交互,因此用户本身无法同意这些权限。 若要解决此问题,作为管理员的你必须代表租户中的所有用户同意这些权限:

    1. 选择“为<租户名称>授予管理员同意”,然后选择“是”。
    2. 选择“刷新”,然后验证两个权限的“状态”下是否均显示“已为 租户名称< 授予”>

创建 Node.js 守护程序项目

创建一个用于托管 Node.js 守护程序应用程序的文件夹,例如 ciam-call-api-node-daemon

  1. 在终端中,将目录切换到 Node 守护程序应用文件夹(例如 cd ciam-call-api-node-daemon)中,然后运行 npm init -y。 此命令为 Node.js 项目创建默认的 package.json 文件。 此命令将为 Node.js 项目创建一个默认 package.json 文件。

  2. 创建额外的文件夹和文件,以实现以下项目结构:

        ciam-call-api-node-daemon/
        ├── auth.js
        └── authConfig.js
        └── fetch.js
        └── index.js 
        └── package.json
    

安装应用依赖项

在终端中,通过运行以下命令来安装 axiosyargs@azure/msal-node 包:

npm install axios yargs @azure/msal-node   

创建 MSAL 配置对象

在代码编辑器中,打开 authConfig.js 文件,然后添加以下代码:

require('dotenv').config();

/**
 * Configuration object to be passed to MSAL instance on creation.
 * For a full list of MSAL Node configuration parameters, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md
 */    
const msalConfig = {
    auth: {
        clientId: process.env.CLIENT_ID || 'Enter_the_Application_Id_Here', // 'Application (client) ID' of app registration in Azure portal - this value is a GUID
        authority: process.env.AUTHORITY || 'https://Enter_the_Tenant_Subdomain_Here.ciamlogin.com/', // Replace "Enter_the_Tenant_Subdomain_Here" with your tenant subdomain
        clientSecret: process.env.CLIENT_SECRET || 'Enter_the_Client_Secret_Here', // Client secret generated from the app 
    },
    system: {
        loggerOptions: {
            loggerCallback(loglevel, message, containsPii) {
                console.log(message);
            },
            piiLoggingEnabled: false,
            logLevel: 'Info',
        },
    },
};    
const protectedResources = {
    apiToDoList: {
        endpoint: process.env.API_ENDPOINT || 'https://localhost:44351/api/todolist',
        scopes: [process.env.SCOPES || 'api://Enter_the_Web_Api_Application_Id_Here'],
    },
};

module.exports = {
    msalConfig,
    protectedResources,
};

msalConfig 对象包含一组用于自定义授权流的行为的配置选项。

在 authConfig.js 文件中替换以下项

  • Enter_the_Application_Id_Here 替换为之前注册的客户端守护程序应用的应用程序(客户端)ID。

  • Enter_the_Tenant_Subdomain_Here 并将其替换为 Directory (tenant) 子域。 例如,如果租户主域名是 contoso.onmicrosoft.com,请使用 contoso。 如果没有租户名称,请了解如何读取租户详细信息

  • Enter_the_Client_Secret_Here 替换为之前复制的客户端守护程序应用机密值。

  • Enter_the_Web_Api_Application_Id_Here 替换为之前复制的 Web API 应用的应用程序(客户端)ID。

请注意,变量 scopes 中的 protectedResources 属性是作为先决条件的一部分注册的 Web API 的资源标识符(应用程序 ID URI)。 完整的范围 URI 类似于 api://Enter_the_Web_Api_Application_Id_Here/.default

获取访问令牌

在代码编辑器中,打开 auth.js 文件,然后添加以下代码:

const msal = require('@azure/msal-node');
const { msalConfig, protectedResources } = require('./authConfig');
/**
 * With client credentials flows permissions need to be granted in the portal by a tenant administrator.
 * The scope is always in the format '<resource-appId-uri>/.default'. For more, visit:
 * https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
 */
const tokenRequest = {
    scopes: [`${protectedResources.apiToDoList.scopes}/.default`],
};

const apiConfig = {
    uri: protectedResources.apiToDoList.endpoint,
};

/**
 * Initialize a confidential client application. For more info, visit:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/initialize-confidential-client-application.md
 */
const cca = new msal.ConfidentialClientApplication(msalConfig);
/**
 * Acquires token with client credentials.
 * @param {object} tokenRequest
 */
async function getToken(tokenRequest) {
    return await cca.acquireTokenByClientCredential(tokenRequest);
}

module.exports = {
    apiConfig: apiConfig,
    tokenRequest: tokenRequest,
    getToken: getToken,
};

在代码中:

  • 准备 tokenRequestapiConfig 对象。 tokenRequest 包含您请求的访问令牌的范围。 范围类似于 api://Enter_the_Web_Api_Application_Id_Here/.defaultapiConfig 对象包含 Web API 的终结点。 详细了解 OAuth 2.0 客户端凭据流

  • 通过将 msalConfig 对象传递给 ConfidentialClientApplication 类的构造函数来创建机密客户端实例。

    const cca = new msal.ConfidentialClientApplication(msalConfig);
    
  • 然后,使用 acquireTokenByClientCredential 函数获取访问令牌。 在 getToken 函数中实现此逻辑:

    cca.acquireTokenByClientCredential(tokenRequest);
    

获取访问令牌后,可以继续调用 API。

调用 API

在代码编辑器中,打开 fetch.js 文件,然后添加以下代码:

const axios = require('axios');

/**
 * Calls the endpoint with authorization bearer token.
 * @param {string} endpoint
 * @param {string} accessToken 
 */
async function callApi(endpoint, accessToken) {

    const options = {
        headers: {
            Authorization: `Bearer ${accessToken}`
        }
    };

    console.log('request made to web API at: ' + new Date().toString());

    try {
        const response = await axios.get(endpoint, options);
        return response.data;
    } catch (error) {
        console.log(error)
        return error;
    }
};

module.exports = {
    callApi: callApi
};

在此代码中,可以通过在请求 Authorization 标头中将访问令牌作为持有者令牌传递来调用 Web API:

 Authorization: `Bearer ${accessToken}`

使用之前在获取访问令牌中获取的访问令牌。

Web API 收到请求后,它会对其进行评估,然后确定它是应用程序请求。 如果访问令牌有效,Web API 将返回请求的数据。 否则,API 将返回 401 Unauthorized HTTP 错误。

完成守护程序应用

在代码编辑器中,打开 index.js 文件,然后添加以下代码:

#!/usr/bin/env node

// read in env settings

require('dotenv').config();

const yargs = require('yargs');
const fetch = require('./fetch');
const auth = require('./auth');

const options = yargs
    .usage('Usage: --op <operation_name>')
    .option('op', { alias: 'operation', describe: 'operation name', type: 'string', demandOption: true })
    .argv;

async function main() {
    console.log(`You have selected: ${options.op}`);

    switch (yargs.argv['op']) {
        case 'getToDos':
            try {
                const authResponse = await auth.getToken(auth.tokenRequest);
                const todos = await fetch.callApi(auth.apiConfig.uri, authResponse.accessToken);                
            } catch (error) {
                console.log(error);
            }

            break;
        default:
            console.log('Select an operation first');
            break;
    }
};

main();

此代码是应用的入口点。 使用适用于 Node.js 应用的 yargs JavaScript 命令行参数分析库,以交互方式提取访问令牌,然后调用 API。 使用之前定义的 getTokencallApi 函数:

const authResponse = await auth.getToken(auth.tokenRequest);
const todos = await fetch.callApi(auth.apiConfig.uri, authResponse.accessToken);                

运行和测试守护程序应用和 API

此时,已准备好测试客户端守护程序应用和 Web API:

  1. 使用在保护 ASP.NET 网络 API教程中学习的步骤启动你的网络 API。 Web API 现已准备好处理客户端请求。 如果未在 authConfig.js 文件中指定的 44351 端口上运行 Web API,请务必更新 authConfig.js 文件,以使用 Web API 的正确端口号。

  2. 在终端中,请确保你在包含守护程序 Node.js 应用的项目文件夹(例如 ciam-call-api-node-daemon)中,然后运行以下命令:

    node . --op getToDos
    

如果守护程序应用和 Web API 成功运行,那么你应该会在控制台窗口中找到 Web API 终结点 todos 变量返回的数据,它类似于以下 JSON 数组:

{
    id: 1,
    owner: '3e8....-db63-43a2-a767-5d7db...',
    description: 'Pick up grocery'
},
{
    id: 2,
    owner: 'c3cc....-c4ec-4531-a197-cb919ed.....',
    description: 'Finish invoice report'
},
{
    id: 3,
    owner: 'a35e....-3b8a-4632-8c4f-ffb840d.....',
    description: 'Water plants'
}

后续步骤