你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

教程:让用户登录并从 JavaScript 单页应用程序 (SPA) 调用 Microsoft 图形 API

在本教程中,你将生成一个 JavaScript 单页应用程序 (SPA),让用户使用隐式流登录并调用 Microsoft Graph。 生成的 SPA 使用适用于 JavaScript v1.0 的 Microsoft 身份验证库 (MSAL)。

本教程的内容:

  • 使用 npm 创建 JavaScript 项目
  • 在 Azure 门户中注册应用程序
  • 添加代码以支持用户登录和注销
  • 添加代码以调用 Microsoft Graph API
  • 测试应用

提示

本教程使用 MSAL.js v1.x,它仅限于对单页应用程序使用隐式授权流。 建议将所有新应用程序改为使用 MSAL 2.x 和提供 PKCE 和 CORS 支持的授权代码流

先决条件

  • 用于运行本地 Web 服务器的 Node.js
  • 用于修改项目文件的 Visual Studio Code 或其他编辑器。
  • 新式 Web 浏览器。 在本教程中生成的应用不支持 Internet Explorer,因为应用使用 ES6 约定 。

本指南生成的示例应用的工作原理

Shows how the sample app generated by this tutorial works

本指南创建的示例应用程序允许 JavaScript SPA 查询从 Microsoft 标识平台接受令牌的 Microsoft Graph API 或 Web API。 在此方案中,用户登录后请求了访问令牌,并通过授权标头将其添加到 HTTP 请求。 此令牌将用于通过 MS Graph API 获取用户的个人资料和邮件。

令牌获取和更新由适用于 JavaScript 的 Microsoft 身份验证库 (MSAL) 处理。

设置 Web 服务器或项目

想要改为下载此示例的项目? 下载项目文件

若要在执行代码示例之前对其进行配置,请跳到配置步骤

创建项目

确保已安装 Node.js,然后创建一个用于托管应用程序的文件夹。 我们将在此处实现一个简单的 Express Web 服务器来为 index.html 文件提供服务。

  1. 使用终端(如 Visual Studio Code 集成终端)找到项目文件夹,然后键入:

    npm init
    
  2. 接下来,安装必需的依赖项:

    npm install express --save
    npm install morgan --save
    
  3. 现在,创建一个名为 server.js 的 .js 文件并添加以下代码:

    const express = require('express');
    const morgan = require('morgan');
    const path = require('path');
    
    //initialize express.
    const app = express();
    
    // Initialize variables.
    const port = 3000; // process.env.PORT || 3000;
    
    // Configure morgan module to log all requests.
    app.use(morgan('dev'));
    
    // Set the front-end folder to serve public assets.
    app.use(express.static('JavaScriptSPA'))
    
    app.get('*', function (req, res) {
        res.sendFile(path.join(__dirname + '/JavaScriptSPA/index.html'));
    });
    
    // Start the server.
    app.listen(port);
    console.log('Listening on port ' + port + '...');
    

现在,已有一个可为 SPA 提供服务的简单服务器。 在本教程结束时,所需的文件夹结构如下:

a text depiction of the intended SPA folder structure

创建 SPA UI

  1. 为 JavaScript SPA 创建 index.html 文件。 此文件实现通过 Bootstrap 4 Framework 生成的 UI,并导入用于配置、身份验证和 API 调用的脚本文件。

    index.html 文件中添加以下代码:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
        <title>Quickstart | MSAL.JS Vanilla JavaScript SPA</title>
    
        <!-- msal.js with a fallback to backup CDN -->
        <script type="text/javascript" src="https://alcdn.msauth.net/lib/1.2.1/js/msal.js" integrity="sha384-9TV1245fz+BaI+VvCjMYL0YDMElLBwNS84v3mY57pXNOt6xcUYch2QLImaTahcOP" crossorigin="anonymous"></script>
        <script type="text/javascript">
          if(typeof Msal === 'undefined')document.write(unescape("%3Cscript src='https://alcdn.msftauth.net/lib/1.2.1/js/msal.js' type='text/javascript' integrity='sha384-m/3NDUcz4krpIIiHgpeO0O8uxSghb+lfBTngquAo2Zuy2fEF+YgFeP08PWFo5FiJ' crossorigin='anonymous'%3E%3C/script%3E"));
        </script>
    
        <!-- adding Bootstrap 4 for UI components  -->
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
      </head>
      <body>
        <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
          <a class="navbar-brand" href="/">MS Identity Platform</a>
          <div class="btn-group ml-auto dropleft">
            <button type="button" id="signIn" class="btn btn-secondary" onclick="signIn()">Sign In</button>
            <button type="button" id="signOut" class="btn btn-success d-none" onclick="signOut()">Sign Out</button>
        </div>
        </nav>
        <br>
        <h5 class="card-header text-center">Vanilla JavaScript SPA calling MS Graph API with MSAL.JS</h5>
        <br>
        <div class="row" style="margin:auto" >
        <div id="card-div" class="col-md-3 d-none">
        <div class="card text-center">
          <div class="card-body">
            <h5 class="card-title" id="welcomeMessage">Please sign-in to see your profile and read your mails</h5>
            <div id="profile-div"></div>
            <br>
            <br>
            <button class="btn btn-primary" id="seeProfile" onclick="seeProfile()">See Profile</button>
            <br>
            <br>
            <button class="btn btn-primary d-none" id="readMail" onclick="readMail()">Read Mails</button>
          </div>
        </div>
        </div>
        <br>
        <br>
          <div class="col-md-4">
            <div class="list-group" id="list-tab" role="tablist">
            </div>
          </div>
          <div class="col-md-5">
            <div class="tab-content" id="nav-tabContent">
            </div>
          </div>
        </div>
        <br>
        <br>
    
        <!-- importing bootstrap.js and supporting js libraries -->
        <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
    
        <!-- importing app scripts (load order is important) -->
        <script type="text/javascript" src="./authConfig.js"></script>
        <script type="text/javascript" src="./graphConfig.js"></script>
        <script type="text/javascript" src="./ui.js"></script>
    
        <!-- replace next line with authRedirect.js if you would like to use the redirect flow -->
        <!-- <script type="text/javascript" src="./authRedirect.js"></script>   -->
        <script type="text/javascript" src="./authPopup.js"></script>
        <script type="text/javascript" src="./graph.js"></script>
      </body>
    </html>
    

    提示

    可以将上述脚本中的 MSAL.js 版本替换为 MSAL.js 版本下的最新发布版本。

  2. 现在,创建名为 ui.js 的、用于访问和更新 DOM 元素的 .js 文件,并添加以下代码:

    // Select DOM elements to work with
    const welcomeDiv = document.getElementById("welcomeMessage");
    const signInButton = document.getElementById("signIn");
    const signOutButton = document.getElementById('signOut');
    const cardDiv = document.getElementById("card-div");
    const mailButton = document.getElementById("readMail");
    const profileButton = document.getElementById("seeProfile");
    const profileDiv = document.getElementById("profile-div");
    
    function showWelcomeMessage(account) {
      // Reconfiguring DOM elements
      cardDiv.classList.remove('d-none');
      welcomeDiv.innerHTML = `Welcome ${account.name}`;
      signInButton.classList.add('d-none');
      signOutButton.classList.remove('d-none');
    }
    
    function updateUI(data, endpoint) {
      console.log('Graph API responded at: ' + new Date().toString());
    
      if (endpoint === graphConfig.graphMeEndpoint) {
        const title = document.createElement('p');
        title.innerHTML = "<strong>Title: </strong>" + data.jobTitle;
        const email = document.createElement('p');
        email.innerHTML = "<strong>Mail: </strong>" + data.mail;
        const phone = document.createElement('p');
        phone.innerHTML = "<strong>Phone: </strong>" + data.businessPhones[0];
        const address = document.createElement('p');
        address.innerHTML = "<strong>Location: </strong>" + data.officeLocation;
        profileDiv.appendChild(title);
        profileDiv.appendChild(email);
        profileDiv.appendChild(phone);
        profileDiv.appendChild(address);
    
      } else if (endpoint === graphConfig.graphMailEndpoint) {
          if (data.value.length < 1) {
            alert("Your mailbox is empty!")
          } else {
            const tabList = document.getElementById("list-tab");
            tabList.innerHTML = ''; // clear tabList at each readMail call
            const tabContent = document.getElementById("nav-tabContent");
    
            data.value.map((d, i) => {
              // Keeping it simple
              if (i < 10) {
                const listItem = document.createElement("a");
                listItem.setAttribute("class", "list-group-item list-group-item-action")
                listItem.setAttribute("id", "list" + i + "list")
                listItem.setAttribute("data-toggle", "list")
                listItem.setAttribute("href", "#list" + i)
                listItem.setAttribute("role", "tab")
                listItem.setAttribute("aria-controls", i)
                listItem.innerHTML = d.subject;
                tabList.appendChild(listItem)
    
                const contentItem = document.createElement("div");
                contentItem.setAttribute("class", "tab-pane fade")
                contentItem.setAttribute("id", "list" + i)
                contentItem.setAttribute("role", "tabpanel")
                contentItem.setAttribute("aria-labelledby", "list" + i + "list")
                contentItem.innerHTML = "<strong> from: " + d.from.emailAddress.address + "</strong><br><br>" + d.bodyPreview + "...";
                tabContent.appendChild(contentItem);
              }
            });
          }
      }
    }
    

注册应用程序

在继续进行身份验证之前,请在 Azure Active Directory 中注册你的应用程序。

  1. 登录 Azure 门户
  2. 如果有权访问多个租户,请使用顶部菜单中的“目录 + 订阅”筛选器 ,以切换到要在其中注册应用程序的租户。
  3. 搜索并选择“Azure Active Directory” 。
  4. 在“管理”下,选择“应用注册”>“新建注册” 。
  5. 输入应用程序的名称。 应用的用户可能会看到此名称,你稍后可对其进行更改。
  6. 在“支持的帐户类型”下,选择“任何组织目录中的帐户和个人 Microsoft 帐户”。
  7. 在“重定向 URI”部分的下拉列表中选择“Web”平台,然后将值设置为基于 Web 服务器的应用程序 URL。
  8. 选择“注册”。
  9. 在应用的“概述”页上,记下“应用程序(客户端) ID”值,供稍后使用 。
  10. 在“管理”下,选择“身份验证”。
  11. 在“隐式授权和混合流”部分,选择“ID 令牌”和“访问令牌” 。 由于此应用必须将用户登录并调用 API,因此需要 ID 令牌和访问令牌。
  12. 选择“保存” 。

设置 Node.js 的重定向 URL

对于 Node.js,可以在 server.js 文件中设置 Web 服务器端口。 本教程使用端口 3000,但你可以使用任何其他可用端口。

若要设置应用程序注册信息中的重定向 URL,请切换回“应用程序注册”窗格,然后执行以下两项操作之一:

  • http://localhost:3000/ 设置为“重定向 URL”。
  • 如果使用的是自定义 TCP 端口,请使用 http://localhost:<port>/(其中,<端口> 是自定义 TCP 端口号)。
    1. 复制“URL”的值。
    2. 切换回“应用程序注册”窗格,然后将已复制的值粘贴为“重定向 URL”。

配置 JavaScript SPA

创建名为 authConfig.js 的、包含用于身份验证的配置参数新 .js 文件,并添加以下代码:

  const msalConfig = {
    auth: {
      clientId: "Enter_the_Application_Id_Here",
      authority: "Enter_the_Cloud_Instance_Id_Here/Enter_the_Tenant_Info_Here",
      redirectUri: "Enter_the_Redirect_Uri_Here",
    },
    cache: {
      cacheLocation: "sessionStorage", // This configures where your cache will be stored
      storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
    }
  };

  // Add here scopes for id token to be used at MS Identity Platform endpoints.
  const loginRequest = {
    scopes: ["openid", "profile", "User.Read"]
  };

  // Add here scopes for access token to be used at MS Graph API endpoints.
  const tokenRequest = {
    scopes: ["Mail.Read"]
  };

修改 msalConfig 部分中的值,如下所述:

  • <Enter_the_Application_Id_Here> 是已注册的应用程序的应用程序(客户端)ID
  • <Enter_the_Cloud_Instance_Id_Here> 是 Azure 云的实例。 对于主要云或全球 Azure 云,请输入 https://login.microsoftonline.com。 对于国家云(例如“中国”云),请参阅国家云
  • 将 <Enter_the_Tenant_info_here> 设置为以下选项之一:
    • 如果应用程序支持“此组织目录中的帐户”,请将此值替换为“租户 ID”或“租户名称”(例如,contoso.microsoft.com)。
    • 如果应用程序支持“任何组织目录中的帐户”,请将此值替换为 organizations
    • 如果应用程序支持“任何组织目录中的帐户和个人 Microsoft 帐户”,请将此值替换为 common。 若要限制对“仅限个人 Microsoft 帐户”的支持,请将此值替换为 consumers

使用 Microsoft 身份验证库 (MSAL) 登录用户

创建名为 authPopup.js 的、包含身份验证和令牌获取逻辑的新 .js 文件,并添加以下代码:

const myMSALObj = new Msal.UserAgentApplication(msalConfig);

function signIn() {
  myMSALObj.loginPopup(loginRequest)
    .then(loginResponse => {
      console.log('id_token acquired at: ' + new Date().toString());
      console.log(loginResponse);

      if (myMSALObj.getAccount()) {
        showWelcomeMessage(myMSALObj.getAccount());
      }
    }).catch(error => {
      console.log(error);
    });
}

function signOut() {
  myMSALObj.logout();
}

function callMSGraph(theUrl, accessToken, callback) {
    var xmlHttp = new XMLHttpRequest();
    xmlHttp.onreadystatechange = function () {
        if (this.readyState == 4 && this.status == 200) {
           callback(JSON.parse(this.responseText));
        }
    }
    xmlHttp.open("GET", theUrl, true); // true for asynchronous
    xmlHttp.setRequestHeader('Authorization', 'Bearer ' + accessToken);
    xmlHttp.send();
}

function getTokenPopup(request) {
  return myMSALObj.acquireTokenSilent(request)
    .catch(error => {
      console.log(error);
      console.log("silent token acquisition fails. acquiring token using popup");

      // fallback to interaction when silent call fails
        return myMSALObj.acquireTokenPopup(request)
          .then(tokenResponse => {
            return tokenResponse;
          }).catch(error => {
            console.log(error);
          });
    });
}

function seeProfile() {
  if (myMSALObj.getAccount()) {
    getTokenPopup(loginRequest)
      .then(response => {
        callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI);
        profileButton.classList.add('d-none');
        mailButton.classList.remove('d-none');
      }).catch(error => {
        console.log(error);
      });
  }
}

function readMail() {
  if (myMSALObj.getAccount()) {
    getTokenPopup(tokenRequest)
      .then(response => {
        callMSGraph(graphConfig.graphMailEndpoint, response.accessToken, updateUI);
      }).catch(error => {
        console.log(error);
      });
  }
}

详细信息

用户首次选择“登录”按钮后,signIn 方法将调用 loginPopup 以将用户登录。 此方法会打开一个包含 Microsoft 标识平台终结点的弹出窗口,以提示并验证用户的凭据。 成功登录后,用户将重定向回到原始的 index.html 页。 他们将接收到一个由 msal.js 处理的令牌,该令牌包含的信息已缓存。 该令牌称为 ID令牌,并包含有关用户的基本信息,如用户显示名。 如果计划将此令牌提供的数据用于任何目的,请确保此令牌会由后端服务器验证,以保证该令牌颁发给应用程序的有效用户。

本指南生成的 SPA 调用 acquireTokenSilent 和/或 acquireTokenPopup 来获取用于查询 Microsoft Graph API 以获取用户配置文件信息的访问令牌。 如果需要用于验证 ID 令牌的示例,请查看 GitHub 中的示例应用程序。 该示例使用 ASP.NET Web API 进行令牌验证。

以交互方式获取用户令牌

首次登录后,你不希望在每次用户需要请求令牌来访问资源时,都要求他们重新进行身份验证。 因此,在大部分时间应使用 acquireTokenSilent 来获取令牌。 但是,在有些情况下,你会强制用户与 Microsoft 标识平台进行交互。 示例包括:

  • 由于密码已过期,用户可能需要重新输入其凭据。
  • 应用程序正在请求访问资源,这需要用户的许可。
  • 需要双重身份验证。

调用 acquireTokenPopup 会打开一个弹出窗口(或者,acquireTokenRedirect 会将用户重定向到 Microsoft 标识平台) 。 在该窗口中,为了进行交互,用户需要确认其凭据、为所需的资源提供许可,或者完成双重身份验证。

以无提示方式获取用户令牌

acquireTokenSilent 方法处理令牌获取和续订,无需进行任何用户交互。 首次执行 loginPopup(或 loginRedirect)后,通常使用 acquireTokenSilent 方法获取用于访问受保护资源的令牌,以便进行后续调用。 (请求或续订令牌的调用是以静默方式发出的。)acquireTokenSilent 在某些情况下可能会失败。 例如,用户的密码可能已过期。 应用程序可以通过两种方式处理此异常:

  1. 立即调用 acquireTokenPopup,这会触发用户登录提示。 此模式通常用于联机应用程序,此时应用程序中没有可供用户使用的未经身份验证的内容。 本指导式设置生成的示例使用此模式。

  2. 应用程序还可以直观地提示用户以交互方式登录,用户可以选择在合适的时间登录,或者应用程序可以稍后重试 acquireTokenSilent。 如果用户可以在不中断应用程序的情况下使用应用程序的其他功能,则通常会使用此方法。 例如,应用程序中有可用的未经身份验证的内容。 在这种情况下,用户可以决定何时登录并访问受保护的资源,或何时刷新已过时的信息。

注意

本快速入门默认使用 loginPopupacquireTokenPopup 方法。 如果使用 Internet Explorer 作为浏览器,我们建议使用 loginRedirectacquireTokenRedirect 方法,因为 Internet Explorer 处理弹出窗口的方式存在一个已知问题。 若要了解如何使用 Redirect methods 实现相同的结果,请参阅此文

使用刚刚获取的令牌调用 Microsoft Graph API

  1. 首先,创建名为 graphConfig.js 的 .js 文件用于存储 REST 终结点。 添加以下代码:

       const graphConfig = {
         graphMeEndpoint: "Enter_the_Graph_Endpoint_Here/v1.0/me",
         graphMailEndpoint: "Enter_the_Graph_Endpoint_Here/v1.0/me/messages"
       };
    

    其中:

    • <Enter_the_Graph_Endpoint_Here> 是 MS Graph API 的实例。 对于全局 MS Graph API 终结点,只需将此字符串替换为 https://graph.microsoft.com 即可。 对于国家云部署,请参阅 Graph API 文档
  2. 接下来,创建名为 graph.js 的、用于对 Microsoft Graph API 发出 REST 调用的 .js 文件,并添加以下代码:

    function callMSGraph(endpoint, token, callback) {
      const headers = new Headers();
      const bearer = `Bearer ${token}`;
    
      headers.append("Authorization", bearer);
    
      const options = {
          method: "GET",
          headers: headers
      };
    
      console.log('request made to Graph API at: ' + new Date().toString());
    
      fetch(endpoint, options)
        .then(response => response.json())
        .then(response => callback(response, endpoint))
        .catch(error => console.log(error))
    }
    

对受保护 API 进行 REST 调用的详细信息

在本指南创建的示例应用程序中,将使用 callMSGraph() 方法对需要令牌的受保护资源发出 HTTP GET 请求。 然后,该请求将内容返回给调用方。 此方法可在 HTTP 授权标头中添加获取的令牌。 本指南创建的示例应用程序中的资源是 Microsoft Graph API me 终结点,它显示用户个人资料信息。

测试代码

  1. 配置服务器侦听基于“index.html”文件位置的 TCP 端口。 对于 Node.js,请通过在命令提示符下从应用程序文件夹运行以下命令,启动 Web 服务器来侦听该端口:

    npm install
    npm start
    
  2. 在浏览器中输入 http://localhost:3000http://localhost:{port} ,其中,port 是 Web 服务器正在侦听的端口。 应会显示 index.html 文件的内容和“登录”按钮。

重要

在浏览器设置中为站点启用弹出窗口和重定向。

在浏览器加载 index.html 文件后,选择“登录”。 系统将提示你使用 Microsoft 标识平台进行登录:

The JavaScript SPA account sign-in window

首次登录到应用程序时,系统会提示你授予其访问你的个人资料的权限,并将你登录:

The

查看应用程序结果

登录后,你的用户个人资料信息将在显示的 Microsoft Graph API 响应中返回:

Results from the Microsoft Graph API call

有关作用域和委派权限的详细信息

Microsoft Graph API 需要 user.read 作用域来读取用户的个人资料。 默认情况下,在注册门户上注册的每个应用程序中,都会自动添加此范围。 Microsoft Graph 的其他 API 以及后端服务器的自定义 API 可能需要其他作用域。 例如,Microsoft Graph API 需要使用 Mail.Read 范围列出用户的邮件。

注意

当你增加作用域数量时,可能会提示用户另外进行许可。

帮助和支持

如果需要帮助、需要报告问题,或者需要详细了解支持选项,请参阅面向开发人员的帮助和支持

后续步骤

在由多部分组成的方案系列中,深入了解 Microsoft 标识平台上的单页应用程序 (SPA) 开发。