教學課程:使用驗證碼流程從 Angular 單頁應用程式登入使用者並呼叫 Microsoft Graph API

在本教學課程中,您將建置 Angular 單頁應用程式 (SPA),以使用授權碼流程搭配 PKCE 來登入使用者並呼叫 Microsoft Graph API。 您所建置的 SPA 會使用適用於 Angular v2 的 Microsoft 驗證連結庫 (MSAL)。

在本教學課程中:

  • 在 Microsoft Entra 系統管理中心註冊應用程式
  • 使用建立 Angular 專案 npm
  • 新增程式代碼以支援使用者登入和註銷
  • 新增程式代碼以呼叫 Microsoft Graph API
  • 測試應用程式

MSAL Angular v2 藉由在瀏覽器中支援授權碼流程,而不是隱含授與流程,改善 MSAL Angular v1。 MSAL Angular v2 不支援隱含流程。

必要條件

  • Node.js執行本機網頁伺服器。
  • Visual Studio Code 或其他編輯器,用於修改項目檔。

範例應用程式的運作方式

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

本教學課程中建立的範例應用程式可讓 Angular SPA 查詢 Microsoft Graph API 或 Web API,以接受 Microsoft 身分識別平台 所簽發的令牌。 它會使用 Microsoft Authentication Library (MSAL) for Angular v2,這是 MSAL.js v2 連結庫的包裝函式。 MSAL Angular 可讓 Angular 9+ 應用程式使用 Microsoft Entra ID 來驗證企業使用者,以及具有 Facebook、Google 和 LinkedIn 等 Microsoft 帳戶和社交身分識別的使用者。 連結庫也可讓應用程式存取 Microsoft 雲端服務和 Microsoft Graph。

在此案例中,在使用者登入之後,會要求存取令牌,並透過授權標頭新增至 HTTP 要求。 令牌擷取和更新是由 MSAL 處理。

Libraries

本教學課程使用下列連結庫:

程式庫 描述
MSAL Angular 適用於 JavaScript Angular 包裝函式的 Microsoft 驗證連結庫
MSAL 瀏覽器 適用於 JavaScript v2 瀏覽器套件的 Microsoft 驗證連結庫

您可以在 GitHub 上的存放庫中找到所有MSAL.js連結庫的 microsoft-authentication-library-for-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. 註冊完成時,會顯示應用程式的 [ 概觀 ] 窗格。 記錄要用於應用程式原始碼的目錄(租使用者)標識碼應用程式(用戶端)標識碼

建立專案

  1. 開啟 Visual Studio Code,選取 [檔案>開啟資料夾...]。流覽至 並選取要在其中建立專案的位置。

  2. 選取 [終端>機新終端機] 以開啟新的終端機。

    1. 您可能需要切換終端機類型。 選取終端機中圖示旁的 + 向下箭號,然後選取 [ 命令提示字元]。
  3. 執行下列命令以建立名稱為 msal-angular-tutorial的新 Angular 專案、安裝 Angular Material 元件庫、MSAL Browser、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.ts。 與 MsalModuleMsalInterceptor 必須與 常數一起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先前從已註冊應用程式概觀頁面記錄的應用程式 (client) 識別碼 值。
    • authority - 這由兩個部分組成:
      • 實例是雲端提供者的端點。 針對主要或全域 Azure 雲端,輸入 https://login.microsoftonline.com。 在國家雲端檢查不同的可用端點。
      • 用戶標識碼 是註冊應用程式之租用戶的標識碼。 _Enter_the_Tenant_Info_Here將 取代為先前從已註冊應用程式概觀頁面記錄的 Directory (tenant) 識別碼值。
    • 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。 這是處理重新導向的專用重新導向元件。 將匯入 MsalModuleAppComponent 啟動程式變更為類似下列內容:

    ...
    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 是您可以用來保護路由的類別,而且在存取受保護的路由之前需要驗證。 下列步驟會將 新增 MsalGuardProfile 路由。 Profile保護路由表示,即使使用者未使用Login按鈕登入,如果他們嘗試存取Profile路由或選取Profile該按鈕,則會MsalGuard提示使用者在顯示Profile頁面之前先透過快顯或重新導向進行驗證。

MsalGuard 是一個方便的類別,可用來改善用戶體驗,但不應該依賴它的安全性。 攻擊者可能會繞過用戶端防護,您應該確保伺服器不會傳回使用者不應該存取的任何數據。

  1. 在 src/app/app.module.ts 中,將 MsalGuard 類別新增為應用程式中的提供者,並新增的MsalGuard組態。 稍後可以在 中 authRequest提供取得權杖所需的範圍,而 Guard 的互動類型可以設定為 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. MsalGuard在您要在 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";
    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 會區分大小寫。 針對每個資源,請在存取令牌中新增要求傳回的範圍。

    例如:

    • ["user.read"] for Microsoft Graph
    • ["<Application ID URL>/scope"] 適用於自訂 Web API(也就是 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 要求擷取使用者配置檔,並以 Microsoft Graph 端點取代 GRAPH_ENDPOINT

    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) 以在下列多部分 教學課程系列中登入使用者,以深入瞭解。