在使用 MSAL.js 以无提示方式获取和续订令牌时避免页面重载Avoid page reloads when acquiring and renewing tokens silently using MSAL.js

适用于 JavaScript 的 Microsoft 身份验证库 (MSAL.js) 使用隐藏的 iframe 元素在后台中以无提示方式获取和续订令牌。Microsoft Authentication Library for JavaScript (MSAL.js) uses hidden iframe elements to acquire and renew tokens silently in the background. Azure AD 将令牌返回到在令牌请求中指定的已注册 redirect_uri(默认情况下,这是应用的根页面)。Azure AD returns the token back to the registered redirect_uri specified in the token request(by default this is the app's root page). 由于响应是 302,因此会生成 HTML,与在 iframe 中加载的 redirect_uri 相对应。Since the response is a 302, it results in the HTML corresponding to the redirect_uri getting loaded in the iframe. 通常情况下,应用的 redirect_uri 是根页面,这样会导致它重载。Usually the app's redirect_uri is the root page and this causes it to reload.

在其他情况下,如果导航到应用的根页面需要身份验证,则可能导致嵌套的 iframe 元素或 X-Frame-Options: deny 错误。In other cases, if navigating to the app's root page requires authentication, it might lead to nested iframe elements or X-Frame-Options: deny error.

由于 MSAL.js 不能消除 Azure AD 发出的 302 且是处理返回的令牌所必需的,因此它不能防止 redirect_uriiframe 中加载。Since MSAL.js cannot dismiss the 302 issued by Azure AD and is required to process the returned token, it cannot prevent the redirect_uri from getting loaded in the iframe.

若要避免整个应用再次重载,或者避免因此导致的其他错误,请执行以下解决方法。To avoid the entire app reloading again or other errors caused due to this, please follow these workarounds.

为 iframe 指定不同的 HTMLSpecify different HTML for the iframe

将配置中的 redirect_uri 属性设置为一个简单页面,这不需要身份验证。Set the redirect_uri property on config to a simple page, that does not require authentication. 必须确保它与在 Azure 门户中注册的 redirect_uri 匹配。You have to make sure that it matches with the redirect_uri registered in Azure portal. 这不会影响用户的登录体验,因为 MSAL 会在用户开始登录过程时保存启动页,并在登录完成后重定向回确切的位置。This will not affect user's login experience as MSAL saves the start page when user begins the login process and redirects back to the exact location after login is completed.

主应用文件中的初始化Initialization in your main app file

如果在设置应用的结构时,使用一个中心 Javascript 文件来定义应用的初始化、路由和其他事项,则可根据应用是否在 iframe 中加载来有条件性地加载应用模块。If your app is structured such that there is one central Javascript file that defines the app's initialization, routing, and other stuff, you can conditionally load your app modules based on whether the app is loading in an iframe or not. 例如:For example:

在 AngularJS 中:app.jsIn AngularJS: app.js

// Check that the window is an iframe and not popup
if (window !== window.parent && !window.opener) {
angular.module('todoApp', ['ui.router', 'MsalAngular'])
    .config(['$httpProvider', 'msalAuthenticationServiceProvider','$locationProvider', function ($httpProvider, msalProvider,$locationProvider) {
        msalProvider.init(
            // msal configuration
        );

        $locationProvider.html5Mode(false).hashPrefix('');
    }]);
}
else {
    angular.module('todoApp', ['ui.router', 'MsalAngular'])
        .config(['$stateProvider', '$httpProvider', 'msalAuthenticationServiceProvider', '$locationProvider', function ($stateProvider, $httpProvider, msalProvider, $locationProvider) {
            $stateProvider.state("Home", {
                url: '/Home',
                controller: "homeCtrl",
                templateUrl: "/App/Views/Home.html",
            }).state("TodoList", {
                url: '/TodoList',
                controller: "todoListCtrl",
                templateUrl: "/App/Views/TodoList.html",
                requireLogin: true
            })

            $locationProvider.html5Mode(false).hashPrefix('');

            msalProvider.init(
                // msal configuration
            );
        }]);
}

在 Angular 中:app.module.tsIn Angular: app.module.ts

// Imports...
@NgModule({
  declarations: [
    AppComponent,
    MsalComponent,
    MainMenuComponent,
    AccountMenuComponent,
    OsNavComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
    MsalModule.forRoot(environment.MsalConfig),
    SuiModule,
    PagesModule
  ],
  providers: [
    HttpServiceHelper,
    {provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true},
    AuthService
  ],
  entryComponents: [
    AppComponent,
    MsalComponent
  ]
})
export class AppModule {
  constructor() {
    console.log('APP Module Constructor!');
  }

  ngDoBootstrap(ref: ApplicationRef) {
    if (window !== window.parent && !window.opener)
    {
      console.log("Bootstrap: MSAL");
      ref.bootstrap(MsalComponent);
    }
    else
    {
    //this.router.resetConfig(RouterModule);
      console.log("Bootstrap: App");
      ref.bootstrap(AppComponent);
    }
  }
}

MsalComponent:MsalComponent:

import { Component} from '@angular/core';
import { MsalService } from '@azure/msal-angular';

// This component is used only to avoid Angular reload
// when doing acquireTokenSilent()

@Component({
  selector: 'app-root',
  template: '',
})
export class MsalComponent {
  constructor(private Msal: MsalService) {
  }
}

后续步骤Next steps

详细了解如何使用 MSAL.js 生成单页应用程序 (SPA)Learn more about building a single-page application (SPA) using MSAL.js.