這很重要
自 2025 年 5 月 1 日起,Azure AD B2C 將不再可供新客戶購買。 在我們的常見問題中深入瞭解。
本文說明如何將 Azure Active Directory B2C (Azure AD B2C) 驗證新增至您自己的 Angular 單頁應用程式 (SPA)。 瞭解如何整合 Angular 應用程式與 MSAL for Angular 驗證連結庫。
使用這篇文章與標題為 在 Angular 單頁範例應用程式中設定驗證 的相關文章。 以您自己的 Angular 應用程式取代範例 Angular 應用程式。 完成本文中的步驟之後,您的應用程式會透過 Azure AD B2C 接受登入。
先決條件
完成在 Angular 單頁應用程式範例 中設定驗證一文中的步驟。
建立 Angular 應用程式專案
您可以使用現有的 Angular 應用程式專案,或建立新的專案。 若要建立新的專案,請執行下列命令。
命令:
- 使用 npm 套件管理員安裝 Angular CLI 。
- 使用路由模組建立 Angular 工作區。 應用程式名稱稱為
msal-angular-tutorial。 您可以變更為任何有效的 Angular 應用程式名稱, 例如contoso-car-service。 - 移至應用程式目錄資料夾。
npm install -g @angular/cli
ng new msal-angular-tutorial --routing=true --style=css --strict=false
cd msal-angular-tutorial
安裝相依性
若要在應用程式中安裝 MSAL Browser 和 MSAL Angular 連結庫,請在命令殼層中執行下列命令:
npm install @azure/msal-browser @azure/msal-angular
安裝 Angular Material 元件庫 (選擇性,適用於 UI):
npm install @angular/material @angular/cdk
新增驗證元件
範例程式代碼包含下列元件:
| 元件 | 類型 | 說明 |
|---|---|---|
| auth-config.ts | 常數 | 此組態檔包含 Azure AD B2C 識別提供者和 Web API 服務的相關信息。 Angular 應用程式會使用此資訊來建立與 Azure AD B2C 的信任關係、登入和註銷使用者、取得令牌,以及驗證令牌。 |
| app.module.ts | Angular 模組 | 此元件描述應用程式元件如何配合在一起。 這是用來啟動程式並開啟應用程式的根模組。 在本逐步解說中,您會將某些元件新增至 app.module.ts 模組,並使用 MSAL 組態對象啟動 MSAL 連結庫。 |
| app-routing.module.ts | Angular 路由模組 | 此元件可藉由解譯瀏覽器 URL 並載入對應的元件來啟用流覽。 在本逐步解說中,您會將一些元件新增至路由模組,並使用 MSAL Guard 來保護元件。 只有授權的使用者才能存取受保護的元件。 |
| app.component.* | Angular 元件 | 命令 ng new 會建立具有根元件的 Angular 專案。 在本演練中,您會將 應用程式 元件變更為用來承載頂端導覽列。 導覽列包含各種按鈕,包括登入和註銷按鈕。 類別 app.component.ts 會處理登入和註銷事件。 |
| home.component.* | Angular 元件 | 在本逐步解說中,您會新增 home 元件來渲染首頁以進行匿名存取。 此元件示範如何檢查使用者是否已登入。 |
| profile.component.* | Angular 元件 | 在此逐步解說中,您會新增 profile 元件,以了解如何讀取識別碼權杖宣告。 |
| webapi.component.* | Angular 元件 | 在本逐步解說中,您會新增 webapi 元件,以瞭解如何呼叫 Web API。 |
若要將下列元件新增至您的應用程式,請執行下列 Angular CLI 命令。 generate component命令:
- 為每個元件建立資料夾。 資料夾包含 TypeScript、HTML、CSS 和測試檔案。
- 更新
app.module.ts和app-routing.module.ts檔案,以包含對新元件的參考。
ng generate component home
ng generate component profile
ng generate component webapi
新增應用程式設定
Azure AD B2C 識別提供者和 Web API 的設定會儲存在 auth-config.ts 檔案中。 在您的 src/app 資料夾中,建立名為 auth-config.ts 的檔案,其中包含下列程式代碼。 然後,如 3.1 設定 Angular 範例中所述變更設定。
import { LogLevel, Configuration, BrowserCacheLocation } from '@azure/msal-browser';
const isIE = window.navigator.userAgent.indexOf("MSIE ") > -1 || window.navigator.userAgent.indexOf("Trident/") > -1;
export const b2cPolicies = {
names: {
signUpSignIn: "b2c_1_susi_reset_v2",
editProfile: "b2c_1_edit_profile_v2"
},
authorities: {
signUpSignIn: {
authority: "https://your-tenant-name.b2clogin.com/your-tenant-name.onmicrosoft.com/b2c_1_susi_reset_v2",
},
editProfile: {
authority: "https://your-tenant-name.b2clogin.com/your-tenant-name.onmicrosoft.com/b2c_1_edit_profile_v2"
}
},
authorityDomain: "your-tenant-name.b2clogin.com"
};
export const msalConfig: Configuration = {
auth: {
clientId: '<your-MyApp-application-ID>',
authority: b2cPolicies.authorities.signUpSignIn.authority,
knownAuthorities: [b2cPolicies.authorityDomain],
redirectUri: '/',
},
cache: {
cacheLocation: BrowserCacheLocation.LocalStorage,
storeAuthStateInCookie: isIE,
},
system: {
loggerOptions: {
loggerCallback: (logLevel, message, containsPii) => {
console.log(message);
},
logLevel: LogLevel.Verbose,
piiLoggingEnabled: false
}
}
}
export const protectedResources = {
todoListApi: {
endpoint: "http://localhost:5000/api/todolist",
scopes: ["https://your-tenant-name.onmicrosoft.com/api/tasks.read"],
},
}
export const loginRequest = {
scopes: []
};
啟動驗證程式庫
公用用戶端應用程式不受信任,無法安全地保留應用程式秘密,因此它們沒有客戶端密碼。 在 src/app 資料夾中,開啟 app.module.ts 並進行下列變更:
- 匯入 MSAL Angular 和 MSAL Browser 連結庫。
- 匯入 Azure AD B2C 組態模組。
- 匯入
HttpClientModule。 HTTP 用戶端可用來呼叫 Web API。 - 匯入 Angular HTTP 攔截器。 MSAL 會使用攔截器將持有人令牌插入 HTTP 授權標頭。
- 新增必要的 Angular 材質。
- 使用多帳戶公用客戶端應用程式物件來具現化 MSAL。 MSAL 初始化包括傳遞:
- auth-config.ts的組態物件。
- 路由防護的組態物件。
- MSAL 攔截器的組態物件。 攔截器類別會自動取得傳出要求的權杖,而此權杖使用已知受保護資源的 Angular HttpClient 類別。
- 設定
HTTP_INTERCEPTORS和MsalGuardAngular 提供者。 - 將
MsalRedirectComponent新增至 Angular 啟動程序。
在 src/app 資料夾中,編輯 app.module.ts 並進行下列代碼段所示的修改。 這些變更會標幟為「變更從這裡開始」和「變更在這裡結束」。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
/* Changes start here. */
// Import MSAL and MSAL browser libraries.
import { MsalGuard, MsalInterceptor, MsalModule, MsalRedirectComponent } from '@azure/msal-angular';
import { InteractionType, PublicClientApplication } from '@azure/msal-browser';
// Import the Azure AD B2C configuration
import { msalConfig, protectedResources } from './auth-config';
// Import the Angular HTTP interceptor.
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { ProfileComponent } from './profile/profile.component';
import { HomeComponent } from './home/home.component';
import { WebapiComponent } from './webapi/webapi.component';
// Add the essential Angular materials.
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatListModule } from '@angular/material/list';
import { MatTableModule } from '@angular/material/table';
/* Changes end here. */
@NgModule({
declarations: [
AppComponent,
ProfileComponent,
HomeComponent,
WebapiComponent
],
imports: [
BrowserModule,
AppRoutingModule,
/* Changes start here. */
// Import the following Angular materials.
MatButtonModule,
MatToolbarModule,
MatListModule,
MatTableModule,
// Import the HTTP client.
HttpClientModule,
// Initiate the MSAL library with the MSAL configuration object
MsalModule.forRoot(new PublicClientApplication(msalConfig),
{
// The routing guard configuration.
interactionType: InteractionType.Redirect,
authRequest: {
scopes: protectedResources.todoListApi.scopes
}
},
{
// MSAL interceptor configuration.
// The protected resource mapping maps your web API with the corresponding app scopes. If your code needs to call another web API, add the URI mapping here.
interactionType: InteractionType.Redirect,
protectedResourceMap: new Map([
[protectedResources.todoListApi.endpoint, protectedResources.todoListApi.scopes]
])
})
/* Changes end here. */
],
providers: [
/* Changes start here. */
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
multi: true
},
MsalGuard
/* Changes end here. */
],
bootstrap: [
AppComponent,
/* Changes start here. */
MsalRedirectComponent
/* Changes end here. */
]
})
export class AppModule { }
設定路由
在本節中,設定 Angular 應用程式的路由。 當使用者選取頁面上的連結以在單頁應用程式中移動,或在網址列中輸入 URL 時,路由會將 URL 對應至 Angular 元件。 Angular 路由 canActivate 介面會使用 MSAL Guard 來檢查使用者是否已登入。 如果使用者未登入,MSAL 會將使用者帶往 Azure AD B2C 進行驗證。
在 src/app 資料夾中,編輯 app-routing.module.ts 進行下列代碼段中顯示的修改。 這些變更會標幟為「變更從這裡開始」和「變更在這裡結束」。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { MsalGuard } from '@azure/msal-angular';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { WebapiComponent } from './webapi/webapi.component';
const routes: Routes = [
/* Changes start here. */
{
path: 'profile',
component: ProfileComponent,
// The profile component is protected with MSAL Guard.
canActivate: [MsalGuard]
},
{
path: 'webapi',
component: WebapiComponent,
// The profile component is protected with MSAL Guard.
canActivate: [MsalGuard]
},
{
// The home component allows anonymous access
path: '',
component: HomeComponent
}
/* Changes end here. */
];
@NgModule({
/* Changes start here. */
// Replace the following line with the next one
//imports: [RouterModule.forRoot(routes)],
imports: [RouterModule.forRoot(routes, {
initialNavigation:'enabled'
})],
/* Changes end here. */
exports: [RouterModule]
})
export class AppRoutingModule { }
新增登入和註銷按鈕
在本節中,您會將登入和註銷按鈕新增至 應用程式 元件。 在 src/app 資料夾中,開啟 app.component.ts 檔案,並進行下列變更:
匯入必要的元件。
變更 類別以實作 OnInit 方法。
OnInit方法會訂閱 MSAL MsalBroadcastServiceinProgress$可觀察事件。 使用此事件來知道用戶互動的狀態,特別是檢查是否已完成互動。在與 MSAL 帳戶物件互動之前,請先檢查
InteractionStatus屬性是否傳回InteractionStatus.None。 事件subscribe會呼叫setLoginDisplay方法來檢查使用者是否已通過驗證。新增類別變數。
新增啟動
login授權流程的方法。新增可登出使用者的
logout方法。setLoginDisplay新增方法,以檢查使用者是否已通過驗證。新增 ngOnDestroy 方法來清除
inProgress$訂閱事件。
變更之後,您的程式代碼看起來應該像下列代碼段:
import { Component, OnInit, 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']
})
/* Changes start here. */
export class AppComponent implements OnInit{
title = 'msal-angular-tutorial';
loginDisplay = false;
private readonly _destroying$ = new Subject<void>();
constructor(@Inject(MSAL_GUARD_CONFIG) private msalGuardConfig: MsalGuardConfiguration, private broadcastService: MsalBroadcastService, private authService: MsalService) { }
ngOnInit() {
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() {
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();
}
/* Changes end here. */
}
在 src/app 資料夾中,編輯 app.component.html 並進行下列變更:
- 新增設定檔和 Web API 元件的連結。
- 新增 [登入] 按鈕,並將 click 事件屬性設置為
login()方法。 只有當loginDisplay類別變數是false時,才會顯示此按鈕。 - 新增 [註銷] 按鈕,並將 click 事件屬性設定為
logout()方法。 只有當loginDisplay類別變數是true時,才會顯示此按鈕。 - 新增 router-outlet 元素。
變更之後,您的程式代碼看起來應該像下列代碼段:
<mat-toolbar color="primary">
<a class="title" href="/">{{ title }}</a>
<div class="toolbar-spacer"></div>
<a mat-button [routerLink]="['profile']">Profile</a>
<a mat-button [routerLink]="['webapi']">Web API</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">
<router-outlet></router-outlet>
</div>
或者,使用下列 CSS 代碼段更新 app.component.css 檔案:
.toolbar-spacer {
flex: 1 1 auto;
}
a.title {
color: white;
}
處理應用程式重新導向
當您使用 MSAL 進行重新導向時,必須將 應用程式重新導向 指示詞新增至 index.html。 在 src 資料夾中,編輯 index.html ,如下列代碼段所示:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MsalAngularTutorial</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>
<!-- Changes start here -->
<app-redirect></app-redirect>
<!-- Changes end here -->
</body>
</html>
設定應用程式 CSS (選擇性 )
在 /src 資料夾中,使用下列 CSS 代碼段更新 styles.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%; }
小提示
此時,您可以執行應用程式並測試登入體驗。 若要執行您的應用程式,請參閱 執行 Angular 應用程式 一節。
檢查使用者是否已通過驗證
home.component 檔案示範如何檢查使用者是否已通過驗證。 在 src/app/home 資料夾中,使用下列代碼段更新 home.component.ts 。
程式碼:
- 訂閱 MSAL MsalBroadcastService
msalSubject$和inProgress$可觀察事件。 - 確保
msalSubject$事件會將驗證結果寫入瀏覽器控制台。 - 確保
inProgress$事件會檢查使用者是否已通過驗證。 方法getAllAccounts()會傳回一或多個物件。
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;
}
}
在 src/app/home 資料夾中,使用下列 HTML 代碼段更新 home.component.html 。 *ngIf 指令會檢查loginDisplay類別變數,以顯示或隱藏歡迎訊息。
<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>
讀取識別碼權杖宣告
profile.component 檔案示範如何存取使用者的 ID 權杖聲明。 在 src/app/profile 資料夾中,使用下列代碼段更新 profile.component.ts 。
程式碼:
- 匯入必要的元件。
- 訂閱 MSAL MsalBroadcastService
inProgress$可觀察事件。 此事件會載入帳戶,並讀取識別碼權杖宣告。 - 確保
checkAndSetActiveAccount方法會檢查並設定使用中帳戶。 當應用程式與多個 Azure AD B2C 使用者流程或自定義原則互動時,此動作很常見。 - 確保
getClaims方法會從使用中 MSAL account 物件取得識別碼權杖宣告。 方法接著會將宣告新增至dataSource陣列。 陣列會透過元件的範本繫結呈現給使用者。
import { Component, OnInit } from '@angular/core';
import { MsalBroadcastService, MsalService } from '@azure/msal-angular';
import { EventMessage, EventType, InteractionStatus } from '@azure/msal-browser';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
displayedColumns: string[] = ['claim', 'value'];
dataSource: Claim[] = [];
private readonly _destroying$ = new Subject<void>();
constructor(private authService: MsalService, private msalBroadcastService: MsalBroadcastService) { }
ngOnInit(): void {
this.msalBroadcastService.inProgress$
.pipe(
filter((status: InteractionStatus) => status === InteractionStatus.None || status === InteractionStatus.HandleRedirect),
takeUntil(this._destroying$)
)
.subscribe(() => {
this.checkAndSetActiveAccount();
this.getClaims(this.authService.instance.getActiveAccount()?.idTokenClaims)
})
}
checkAndSetActiveAccount() {
let activeAccount = this.authService.instance.getActiveAccount();
if (!activeAccount && this.authService.instance.getAllAccounts().length > 0) {
let accounts = this.authService.instance.getAllAccounts();
this.authService.instance.setActiveAccount(accounts[0]);
}
}
getClaims(claims: any) {
let list: Claim[] = new Array<Claim>();
Object.keys(claims).forEach(function(k, v){
let c = new Claim()
c.id = v;
c.claim = k;
c.value = claims ? claims[k]: null;
list.push(c);
});
this.dataSource = list;
}
ngOnDestroy(): void {
this._destroying$.next(undefined);
this._destroying$.complete();
}
}
export class Claim {
id: number;
claim: string;
value: string;
}
在 src/app/profile 資料夾中,使用下列 HTML 代碼段更新 profile.component.html :
<h1>ID token claims:</h1>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<!-- Claim Column -->
<ng-container matColumnDef="claim">
<th mat-header-cell *matHeaderCellDef> Claim </th>
<td mat-cell *matCellDef="let element"> {{element.claim}} </td>
</ng-container>
<!-- Value Column -->
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef> Value </th>
<td mat-cell *matCellDef="let element"> {{element.value}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
呼叫 Web API
若要呼叫 令牌型授權 Web API,應用程式必須具有有效的存取令牌。 MsalInterceptor 提供者會自動取得傳出要求的權杖,而此權杖使用已知受保護資源的 Angular HttpClient 類別。
這很重要
MSAL 初始化方法 (在 app.module.ts 類別中) 會使用 protectedResourceMap 物件,將受保護的資源,例如 Web API 對應到所需的應用程式範圍。 如果您的程式代碼需要呼叫另一個 Web API,請將具有對應範圍的 Web API URI 和 Web API HTTP 方法新增至 protectedResourceMap 物件。 如需詳細資訊,請參閱 受保護資源地圖。
當 HttpClient 物件呼叫 Web API 時, MsalInterceptor 提供者會採取下列步驟:
取得具有 Web API 端點所需許可權(範圍)的存取令牌。
使用下列格式,將存取令牌作為持有者的憑證傳遞到 HTTP 要求的授權標頭中:
Authorization: Bearer <access-token>
webapi.component 檔案示範如何呼叫Web API。 在 src/app/webapi 資料夾中,使用下列代碼段更新 webapi.component.ts 。
程式碼:
- 使用 Angular HttpClient 類別來呼叫 Web API。
- 讀取
auth-config類別的protectedResources.todoListApi.endpoint元素。 這個元素指定了 Web API URI。 MSAL 攔截器會根據 Web API URI 來取得具有對應範圍的存取權杖。 - 從 Web API 取得設定檔,並設定
profile類別變數。
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { protectedResources } from '../auth-config';
type ProfileType = {
name?: string
};
@Component({
selector: 'app-webapi',
templateUrl: './webapi.component.html',
styleUrls: ['./webapi.component.css']
})
export class WebapiComponent implements OnInit {
todoListEndpoint: string = protectedResources.todoListApi.endpoint;
profile!: ProfileType;
constructor(
private http: HttpClient
) { }
ngOnInit() {
this.getProfile();
}
getProfile() {
this.http.get(this.todoListEndpoint)
.subscribe(profile => {
this.profile = profile;
});
}
}
在 src/app/webapi 資料夾中,使用下列 HTML 代碼段更新 webapi.component.html 。 元件的範本會轉譯 Web API 傳回的名稱。 在頁面底部,範本會轉譯 Web API 位址。
<h1>The web API returns:</h1>
<div>
<p><strong>Name: </strong> {{profile?.name}}</p>
</div>
<div class="footer-text">
Web API: {{todoListEndpoint}}
</div>
或者,使用下列 CSS 代碼段更新 webapi.component.css 檔案:
.footer-text {
position: absolute;
bottom: 50px;
color: gray;
}
執行 Angular 應用程式
執行下列命令:
npm start
主控台視窗會顯示裝載應用程式的埠數目。
Listening on port 4200...
小提示
或者,若要執行 npm start 命令,請使用 Visual Studio Code 調試程式。 調試程式可協助您加速編輯、編譯和偵錯迴圈。
在瀏覽器中移至 http://localhost:4200 以檢視應用程式。