使用 Azure Active Directory B2C 在自己的 Angular 应用程序中启用身份验证

重要

自 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 应用项目或创建新的 Angular 应用项目。 若要创建新项目,请运行以下命令。

命令:

  1. 使用 npm 包管理器安装 Angular CLI
  2. 使用路由模块创建 Angular 工作区。 应用名称为 msal-angular-tutorial. 可以将它更改为任何有效的 Angular 应用名称,例如 contoso-car-service
  3. 切换到应用目录文件夹。
npm install -g @angular/cli 
ng new msal-angular-tutorial --routing=true --style=css --strict=false
cd msal-angular-tutorial

安装依赖项

若要在应用程序中安装 MSAL 浏览器MSAL Angular 库,请在命令行界面中运行以下命令:

npm install @azure/msal-browser @azure/msal-angular

安装 Angular Material 组件库 (可选,适用于 UI):

npm install @angular/material @angular/cdk

添加身份验证组件

示例代码包含以下组件:

组件 类型 DESCRIPTION
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 组件 在本演练中,将添加 组件以呈现主页以供匿名访问。 此组件演示如何检查用户是否已登录。
profile.component.* Angular 组件 在本演练中,你将添加 profile 组件以了解如何读取 ID 令牌声明
webapi.component.* Angular 组件 在本演练中,将添加 webapi 组件以了解如何调用 Web API。

若要将以下组件添加到应用,请运行以下 Angular CLI 命令。 命令 generate component

  1. 为每个组件创建一个文件夹。 该文件夹包含 TypeScript、HTML、CSS 和测试文件。
  2. 更新 app.module.tsapp-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 并进行以下更改:

  1. 导入 MSAL Angular 和 MSAL 浏览器库。
  2. 导入 Azure AD B2C 配置模块。
  3. 导入 HttpClientModule。 HTTP 客户端用于调用 Web API。
  4. 导入 Angular HTTP 侦听器。 MSAL 使用侦听器将持有者令牌注入 HTTP 授权标头。
  5. 添加必要的 Angular 材料。
  6. 使用多帐户公共客户端应用对象实例化 MSAL。 MSAL 初始化包括传递以下对象:
    1. auth-config.ts的配置对象。
    2. 路由防护的配置对象。
    3. MSAL 侦听器的配置对象。 interceptor 类会自动为向已知受保护资源发出的、使用 Angular HttpClient 类的传出请求获取令牌。
  7. 配置 HTTP_INTERCEPTORSMsalGuard Angular 提供程序
  8. 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 文件并进行以下更改:

  1. 导入所需的组件。

  2. 更改类以实现 OnInit 方法OnInit 方法订阅了 MSAL MsalBroadcastServiceinProgress$ 可观测事件。 使用此事件了解用户交互的状态,特别是检查交互是否已完成。

    在与 MSAL 帐户对象交互之前,请检查 InteractionStatus 属性是否返回 InteractionStatus.None。 该 subscribe 事件调用 setLoginDisplay 该方法以检查用户是否已进行身份验证。

  3. 添加类变量。

  4. 添加login方法以启动授权流。

  5. 添加用于注销用户的logout方法。

  6. setLoginDisplay添加用于检查用户是否已进行身份验证的方法。

  7. 添加 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 并进行以下更改:

  1. 添加指向个人资料和 Web API 组件的链接。
  2. 添加登录按钮,并将单击事件属性设置为 login() 方法。 仅当 loginDisplay 类变量为 false 时才显示此按钮。
  3. 添加注销按钮,并将单击事件属性设置为 logout() 方法。 仅当类变量loginDisplaytrue时,才会显示此按钮。
  4. 添加 路由器出口 元素。

更改后,代码应类似于以下代码片段:

<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 一起使用时,必须将 app-redirect 指令添加到 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

代码:

  1. 订阅 MSAL MsalBroadcastServicemsalSubject$inProgress$ 可观测事件。
  2. 确保 msalSubject$ 事件将身份验证结果写入浏览器控制台。
  3. 确保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>

读取 ID 令牌声明

profile.component 文件演示如何访问用户的 ID 令牌声明。 在 src/app/profile 文件夹中,使用以下代码片段更新 profile.component.ts

代码:

  1. 导入所需的组件。
  2. 订阅 MSAL MsalBroadcastServiceinProgress$ 可观测事件。 该事件加载帐户并读取 ID 令牌声明。
  3. 确保 checkAndSetActiveAccount 方法检查并设置活动帐户。 当应用与多个 Azure AD B2C 用户流或自定义策略交互时,此作很常见。
  4. 确保 getClaims 方法从活动的 MSAL 帐户对象获取 ID 令牌声明。 然后,该方法将声明添加到 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 提供程序将执行以下步骤:

  1. 获取对 Web API 终结点拥有所需权限(范围)的访问令牌。

  2. 使用以下格式,在 HTTP 请求的授权标头中将该访问令牌作为持有者令牌进行传递:

    Authorization: Bearer <access-token>
    

webapi.component 文件演示如何调用 Web API。 在 src/app/webapi 文件夹中,使用以下代码片段更新 webapi.component.ts

代码:

  1. 使用 Angular HttpClient 类调用 Web API。
  2. 读取auth-config类的protectedResources.todoListApi.endpoint元素。 此元素指定 Web API URI。 MSAL 拦截器基于 Web API URI 获取具有相应范围的访问令牌。
  3. 从 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,查看此应用程序。

后续步骤