通过


如何将 JavaScript 应用从 ADAL.js 迁移到 MSAL.js

Microsoft Authentication Library for JavaScript(MSAL.js,也称为 msal-browser) 2.x 是我们建议在Microsoft标识平台上与 JavaScript 应用程序一起使用的身份验证库。 本文重点介绍需要做出哪些更改才能将使用 ADAL.js 的应用迁移到 MSAL.js 2.x

注意

强烈建议使用 MSAL.js 2.x 而不是 MSAL.js 1.x。 授权代码授予流更安全,并且可以使单页应用程序保持良好的用户体验(尽管 Safari 等浏览器实施了隐私保护措施来阻止第三方 Cookie),此外,它还具有其他许多优势。

先决条件

  • 必须在应用注册门户中将 平台 / 回复 URL 类型 设置为 单页应用程序 (如果在应用注册中添加了其他平台(如 Web),则需要确保重定向 URI 不重叠。请参阅: 重定向 URI 限制
  • 必须为 MSAL.js 依赖的 ES6 功能提供 polyfills(例如,promises),才能在 Internet Explorer 上运行您的应用程序。
  • 将Microsoft Entra应用迁移到 v2 终结点(如果尚未这样做)

安装并导入 MSAL

可通过两种方式安装 MSAL.js 2.x 库:

通过 npm:

npm install @azure/msal-browser

然后,根据所用的模块系统,按如下所示导入该库:

import * as msal from "@azure/msal-browser"; // ESM

const msal = require('@azure/msal-browser'); // CommonJS

通过 CDN:

在 HTML 文档的 header 节中加载以下脚本:

<!DOCTYPE html>
<html>
  <head>
    <script type="text/javascript" src="https://alcdn.msauth.net/browser/2.14.2/js/msal-browser.min.js"></script>
  </head>
</html>

有关使用 CDN 时的备用 CDN 链接和最佳做法,请参阅: CDN 使用情况

初始化 MSAL

在 ADAL.js中,实例化 AuthenticationContext 类,然后公开可用于实现身份验证的方法(loginacquireTokenPopup等等)。 此对象用作应用程序与授权服务器或标识提供者之间的连接的表示形式。 初始化时,唯一必需的参数是 clientId

window.config = {
  clientId: "YOUR_CLIENT_ID"
};

var authContext = new AuthenticationContext(config);

在 MSAL.js 中,改为实例化 PublicClientApplication 类。 与 ADAL.js一样,构造函数需要至少包含参数clientId。 有关详细信息,请参阅: 初始化 MSAL.js

const msalConfig = {
  auth: {
      clientId: 'YOUR_CLIENT_ID'
  }
};

const msalInstance = new msal.PublicClientApplication(msalConfig);

在 ADAL.js 和 MSAL.js 中,如果未指定 "颁发机构 URI",则其默认值为 https://login.microsoftonline.com/common

注意

如果在 v2.0 中使用 https://login.microsoftonline.com/common 授权,则允许用户使用任何 Microsoft Entra 组织或个人 Microsoft 帐户(MSA)登录。 在 MSAL.js中,如果要将登录限制为任何Microsoft Entra帐户(与 ADAL.js行为相同),请改用 https://login.microsoftonline.com/organizations

配置 MSAL

MSAL.js中弃用了初始化 AuthenticationContext 时使用的 ADAL.js中的一些配置选项,同时引入了一些新选项。 请参阅 可用选项的完整列表。 重要的是,这些选项中的许多(除了 clientId)可以在令牌获取期间被重写,从而允许按 请求 设置它们。 例如,您可以在获取令牌时使用与初始化期间设置的 颁发机构 URI重定向 URI 不同的 URI。

此外,不再需要通过配置选项指定登录体验(即,是要使用弹出窗口还是重定向页面)。 MSAL.js而是通过loginPopup实例公开loginRedirectPublicClientApplication方法。

启用日志记录

在 ADAL.js 中,你可以在代码中的任何位置单独配置日志记录:

window.config = {
  clientId: "YOUR_CLIENT_ID"
};

var authContext = new AuthenticationContext(config);

var Logging = {
  level: 3,
  log: function (message) {
      console.log(message);
  },
  piiLoggingEnabled: false
};


authContext.log(Logging)

在 MSAL.js中,日志记录是配置选项的一部分,并在初始化 PublicClientApplication过程中创建:

const msalConfig = {
  auth: {
      // authentication related parameters
  },
  cache: {
      // cache related parameters
  },
  system: {
      loggerOptions: {
          loggerCallback(loglevel, message, containsPii) {
              console.log(message);
          },
          piiLoggingEnabled: false,
          logLevel: msal.LogLevel.Verbose,
      }
  }
}

const msalInstance = new msal.PublicClientApplication(msalConfig);

切换到 MSAL API

ADAL.js 中的某些公共方法在 MSAL.js 中有等效方法:

ADAL MSAL 说明
acquireToken acquireTokenSilent 已重命名,现在需要 帐户 对象
acquireTokenPopup acquireTokenPopup 现在是异步的,并返回承诺
acquireTokenRedirect acquireTokenRedirect 现在是异步的,并返回承诺
handleWindowCallback handleRedirectPromise 使用重定向体验时需要
getCachedUser getAllAccounts 已重命名,现在会返回帐户数组。

其他方法已弃用,不过 MSAL.js 提供了新方法:

ADAL MSAL 说明
login 不适用 已弃用。 使用 loginPopuploginRedirect
logOut 不适用 已弃用。 使用 logoutPopuplogoutRedirect
不适用 loginPopup
不适用 loginRedirect
不适用 logoutPopup
不适用 logoutRedirect
不适用 getAccountByHomeId 按家庭 ID(OID + 租户 ID)筛选账户
不适用 getAccountLocalId 按本地 ID 筛选帐户(对于 ADFS 非常有用)
不适用 getAccountUsername 按用户名(如果存在)筛选帐户

此外,与 ADAL.js 不同,MSAL.js 是在 TypeScript 中实现的,因此它会公开各种可在项目中使用的类型和接口。 有关详细信息 ,请参阅MSAL.js API 参考

使用作用域而不是资源

Azure Active Directory v1.0 与 2.0 终结点之间的一个重要区别在于如何访问资源。 对 v1.0 终结点使用 ADAL.js 时,首先在应用注册门户上进行权限注册,然后请求资源(如 Microsoft Graph)的访问令牌,如下所示:

authContext.acquireTokenRedirect("https://graph.microsoft.com", function (error, token) {
  // do something with the access token
});

MSAL.js 仅支持 v2.0 终结点。 v2.0 终结点采用以范围为中心的模型来访问资源。 因此,你在为资源请求访问令牌时,还需要指定该资源的作用域:

msalInstance.acquireTokenRedirect({
  scopes: ["https://graph.microsoft.com/User.Read"]
});

以范围为中心的模型的优点之一是能够使用 动态范围。 使用 v1.0 终结点生成应用程序时,需要注册应用程序所需的完整权限集(称为 静态作用域),以便用户在登录时同意。 在 v2.0 中,可以使用 scope 参数在您想要的时间请求权限(因此,动态作用域)。 这样,用户就可以向范围提供 增量同意 。 因此,如果你最初只是希望用户登录到你的应用程序,而不需要任何类型的访问权限,则可以这样做。 如果后来需要读取用户的日历,则可以在 acquireToken 方法中请求日历范围,并获取用户的许可。 有关详细信息,请参阅: 资源和范围

使用承诺而不是回叫

在 ADAL.js 中,回调用于在成功完成身份验证并获取响应后执行的任何操作:

authContext.acquireTokenPopup(resource, extraQueryParameter, claims, function (error, token) {
  // do something with the access token
});

在 MSAL.js 中,需要改用承诺:

msalInstance.acquireTokenPopup({
      scopes: ["User.Read"] // shorthand for https://graph.microsoft.com/User.Read
  }).then((response) => {
      // do something with the auth response
  }).catch((error) => {
      // handle errors
  });

还可以使用 ES8 附带的 async/await 语法:

const getAccessToken = async() => {
  try {
      const authResponse = await msalInstance.acquireTokenPopup({
          scopes: ["User.Read"]
      });
  } catch (error) {
      // handle errors
  }
}

缓存和检索令牌

与 ADAL.js一样,MSAL.js 使用 Web 存储 API 缓存浏览器存储中的令牌和其他身份验证项目。 建议使用 sessionStorage 选项(请参阅: 配置),因为存储用户获取的令牌更安全,但 localStorage 会在选项卡和用户会话之间提供 单一登录

重要的是,最好不要直接访问缓存。 而应该使用适当的 MSAL.js API 来检索访问令牌或用户帐户等身份验证信息。

使用刷新令牌续订令牌

ADAL.js 使用 OAuth 2.0 隐式流,因为安全原因不会返回刷新令牌(刷新令牌的生存期比访问令牌长,因此在恶意参与者手中更危险)。 因此,ADAL.js 使用隐藏的 IFrame 执行令牌续订,这样可以避免反复提示用户进行身份验证。

借助支持 PKCE 的授权代码流,使用 MSAL.js 2.x 的应用可以同时获取刷新令牌、ID 令牌和访问令牌,并使用这些令牌进行续订。 刷新令牌的使用已抽象化,开发人员不应围绕它们构建逻辑。 MSAL 则是自行使用刷新令牌来管理令牌续订。 以前在 ADAL.js 中使用的令牌缓存无法转移到 MSAL.js,因为令牌缓存架构已更改,与 ADAL.js 中使用的架构不兼容。

处理错误和异常

使用 MSAL.js时,可能会遇到的最常见错误类型是 interaction_in_progress 错误。 当调用交互式 API(loginPopup、、loginRedirectacquireTokenPopupacquireTokenRedirect时,另一个交互式 API 仍在进行中时,将引发此错误。 API 是异步的,因此在调用其他 Promise 之前,你需要确保已经解决了生成的 Promise。

另一个常见错误是 interaction_required。 通常通过启动交互式令牌获取提示,就能解决此错误。 例如,尝试访问的 Web API 可能有条件访问策略到位,要求用户执行多重身份验证(MFA)。 在这种情况下,通过触发interaction_requiredacquireTokenPopup来处理acquireTokenRedirect错误,这将提示用户完成 MFA,从而允许他们进行处理。

然而,你可能面临的另一个常见错误是 consent_required,当用户未同意获取受保护资源的访问令牌所需的权限时,会发生此错误。 如interaction_required,对于consent_required错误的解决方案通常是使用acquireTokenPopupacquireTokenRedirect启动交互式令牌获取提示。

有关详细信息,请参阅: 常见 MSAL.js 错误以及如何处理错误

使用事件 API

MSAL.js(>=v2.4)引入了可以在应用中使用的事件 API。 这些事件与身份验证过程以及 MSAL 在任意时刻执行的操作相关,可用于更新 UI、显示错误消息、检查是否正在进行任何交互,等等。 例如,下面是当登录过程出于任何原因而失败时要调用的事件回调:

const callbackId = msalInstance.addEventCallback((message) => {
  // Update UI or interact with EventMessage here
  if (message.eventType === EventType.LOGIN_FAILURE) {
      if (message.error instanceof AuthError) {
          // Do something with the error
      }
    }
});

为了保持性能,不再需要事件回叫时,应将其取消注册,这一点非常重要。 有关详细信息,请参阅: MSAL.js 事件 API

处理多个帐户

ADAL.js 引入了 "用户" 概念来表示当前经过身份验证的实体。 MSAL.js 将 用户 替换为 帐户,因为鉴于用户可以有多个帐户与之关联。 这也意味着你现在需要管理多个帐户,并选择一个适当的帐户来使用。 以下代码片段演示了此过程:

let homeAccountId = null; // Initialize global accountId (can also be localAccountId or username) used for account lookup later, ideally stored in app state

// This callback is passed into `acquireTokenPopup` and `acquireTokenRedirect` to handle the interactive auth response
function handleResponse(resp) {
  if (resp !== null) {
      homeAccountId = resp.account.homeAccountId; // alternatively: resp.account.homeAccountId or resp.account.username
  } else {
      const currentAccounts = myMSALObj.getAllAccounts();
      if (currentAccounts.length < 1) { // No cached accounts
          return;
      } else if (currentAccounts.length > 1) { // Multiple account scenario
          // Add account selection logic here
      } else if (currentAccounts.length === 1) {
          homeAccountId = currentAccounts[0].homeAccountId; // Single account scenario
      }
  }
}

有关详细信息,请参阅: MSAL.js中的帐户

使用包装器库

如果要针对 Angular 和 React 框架进行开发,则可以分别使用 MSAL Angular v2MSAL React。 这些包装器公开与 MSAL.js 相同的公共 API,同时还提供特定于框架的方法和组件,以简化身份验证和令牌获取过程。

运行应用

完成更改后,运行应用并测试身份验证方案:

npm start

示例:使用 ADAL.js 或 MSAL.js 保护 SPA的比较

以下代码片段展示了单页应用程序中使用Microsoft身份平台验证用户所需的最简代码,以及如何首先使用ADAL.js获取Microsoft Graph的访问令牌,然后使用MSAL.js获取访问令牌。

使用 ADAL.js 使用 MSAL.js

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="https://alcdn.msauth.net/lib/1.0.18/js/adal.min.js"></script>
</head>

<body>
    <div>
        <p id="welcomeMessage" style="visibility: hidden;"></p>
        <button id="loginButton">Login</button>
        <button id="logoutButton" style="visibility: hidden;">Logout</button>
        <button id="tokenButton" style="visibility: hidden;">Get Token</button>
    </div>
    <script>
        // DOM elements to work with
        var welcomeMessage = document.getElementById("welcomeMessage");
        var loginButton = document.getElementById("loginButton");
        var logoutButton = document.getElementById("logoutButton");
        var tokenButton = document.getElementById("tokenButton");

        // if user is logged in, update the UI
        function updateUI(user) {
            if (!user) {
                return;
            }

            welcomeMessage.innerHTML = 'Hello ' + user.profile.upn + '!';
            welcomeMessage.style.visibility = "visible";
            logoutButton.style.visibility = "visible";
            tokenButton.style.visibility = "visible";
            loginButton.style.visibility = "hidden";
        };

        // attach logger configuration to window
        window.Logging = {
            piiLoggingEnabled: false,
            level: 3,
            log: function (message) {
                console.log(message);
            }
        };

        // ADAL configuration
        var adalConfig = {
            instance: 'https://login.microsoftonline.com/',
            clientId: "ENTER_CLIENT_ID_HERE",
            tenant: "ENTER_TENANT_ID_HERE",
            redirectUri: "ENTER_REDIRECT_URI_HERE",
            cacheLocation: "sessionStorage",
            popUp: true,
            callback: function (errorDesc, token, error, tokenType) {
                if (error) {
                    console.log(error, errorDesc);
                } else {
                    updateUI(authContext.getCachedUser());
                }
            }
        };

        // instantiate ADAL client object
        var authContext = new AuthenticationContext(adalConfig);

        // handle redirect response or check for cached user
        if (authContext.isCallback(window.location.hash)) {
            authContext.handleWindowCallback();
        } else {
            updateUI(authContext.getCachedUser());
        }

        // attach event handlers to button clicks
        loginButton.addEventListener('click', function () {
            authContext.login();
        });

        logoutButton.addEventListener('click', function () {
            authContext.logOut();
        });

        tokenButton.addEventListener('click', () => {
            authContext.acquireToken(
                "https://graph.microsoft.com",
                function (errorDesc, token, error) {
                    if (error) {
                        console.log(error, errorDesc);

                        authContext.acquireTokenPopup(
                            "https://graph.microsoft.com",
                            null, // extraQueryParameters
                            null, // claims
                            function (errorDesc, token, error) {
                                if (error) {
                                    console.log(error, errorDesc);
                                } else {
                                    console.log(token);
                                }
                            }
                        );
                    } else {
                        console.log(token);
                    }
                }
            );
        });
    </script>
</body>

</html>


<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript" src="https://alcdn.msauth.net/browser/2.34.0/js/msal-browser.min.js"></script>
</head>

<body>
    <div>
        <p id="welcomeMessage" style="visibility: hidden;"></p>
        <button id="loginButton">Login</button>
        <button id="logoutButton" style="visibility: hidden;">Logout</button>
        <button id="tokenButton" style="visibility: hidden;">Get Token</button>
    </div>
    <script>
        // DOM elements to work with
        const welcomeMessage = document.getElementById("welcomeMessage");
        const loginButton = document.getElementById("loginButton");
        const logoutButton = document.getElementById("logoutButton");
        const tokenButton = document.getElementById("tokenButton");

        // if user is logged in, update the UI
        const updateUI = (account) => {
            if (!account) {
                return;
            }

            welcomeMessage.innerHTML = `Hello ${account.username}!`;
            welcomeMessage.style.visibility = "visible";
            logoutButton.style.visibility = "visible";
            tokenButton.style.visibility = "visible";
            loginButton.style.visibility = "hidden";
        };

        // MSAL configuration
        const msalConfig = {
            auth: {
                clientId: "ENTER_CLIENT_ID_HERE",
                authority: "https://login.microsoftonline.com/ENTER_TENANT_ID_HERE",
                redirectUri: "ENTER_REDIRECT_URI_HERE",
            },
            cache: {
                cacheLocation: "sessionStorage"
            },
            system: {
                loggerOptions: {
                    loggerCallback(loglevel, message, containsPii) {
                        console.log(message);
                    },
                    piiLoggingEnabled: false,
                    logLevel: msal.LogLevel.Verbose,
                }
            }
        };

        // instantiate MSAL client object
        const pca = new msal.PublicClientApplication(msalConfig);

        // handle redirect response or check for cached user
        pca.handleRedirectPromise().then((response) => {
            if (response) {
                pca.setActiveAccount(response.account);
                updateUI(response.account);
            } else {
                const account = pca.getAllAccounts()[0];
                updateUI(account);
            }
        }).catch((error) => {
            console.log(error);
        });

        // attach event handlers to button clicks
        loginButton.addEventListener('click', () => {
            pca.loginPopup().then((response) => {
                pca.setActiveAccount(response.account);
                updateUI(response.account);
            })
        });

        logoutButton.addEventListener('click', () => {
            pca.logoutPopup().then((response) => {
                window.location.reload();
            });
        });

        tokenButton.addEventListener('click', () => {
            const account = pca.getActiveAccount();

            pca.acquireTokenSilent({
                account: account,
                scopes: ["User.Read"]
            }).then((response) => {
                console.log(response);
            }).catch((error) => {
                if (error instanceof msal.InteractionRequiredAuthError) {
                    pca.acquireTokenPopup({
                        scopes: ["User.Read"]
                    }).then((response) => {
                        console.log(response);
                    });
                }

                console.log(error);
            });
        });
    </script>
</body>

</html>

后续步骤