教程:使用授权代码流从 Angular 单页应用程序 (SPA) 将用户登录并调用 Microsoft Graph API

在本教程中,你将生成一个 Angular 单页应用程序 (SPA),该应用使用授权代码流和 PKCE 将用户登录并调用 Microsoft Graph API。 生成的 SPA 使用适用于 Angular 的 Microsoft 身份验证库 (MSAL) v2。

本教程的内容:

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

MSAL Angular v2 支持浏览器中的授权代码流(而不是隐式授权流),从而在 MSAL Angular v1 的基础上进行了改进。 MSAL Angular v2 不支持隐式流。

先决条件

示例应用工作原理

Diagram showing the authorization code flow in a single-page application

本教程中创建的示例应用程序使 Angular SPA 能够查询 Microsoft Graph API 或 Web API,该 API 接受 Microsoft 标识平台颁发的令牌。 它使用适用于 Angular 的 Microsoft 身份验证库 (MSAL) v2 - MSAL.js v2 库的包装器。 MSAL Angular 使 Angular 9+ 应用程序能够使用 Microsoft Entra ID 对企业用户和具有 Microsoft 帐户和社交标识(如 Facebook、Google 和 LinkedIn)的用户进行身份验证。 使用此库,应用程序还可以获取对 Microsoft 云服务和 Microsoft Graph 的访问权限。

在此方案中,用户登录后请求了访问令牌,并通过授权标头将其添加到 HTTP 请求。 令牌获取和续订通过 MSAL 处理。

本教程使用以下库:

说明
MSAL Angular 适用于 JavaScript Angular 的 Microsoft 身份验证库包装器
MSAL 浏览器 “适用于 JavaScript v2 的 Microsoft 身份验证库”浏览器包

可以在 GitHub 上的 microsoft-authentication-library-for-js 存储库中找到所有 MSAL.js 库的源代码。

获取完整代码示例

是否希望改为下载本教程的已完成示例项目? 克隆 ms-identity-javascript-angular-spa

git clone https://github.com/Azure-Samples/ms-identity-javascript-angular-spa.git

要继续本教程并自己构建应用程序,请转到下一节注册应用程序并记录标识符

注册应用程序和记录标识符

提示

本文中的步骤可能因开始使用的门户而略有不同。

若要完成注册,请为应用程序命名,指定支持的帐户类型并添加重定向 URI。 注册后,应用程序“概述”窗格将显示应用程序源代码中所需的标识符。

  1. 至少以应用程序开发人员的身份登录到 Microsoft Entra 管理中心
  2. 如果你有权访问多个租户,请使用顶部菜单中的“设置”图标 ,通过“目录 + 订阅”菜单切换到你希望在其中注册应用程序的租户。
  3. 浏览到“标识”>“应用程序”>“应用注册”。
  4. 选择“新注册”。
  5. 输入应用程序的名称,例如 Angular-SPA-auth-code
  6. 对于“支持的帐户类型”设置,请选择“仅限此组织目录中的帐户”。 要了解不同帐户类型的信息,请选择“帮我选择”选项。
  7. 在“重定向 URI (可选)”下,使用下拉菜单选择“单页应用程序(SPA)”,然后在文本框中输入 http://localhost:4200
  8. 选择“注册”。
  9. 注册完成后,将显示应用程序的“概述”窗格。 记录要在应用程序源代码中使用的目录(租户)ID 和应用程序(客户端)ID。

创建项目

  1. 打开 Visual Studio Code,选择“文件>“打开文件夹...”。导航到要在其中创建项目的位置并选中该位置。

  2. 通过选择“终端”>“新终端”打开一个新的终端。

    1. 可能需要切换终端类型。 选择终端中 + 图标旁边的向下箭头,然后选择“命令提示符”。
  3. 运行以下命令以创建名为 msal-angular-tutorial 的新 Angular 项目,安装 Angular 材料组件库、MSAL 浏览器、MSAL Angular 并生成主组件和配置文件组件。

    npm install -g @angular/cli
    ng new msal-angular-tutorial --routing=true --style=css --strict=false
    cd msal-angular-tutorial
    npm install @angular/material @angular/cdk
    npm install @azure/msal-browser @azure/msal-angular
    ng generate component home
    ng generate component profile
    

配置应用程序并编辑基本 UI

  1. 打开 src/app/app.module.tsMsalModuleMsalInterceptor 需要与常数 isIE 一起添加到 imports。 你还将添加材料模块。 将文件的全部内容替换为以下代码片段:

    import { BrowserModule } from "@angular/platform-browser";
    import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
    import { NgModule } from "@angular/core";
    
    import { MatButtonModule } from "@angular/material/button";
    import { MatToolbarModule } from "@angular/material/toolbar";
    import { MatListModule } from "@angular/material/list";
    
    import { AppRoutingModule } from "./app-routing.module";
    import { AppComponent } from "./app.component";
    import { HomeComponent } from "./home/home.component";
    import { ProfileComponent } from "./profile/profile.component";
    
    import { MsalModule, MsalRedirectComponent } from "@azure/msal-angular";
    import { PublicClientApplication } from "@azure/msal-browser";
    
    const isIE =
      window.navigator.userAgent.indexOf("MSIE ") > -1 ||
      window.navigator.userAgent.indexOf("Trident/") > -1;
    
    @NgModule({
      declarations: [AppComponent, HomeComponent, ProfileComponent],
      imports: [
        BrowserModule,
        BrowserAnimationsModule,
        AppRoutingModule,
        MatButtonModule,
        MatToolbarModule,
        MatListModule,
        MsalModule.forRoot(
          new PublicClientApplication({
            auth: {
              clientId: "Enter_the_Application_Id_here", // Application (client) ID from the app registration
              authority:
                "Enter_the_Cloud_Instance_Id_Here/Enter_the_Tenant_Info_Here", // The Azure cloud instance and the app's sign-in audience (tenant ID, common, organizations, or consumers)
              redirectUri: "Enter_the_Redirect_Uri_Here", // This is your redirect URI
            },
            cache: {
              cacheLocation: "localStorage",
              storeAuthStateInCookie: isIE, // Set to true for Internet Explorer 11
            },
          }),
          null,
          null
        ),
      ],
      providers: [],
      bootstrap: [AppComponent, MsalRedirectComponent],
    })
    export class AppModule {}
    
  2. 将以下值替换为从 Microsoft Entra 管理中心获取的值。 有关可用的可配置选项的详细信息,请阅读初始化客户端应用程序

    • clientId - 应用程序的标识符,也称为客户端。 将 Enter_the_Application_Id_Here 替换为注册应用程序的概述页中先前记录的应用程序(客户端) ID 值。
    • authority - 其由两个部分组成:
      • Instance 是云提供程序的终结点。 对于主要云或全球 Azure 云,请输入 https://login.microsoftonline.com。 在“国家云”查看可用的不同终结点。
      • 租户 ID 是在其中注册应用程序的租户的标识符。 将 _Enter_the_Tenant_Info_Here 替换为注册应用程序的概述页中先前记录的目录(租户)ID 值。
    • redirectUri–在为应用成功授权并为其授予授权代码或访问令牌后,授权服务器将用户发送到的位置。 将 Enter_the_Redirect_Uri_Here 替换为 http://localhost:4200
  3. 打开 src/app/app-routing.module.ts 并将路由添加到组件和配置文件组件。 将文件的全部内容替换为以下代码片段:

    import { NgModule } from "@angular/core";
    import { Routes, RouterModule } from "@angular/router";
    import { BrowserUtils } from "@azure/msal-browser";
    import { HomeComponent } from "./home/home.component";
    import { ProfileComponent } from "./profile/profile.component";
    
    const routes: Routes = [
      {
        path: "profile",
        component: ProfileComponent,
      },
      {
        path: "",
        component: HomeComponent,
      },
    ];
    
    const isIframe = window !== window.parent && !window.opener;
    
    @NgModule({
      imports: [
        RouterModule.forRoot(routes, {
          // Don't perform initial navigation in iframes or popups
          initialNavigation:
            !BrowserUtils.isInIframe() && !BrowserUtils.isInPopup()
              ? "enabledNonBlocking"
              : "disabled", // Set to enabledBlocking to use Angular Universal
        }),
      ],
      exports: [RouterModule],
    })
    export class AppRoutingModule {}
    
  4. 打开 src/app/app.component.html 并将现有代码替换为以下代码:

    <mat-toolbar color="primary">
      <a class="title" href="/">{{ title }}</a>
    
      <div class="toolbar-spacer"></div>
    
      <a mat-button [routerLink]="['profile']">Profile</a>
    
      <button mat-raised-button *ngIf="!loginDisplay" (click)="login()">Login</button>
    
    </mat-toolbar>
    <div class="container">
      <!--This is to avoid reload during acquireTokenSilent() because of hidden iframe -->
      <router-outlet *ngIf="!isIframe"></router-outlet>
    </div>
    
  5. 打开 src/style.css 以定义 CSS:

    @import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
    
    html,
    body {
      height: 100%;
    }
    body {
      margin: 0;
      font-family: Roboto, "Helvetica Neue", sans-serif;
    }
    .container {
      margin: 1%;
    }
    
  6. 打开 src/app/app.component.css,向应用程序添加 CSS 样式:

    .toolbar-spacer {
      flex: 1 1 auto;
    }
    
    a.title {
      color: white;
    }
    

使用弹出窗口登录

  1. 打开 src/app/app.component.ts 并将文件的内容替换为以下内容,以使用弹出窗口登录用户:

    import { MsalService } from '@azure/msal-angular';
    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit {
      title = 'msal-angular-tutorial';
      isIframe = false;
      loginDisplay = false;
    
      constructor(private authService: MsalService) { }
    
      ngOnInit() {
        this.isIframe = window !== window.parent && !window.opener;
      }
    
      login() {
        this.authService.loginPopup()
          .subscribe({
            next: (result) => {
              console.log(result);
              this.setLoginDisplay();
            },
            error: (error) => console.log(error)
          });
      }
    
      setLoginDisplay() {
        this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
      }
    }
    

使用重定向登录

  1. 更新 src/app/app.module.ts 以启动 MsalRedirectComponent。 这是一个用于处理重定向的专用重定向组件。 更改 MsalModule 导入和 AppComponent 启动,使其类似于以下内容:

    ...
    import { MsalModule, MsalRedirectComponent } from '@azure/msal-angular'; // Updated import
    ...
      bootstrap: [AppComponent, MsalRedirectComponent] // MsalRedirectComponent bootstrapped here
    ...
    
  2. 打开 src/index.html,将文件的全部内容替换为以下代码片段,这将添加 <app-redirect> 选择器:

    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>msal-angular-tutorial</title>
      <base href="/">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="icon" type="image/x-icon" href="favicon.ico">
    </head>
    <body>
      <app-root></app-root>
      <app-redirect></app-redirect>
    </body>
    </html>
    
  3. 打开 src/app/app.component.ts 并将代码替换为以下代码,以使用全框架重定向将用户登录:

    import { MsalService } from '@azure/msal-angular';
    import { Component, OnInit } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit {
      title = 'msal-angular-tutorial';
      isIframe = false;
      loginDisplay = false;
    
      constructor(private authService: MsalService) { }
    
      ngOnInit() {
        this.isIframe = window !== window.parent && !window.opener;
      }
    
      login() {
        this.authService.loginRedirect();
      }
    
      setLoginDisplay() {
        this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
      }
    }
    
  4. 导航到 src/app/home/home.component.ts,并将文件的全部内容替换为以下代码片段以订阅 LOGIN_SUCCESS 事件:

    import { Component, OnInit } from '@angular/core';
    import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
    import { EventMessage, EventType, InteractionStatus } from '@azure/msal-browser';
    import { filter } from 'rxjs/operators';
    
    @Component({
      selector: 'app-home',
      templateUrl: './home.component.html',
      styleUrls: ['./home.component.css']
    })
    export class HomeComponent implements OnInit {
      constructor(private authService: MsalService, private msalBroadcastService: MsalBroadcastService) { }
    
      ngOnInit(): void {
        this.msalBroadcastService.msalSubject$
          .pipe(
            filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS),
          )
          .subscribe((result: EventMessage) => {
            console.log(result);
          });
      }
    }
    

按条件呈现

若要仅向已经过身份验证的用户呈现特定的用户界面 (UI),组件必须订阅 MsalBroadcastService,以确定用户是否已登录以及交互是否已完成。

  1. MsalBroadcastService 添加到 src/app/app.component.ts 并订阅 inProgress$ 可观测对象,以便在呈现 UI 之前,检查交互是否已完成以及帐户是否已登录。 代码现应如下所示:

    import { Component, OnInit, OnDestroy } from '@angular/core';
    import { MsalService, MsalBroadcastService } from '@azure/msal-angular';
    import { InteractionStatus } from '@azure/msal-browser';
    import { Subject } from 'rxjs';
    import { filter, takeUntil } from 'rxjs/operators';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit, OnDestroy {
      title = 'msal-angular-tutorial';
      isIframe = false;
      loginDisplay = false;
      private readonly _destroying$ = new Subject<void>();
    
      constructor(private broadcastService: MsalBroadcastService, private authService: MsalService) { }
    
      ngOnInit() {
        this.isIframe = window !== window.parent && !window.opener;
    
        this.broadcastService.inProgress$
        .pipe(
          filter((status: InteractionStatus) => status === InteractionStatus.None),
          takeUntil(this._destroying$)
        )
        .subscribe(() => {
          this.setLoginDisplay();
        })
      }
    
      login() {
        this.authService.loginRedirect();
      }
    
      setLoginDisplay() {
        this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
      }
    
      ngOnDestroy(): void {
        this._destroying$.next(undefined);
        this._destroying$.complete();
      }
    }
    
  2. 更新 src/app/home/home.component.ts 中的代码,以便在更新 UI 之前也检查交互是否已完成。 代码现应如下所示:

    import { Component, OnInit } from '@angular/core';
    import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
    import { EventMessage, EventType, InteractionStatus } from '@azure/msal-browser';
    import { filter } from 'rxjs/operators';
    
    @Component({
      selector: 'app-home',
      templateUrl: './home.component.html',
      styleUrls: ['./home.component.css']
    })
    export class HomeComponent implements OnInit {
      loginDisplay = false;
    
      constructor(private authService: MsalService, private msalBroadcastService: MsalBroadcastService) { }
    
      ngOnInit(): void {
        this.msalBroadcastService.msalSubject$
          .pipe(
            filter((msg: EventMessage) => msg.eventType === EventType.LOGIN_SUCCESS),
          )
          .subscribe((result: EventMessage) => {
            console.log(result);
          });
    
        this.msalBroadcastService.inProgress$
          .pipe(
            filter((status: InteractionStatus) => status === InteractionStatus.None)
          )
          .subscribe(() => {
            this.setLoginDisplay();
          })
      }
    
      setLoginDisplay() {
        this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
      }
    }
    
  3. 将 src/app/home/home.component.html 中的代码替换为以下用于按条件显示内容的代码:

    <div *ngIf="!loginDisplay">
        <p>Please sign-in to see your profile information.</p>
    </div>
    
    <div *ngIf="loginDisplay">
        <p>Login successful!</p>
        <p>Request your profile information by clicking Profile above.</p>
    </div>
    

实现 Angular Guard

使用 MsalGuard 类可以保护路由,并要求在访问受保护路由之前先完成身份验证。 以下步骤将 MsalGuard 添加到 Profile 路由。 保护 Profile 路由意味着,即使用户不使用 Login 按钮登录,在他们尝试访问 Profile 路由或选择 Profile 按钮时,MsalGuard 在显示 Profile 页面之前也会提示用户通过弹出窗口进行身份验证,否则会进行重定向。

MsalGuard 是一个可用于改善用户体验的便捷类,但出于安全考虑,不应依赖于此类。 攻击者可能会绕过客户端保护,你应该确保服务器不会返回用户不应访问的任何数据。

  1. 在 src/app/app.module.ts 中添加 MsalGuard 类作为应用程序中的提供程序,并添加 MsalGuard 的配置。 可以在 authRequest 中提供稍后用于获取令牌的范围,可将保护功能的交互类型设置为 RedirectPopup。 代码应如下所示:

    import { BrowserModule } from "@angular/platform-browser";
    import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
    import { NgModule } from "@angular/core";
    
    import { MatButtonModule } from "@angular/material/button";
    import { MatToolbarModule } from "@angular/material/toolbar";
    import { MatListModule } from "@angular/material/list";
    
    import { AppRoutingModule } from "./app-routing.module";
    import { AppComponent } from "./app.component";
    import { HomeComponent } from "./home/home.component";
    import { ProfileComponent } from "./profile/profile.component";
    
    import {
      MsalModule,
      MsalRedirectComponent,
      MsalGuard,
    } from "@azure/msal-angular"; // MsalGuard added to imports
    import {
      PublicClientApplication,
      InteractionType,
    } from "@azure/msal-browser"; // InteractionType added to imports
    
    const isIE =
      window.navigator.userAgent.indexOf("MSIE ") > -1 ||
      window.navigator.userAgent.indexOf("Trident/") > -1;
    
    @NgModule({
      declarations: [AppComponent, HomeComponent, ProfileComponent],
      imports: [
        BrowserModule,
        BrowserAnimationsModule,
        AppRoutingModule,
        MatButtonModule,
        MatToolbarModule,
        MatListModule,
        MsalModule.forRoot(
          new PublicClientApplication({
            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: "localStorage",
              storeAuthStateInCookie: isIE,
            },
          }),
          {
            interactionType: InteractionType.Redirect, // MSAL Guard Configuration
            authRequest: {
              scopes: ["user.read"],
            },
          },
          null
        ),
      ],
      providers: [
        MsalGuard, // MsalGuard added as provider here
      ],
      bootstrap: [AppComponent, MsalRedirectComponent],
    })
    export class AppModule {}
    
  2. 在 src/app/app-routing.module.ts 中针对要保护的路由设置 MsalGuard

    import { NgModule } from "@angular/core";
    import { Routes, RouterModule } from "@angular/router";
    import { BrowserUtils } from "@azure/msal-browser";
    import { HomeComponent } from "./home/home.component";
    import { ProfileComponent } from "./profile/profile.component";
    import { MsalGuard } from "@azure/msal-angular";
    
    const routes: Routes = [
      {
        path: "profile",
        component: ProfileComponent,
        canActivate: [MsalGuard],
      },
      {
        path: "",
        component: HomeComponent,
      },
    ];
    
    const isIframe = window !== window.parent && !window.opener;
    
    @NgModule({
      imports: [
        RouterModule.forRoot(routes, {
          // Don't perform initial navigation in iframes or popups
          initialNavigation:
            !BrowserUtils.isInIframe() && !BrowserUtils.isInPopup()
              ? "enabledNonBlocking"
              : "disabled", // Set to enabledBlocking to use Angular Universal
        }),
      ],
      exports: [RouterModule],
    })
    export class AppRoutingModule {}
    
  3. 在 src/app/app.component.ts 中调整登录调用,以将保护配置中设置的 authRequest 考虑在内。 代码现在应如下所示:

    import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
    import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration } from '@azure/msal-angular';
    import { InteractionStatus, RedirectRequest } from '@azure/msal-browser';
    import { Subject } from 'rxjs';
    import { filter, takeUntil } from 'rxjs/operators';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit, OnDestroy {
      title = 'msal-angular-tutorial';
      isIframe = false;
      loginDisplay = false;
      private readonly _destroying$ = new Subject<void>();
    
      constructor(@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration, private broadcastService: MsalBroadcastService, private authService: MsalService) { }
    
      ngOnInit() {
        this.isIframe = window !== window.parent && !window.opener;
    
        this.broadcastService.inProgress$
        .pipe(
          filter((status: InteractionStatus) => status === InteractionStatus.None),
          takeUntil(this._destroying$)
        )
        .subscribe(() => {
          this.setLoginDisplay();
        })
      }
    
      login() {
        if (this.msalGuardConfig.authRequest){
          this.authService.loginRedirect({...this.msalGuardConfig.authRequest} as RedirectRequest);
        } else {
          this.authService.loginRedirect();
        }
      }
    
      setLoginDisplay() {
        this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
      }
    
      ngOnDestroy(): void {
        this._destroying$.next(undefined);
        this._destroying$.complete();
      }
    }
    

获取令牌

Angular 侦听器

MSAL Angular 向已知的受保护资源提供一个 Interceptor 类,该类可自动为使用 Angular http 客户端的传出请求获取令牌。

  1. src/app/app.module.ts 中,将 Interceptor 类作为提供程序添加到应用程序,并添加此类的配置。 代码现在应如下所示:

    import { BrowserModule } from "@angular/platform-browser";
    import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
    import { NgModule } from "@angular/core";
    import { HTTP_INTERCEPTORS, HttpClientModule } from "@angular/common/http"; // Import
    
    import { MatButtonModule } from "@angular/material/button";
    import { MatToolbarModule } from "@angular/material/toolbar";
    import { MatListModule } from "@angular/material/list";
    
    import { AppRoutingModule } from "./app-routing.module";
    import { AppComponent } from "./app.component";
    import { HomeComponent } from "./home/home.component";
    import { ProfileComponent } from "./profile/profile.component";
    
    import {
      MsalModule,
      MsalRedirectComponent,
      MsalGuard,
      MsalInterceptor,
    } from "@azure/msal-angular"; // Import MsalInterceptor
    import {
      InteractionType,
      PublicClientApplication,
    } from "@azure/msal-browser";
    
    const isIE =
      window.navigator.userAgent.indexOf("MSIE ") > -1 ||
      window.navigator.userAgent.indexOf("Trident/") > -1;
    
    @NgModule({
      declarations: [AppComponent, HomeComponent, ProfileComponent],
      imports: [
        BrowserModule,
        BrowserAnimationsModule,
        AppRoutingModule,
        MatButtonModule,
        MatToolbarModule,
        MatListModule,
        HttpClientModule,
        MsalModule.forRoot(
          new PublicClientApplication({
            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: "localStorage",
              storeAuthStateInCookie: isIE,
            },
          }),
          {
            interactionType: InteractionType.Redirect,
            authRequest: {
              scopes: ["user.read"],
            },
          },
          {
            interactionType: InteractionType.Redirect, // MSAL Interceptor Configuration
            protectedResourceMap: new Map([
              ["Enter_the_Graph_Endpoint_Here/v1.0/me", ["user.read"]],
            ]),
          }
        ),
      ],
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: MsalInterceptor,
          multi: true,
        },
        MsalGuard,
      ],
      bootstrap: [AppComponent, MsalRedirectComponent],
    })
    export class AppModule {}
    

    受保护的资源是作为 protectedResourceMap 提供的。 在 protectedResourceMap 集合中输入的 URL 区分大小写。 对于每个资源,添加所请求的、需要在访问令牌中返回的范围。

    例如:

    • Microsoft Graph 的 ["user.read"]
    • 自定义 Web API 的 ["<Application ID URL>/scope"](即 api://<Application ID>/access_as_user

    按下面所述修改 protectedResourceMap 中的值:

    • Enter_the_Graph_Endpoint_Here 是应用程序应与之通信的 Microsoft Graph API 实例。 对于全局 Microsoft Graph API 终结点,将此字符串替换为 https://graph.microsoft.com。 对于国家/地区云部署中的终结点,请参阅 Microsoft Graph 文档中的国家/地区云部署
  2. 替换 src/app/profile/profile.component.ts 中的代码以使用 HTTP 请求检索用户配置文件,并将 GRAPH_ENDPOINT 替换为 Microsoft Graph 终结点:

    import { Component, OnInit } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    
    const GRAPH_ENDPOINT = 'Enter_the_Graph_Endpoint_Here/v1.0/me';
    
    type ProfileType = {
      givenName?: string,
      surname?: string,
      userPrincipalName?: string,
      id?: string
    };
    
    @Component({
      selector: 'app-profile',
      templateUrl: './profile.component.html',
      styleUrls: ['./profile.component.css']
    })
    export class ProfileComponent implements OnInit {
      profile!: ProfileType;
    
      constructor(
        private http: HttpClient
      ) { }
    
      ngOnInit() {
        this.getProfile();
      }
    
      getProfile() {
        this.http.get(GRAPH_ENDPOINT)
          .subscribe(profile => {
            this.profile = profile;
          });
      }
    }
    
  3. 替换 src/app/profile/profile.component.html 中的 UI,以显示配置文件信息:

    <div>
        <p><strong>First Name: </strong> {{profile?.givenName}}</p>
        <p><strong>Last Name: </strong> {{profile?.surname}}</p>
        <p><strong>Email: </strong> {{profile?.userPrincipalName}}</p>
        <p><strong>Id: </strong> {{profile?.id}}</p>
    </div>
    

注销

  1. 更新 src/app/app.component.html 中的代码,以按条件显示 Logout 按钮:

    <mat-toolbar color="primary">
      <a class="title" href="/">{{ title }}</a>
    
      <div class="toolbar-spacer"></div>
    
      <a mat-button [routerLink]="['profile']">Profile</a>
    
      <button mat-raised-button *ngIf="!loginDisplay" (click)="login()">Login</button>
      <button mat-raised-button *ngIf="loginDisplay" (click)="logout()">Logout</button>
    
    </mat-toolbar>
    <div class="container">
      <!--This is to avoid reload during acquireTokenSilent() because of hidden iframe -->
      <router-outlet *ngIf="!isIframe"></router-outlet>
    </div>
    

使用重定向注销

  1. 更新 src/app/app.component.ts 中的代码,以使用重定向将用户注销:

    import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
    import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration } from '@azure/msal-angular';
    import { InteractionStatus, RedirectRequest } from '@azure/msal-browser';
    import { Subject } from 'rxjs';
    import { filter, takeUntil } from 'rxjs/operators';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit, OnDestroy {
      title = 'msal-angular-tutorial';
      isIframe = false;
      loginDisplay = false;
      private readonly _destroying$ = new Subject<void>();
    
      constructor(@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration, private broadcastService: MsalBroadcastService, private authService: MsalService) { }
    
      ngOnInit() {
        this.isIframe = window !== window.parent && !window.opener;
    
        this.broadcastService.inProgress$
        .pipe(
          filter((status: InteractionStatus) => status === InteractionStatus.None),
          takeUntil(this._destroying$)
        )
        .subscribe(() => {
          this.setLoginDisplay();
        })
      }
    
      login() {
        if (this.msalGuardConfig.authRequest){
          this.authService.loginRedirect({...this.msalGuardConfig.authRequest} as RedirectRequest);
        } else {
          this.authService.loginRedirect();
        }
      }
    
      logout() { // Add log out function here
        this.authService.logoutRedirect({
          postLogoutRedirectUri: 'http://localhost:4200'
        });
      }
    
      setLoginDisplay() {
        this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
      }
    
      ngOnDestroy(): void {
        this._destroying$.next(undefined);
        this._destroying$.complete();
      }
    }
    

使用弹出窗口注销

  1. 更新 src/app/app.component.ts 中的代码,以使用弹出窗口将用户注销:

    import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
    import { MsalService, MsalBroadcastService, MSAL_GUARD_CONFIG, MsalGuardConfiguration } from '@azure/msal-angular';
    import { InteractionStatus, PopupRequest } from '@azure/msal-browser';
    import { Subject } from 'rxjs';
    import { filter, takeUntil } from 'rxjs/operators';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent implements OnInit, OnDestroy {
      title = 'msal-angular-tutorial';
      isIframe = false;
      loginDisplay = false;
      private readonly _destroying$ = new Subject<void>();
    
      constructor(@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration, private broadcastService: MsalBroadcastService, private authService: MsalService) { }
    
      ngOnInit() {
        this.isIframe = window !== window.parent && !window.opener;
    
        this.broadcastService.inProgress$
        .pipe(
          filter((status: InteractionStatus) => status === InteractionStatus.None),
          takeUntil(this._destroying$)
        )
        .subscribe(() => {
          this.setLoginDisplay();
        })
      }
    
      login() {
        if (this.msalGuardConfig.authRequest){
          this.authService.loginPopup({...this.msalGuardConfig.authRequest} as PopupRequest)
            .subscribe({
              next: (result) => {
                console.log(result);
                this.setLoginDisplay();
              },
              error: (error) => console.log(error)
            });
        } else {
          this.authService.loginPopup()
            .subscribe({
              next: (result) => {
                console.log(result);
                this.setLoginDisplay();
              },
              error: (error) => console.log(error)
            });
        }
      }
    
      logout() { // Add log out function here
        this.authService.logoutPopup({
          mainWindowRedirectUri: "/"
        });
      }
    
      setLoginDisplay() {
        this.loginDisplay = this.authService.instance.getAllAccounts().length > 0;
      }
    
      ngOnDestroy(): void {
        this._destroying$.next(undefined);
        this._destroying$.complete();
      }
    }
    

测试代码

  1. 通过在命令行提示符下从应用程序文件夹运行以下命令,启动 Web 服务器来侦听端口:

    npm install
    npm start
    
  2. 在浏览器中,输入 http://localhost:4200,应会看到如下所示的页面。

    Web browser displaying sign-in dialog

  3. 选择“接受”,向应用授予对配置文件的权限。 这将在首次开始登录时发生。

    Content dialog displayed in web browser

  4. 同意后,如果你同意请求的权限,Web 应用程序将显示成功登录页面。

    Results of a successful sign-in in the web browser

  5. 选择“配置文件”,以查看在调用 Microsoft Graph API 后提供的响应中返回的用户配置文件信息:

    Profile information from Microsoft Graph displayed in the browser

添加范围和委托的权限

Microsoft Graph API 需要 User.Read 作用域来读取用户的个人资料。 User.Read 范围会自动添加到每个应用注册。 Microsoft Graph 的其他 API 以及后端服务器的自定义 API 可能需要其他作用域。 例如,Microsoft Graph API 需要使用 Mail.Read 作用域来列出用户的电子邮件。

添加作用域时,可能会提示用户为添加的作用域提供额外许可。

注意

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

帮助和支持

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

后续步骤

  • 在以下多部分教程系列中详细了解如何生成一个让用户登录的 React 单页应用程序 (SPA)。