教程:使用授权代码流从 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 在浏览器中使用带有 PKCE 的授权代码流,改进了使用隐式授权流的 MSAL Angular v1。 建议将采用 PKCE 的授权代码流用于单页应用程序 (SPA),因为它比隐式流更安全。 MSAL Angular v2 不支持隐式流。
先决条件
- 用于运行本地 Web 服务器的 Node.js。
- 用于修改项目文件的 Visual Studio Code 或其他编辑器。
示例应用工作原理
本教程中创建的示例应用程序使 Angular SPA 能够查询 Microsoft Graph API 或 Web API,该 API 接受 Microsoft 标识平台颁发的令牌。 它使用适用于 Angular 的 Microsoft 身份验证库 (MSAL) v2 - MSAL.js v2 库的包装器。 MSAL Angular 可以让 Angular 9+ 应用程序使用 Microsoft Entra ID 对企业用户进行身份验证。 使用此库,应用程序还可以获取对 Azure 云服务和 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。 注册后,应用程序“概述”窗格将显示应用程序源代码中所需的标识符。
- 至少以应用程序开发人员的身份登录到 Microsoft Entra 管理中心。
- 如果你有权访问多个租户,请使用顶部菜单中的“设置”图标 ,通过“目录 + 订阅”菜单切换到你希望在其中注册应用程序的租户。
- 浏览到“标识”>“应用程序”>“应用注册”。
- 选择“新注册”。
- 输入应用程序的名称,例如 Angular-SPA-auth-code。
- 对于“支持的帐户类型”设置,请选择“仅限此组织目录中的帐户”。 要了解不同帐户类型的信息,请选择“帮我选择”选项。
- 在“重定向 URI (可选)”下,使用下拉菜单选择“单页应用程序(SPA)”,然后在文本框中输入
http://localhost:4200
。 - 选择“注册”。
- 注册完成后,将显示应用程序的“概述”窗格。 记录要在应用程序源代码中使用的目录(租户)ID 和应用程序(客户端)ID。
创建项目
打开 Visual Studio Code。
选择“文件>“打开文件夹...”。导航到要在其中创建项目的位置并选中该位置。
通过选择“终端”>“新终端”打开一个新的终端。
- 可能需要切换终端类型。 选择终端中 + 图标旁边的向下箭头,然后选择“命令提示符”。
运行以下命令以创建名为
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
打开 src/app/app.module.ts。
MsalModule
和MsalInterceptor
需要与常数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 {}
将以下值替换为从 Microsoft Entra 管理中心获取的值。 有关可用的可配置选项的详细信息,请阅读初始化客户端应用程序。
clientId
- 应用程序的标识符,也称为客户端。 将Enter_the_Application_Id_Here
替换为注册应用程序的概述页中先前记录的应用程序(客户端) ID 值。authority
- 其由两个部分组成:- Instance 是云提供程序的终结点。 在“国家云”查看可用的不同终结点。
- 租户 ID 是在其中注册应用程序的租户的标识符。 将
_Enter_the_Tenant_Info_Here
替换为注册应用程序的概述页中先前记录的目录(租户)ID 值。
redirectUri
–在为应用成功授权并为其授予授权代码或访问令牌后,授权服务器将用户发送到的位置。 将Enter_the_Redirect_Uri_Here
替换为http://localhost:4200
。
打开 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 {}
打开 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>
打开 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%; }
打开 src/app/app.component.css,向应用程序添加 CSS 样式:
.toolbar-spacer { flex: 1 1 auto; } a.title { color: white; }
使用弹出窗口登录
打开 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; } }
使用重定向登录
更新 src/app/app.module.ts 以启动
MsalRedirectComponent
。 这是一个用于处理重定向的专用重定向组件。 更改MsalModule
导入和AppComponent
启动,使其类似于以下代码片段:... import { MsalModule, MsalRedirectComponent } from '@azure/msal-angular'; // Updated import ... bootstrap: [AppComponent, MsalRedirectComponent] // MsalRedirectComponent bootstrapped here ...
打开 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>
打开 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; } }
导航到 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 以检查用户是否已登录以及交互是否已完成。
将
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(); } }
更新 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; } }
将 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
是一个可用于改善用户体验的便捷类,但出于安全考虑,不应依赖于此类。 攻击者可能会绕过客户端保护,你应该确保服务器不会返回用户不应访问的任何数据。
在 src/app/app.module.ts 中添加
MsalGuard
类作为应用程序中的提供程序,并添加MsalGuard
的配置。 可以在authRequest
中提供稍后用于获取令牌的范围,可将保护功能的交互类型设置为Redirect
或Popup
。 代码应类似于以下代码片段: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: ["https://microsoftgraph.chinacloudapi.cn/user.read"], }, }, null ), ], providers: [ MsalGuard, // MsalGuard added as provider here ], bootstrap: [AppComponent, MsalRedirectComponent], }) export class AppModule {}
在 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 {}
调整 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
客户端的传出请求获取令牌。
在 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: ["https://microsoftgraph.chinacloudapi.cn/user.read"], }, }, { interactionType: InteractionType.Redirect, // MSAL Interceptor Configuration protectedResourceMap: new Map([ ["Enter_the_Graph_Endpoint_Here/v1.0/me", ["https://microsoftgraph.chinacloudapi.cn/user.read"]], ]), } ), ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true, }, MsalGuard, ], bootstrap: [AppComponent, MsalRedirectComponent], }) export class AppModule {}
受保护的资源是作为
protectedResourceMap
提供的。 在protectedResourceMap
集合中输入的 URL 区分大小写。 对于每个资源,添加所请求的、需要在访问令牌中返回的范围。例如:
- Microsoft Graph 的
["https://microsoftgraph.chinacloudapi.cn/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://microsoftgraph.chinacloudapi.cn
。 对于国家/地区云部署中的终结点,请参阅 Microsoft Graph 文档中的国家/地区云部署。
- Microsoft Graph 的
替换 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; }); } }
替换 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>
注销
更新 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>
使用重定向注销
更新 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(); } }
使用弹出窗口注销
更新 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(); } }
测试代码
通过在命令行提示符下从应用程序文件夹运行以下命令,启动 Web 服务器来侦听端口:
npm install npm start
在浏览器中,输入
http://localhost:4200
,应会看到如下所示的页面。选择“接受”,向应用授予对配置文件的权限。 这发生在首次开始登录时。
如果你同意请求的权限,Web 应用程序将显示成功登录页面。
选择“配置文件”,以查看在调用 Microsoft Graph API 后提供的响应中返回的用户配置文件信息:
添加范围和委托的权限
Microsoft Graph API 需要 User.Read 作用域来读取用户的配置文件。 User.Read 范围会自动添加到每个应用注册。 Microsoft Graph 的其他 API 以及后端服务器的自定义 API 可能需要其他作用域。 例如,Microsoft Graph API 需要使用 Mail.Read 作用域来列出用户的电子邮件。
添加作用域时,可能会提示用户为添加的作用域提供额外许可。
注意
当你增加作用域数量时,可能会提示用户另外进行许可。
帮助和支持
如果需要帮助、需要报告问题,或者需要详细了解支持选项,请参阅面向开发人员的帮助和支持。
后续步骤
- 以在以下多部分系列教程中,通过生成能登录用户的 React 单页应用程序 (SPA) 了解详细信息。