Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(三)
在前面兩篇文章中,我介紹了基于IdentityServer4的一個Identity Service的實現,并且實現了一個Weather API和基于Ocelot的API網關,然后實現了通過Ocelot API網關整合Identity Service做身份認證的API請求。今天,我們進入前端開發,設計一個簡單的Angular SPA,并在Angular SPA上調用受Ocelot API網關和Identity Service保護的Weather API。
回顧
《Angular SPA基于Ocelot API網關與IdentityServer4的身份認證與授權(一)》
《Angular SPA基于Ocelot API網關與IdentityServer4的身份認證與授權(二)》
Angular SPA的實現
我們搭建一個Angular SPA的應用程序,第一步先實現一些基礎功能,比如頁面布局和客戶端路由;第二步先將Ocelot API網關中設置的身份認證功能關閉,并設計一個Component,在Component中調用未受保護的Weather API,此時可以毫無阻攔地在Angular SPA中調用Weather API并將結果顯示在頁面上;第三步,我們在Ocelot API網關上開啟身份認證,然后修改Angular SPA,使其提供登錄按鈕以實現用戶登錄與身份認證,進而訪問受保護的Weather API。在進行接下來的實操演練之前,請確保已經安裝Angular 8 CLI。
基礎功能的實現
在文件系統中,使用ng new命令,新建一個Angular 8的單頁面應用,為了有比較好的界面布局,我使用了Bootstrap。方法很簡單,在項目目錄下,執行npm install –save bootstrap,然后,打開angular.json文件,將bootstrap的js和css添加到配置中:
"styles": [ ????"src/styles.css", ????"node_modules/bootstrap/dist/css/bootstrap.min.css" ], "scripts": [ ????"node_modules/bootstrap/dist/js/bootstrap.min.js" ] |
然后,修改app.component.html,使用下面代碼覆蓋:
<nav class="navbar navbar-expand-md navbar-dark bg-dark"> ??<a class="navbar-brand" href="#">Identity Demo</a> ??<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> ????<span class="navbar-toggler-icon"></span> ??</button> ? ??<div class="collapse navbar-collapse" id="navbarSupportedContent"> ????<ul class="navbar-nav mr-auto"> ??????<li class="nav-item active"> ????????<a class="nav-link" href="#">首頁 <span class="sr-only">(current)</span></a> ??????</li> ??????<li class="nav-item"> ????????<a class="nav-link" href="#">API</a> ??????</li> ??????<li class="nav-item"> ????????<a class="nav-link" href="#">關于</a> ??????</li> ??????? ????</ul> ????<form class="form-inline my-2 my-md-0"> ??????<ul class="navbar-nav mr-auto"> ????????<a class="nav-link" href="javascript:void(0)">登錄</a> ??????</ul> ????</form> ??</div> </nav> |
ng serve跑起來,得到一個具有標題欄的空頁面:
接下來,使用ng g c命令創建3個component,分別是HomeComponent,ApiComponent和AboutComponent,并且修改app.modules.ts文件,將這三個components加入到router中:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; ? import { AppComponent } from './app.component'; import { HomeComponent } from './home/home.component'; import { ApiComponent } from './api/api.component'; import { AboutComponent } from './about/about.component'; ? const appRoutes: Routes = [ ??{ path: 'about', component: AboutComponent }, ??{ path: 'home', component: HomeComponent }, ??{ path: 'api', component: ApiComponent }, ??{ path: '**', component: HomeComponent } ]; ? @NgModule({ ??declarations: [ ????AppComponent, ????HomeComponent, ????ApiComponent, ????AboutComponent ??], ??imports: [ ????BrowserModule, ????RouterModule.forRoot( ??????appRoutes, ??????{ enableTracing: false } ????) ??], ??providers: [], ??bootstrap: [AppComponent] }) export class AppModule { } |
然后,在app.component.html中,加入:
1 | <router-outlet></router-outlet> |
再次運行站點,可以看到,我們已經可以通過菜單來切換component了:
在Angular頁面中調用API顯示結果
Angular調用API的方法我就不詳細介紹了,Angular的官方文檔有很詳細的內容可以參考。在這個演練中,我們需要注意的是,首先將上篇文章中對于Weather API的認證功能關閉,以便測試API的調用是否成功。關閉認證功能其實很簡單,只需要將Ocelot API網關中有關Ocelot的配置的相關節點注釋掉就行了:
{ ??"ReRoutes": [ ????{ ??????"DownstreamPathTemplate": "/weatherforecast", ??????"DownstreamScheme": "http", ??????"DownstreamHostAndPorts": [ ????????{ ??????????"Host": "localhost", ??????????"Port": 5000 ????????} ??????], ??????"UpstreamPathTemplate": "/api/weather", ??????"UpstreamHttpMethod": [ "Get" ], ??????//"AuthenticationOptions": { ??????//? "AuthenticationProviderKey": "AuthKey", ??????//? "AllowedScopes": [] ??????//} ????} ??] } |
接下來修改Angular單頁面應用,在app.module.ts中加入HttpClientModule:
imports: [ ????BrowserModule, ????HttpClientModule, ????RouterModule.forRoot( ??????appRoutes, ??????{ enableTracing: false } ????) ??], |
然后實現一個調用Weather API的Service(服務):
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { WeatherData } from '../models/weather-data'; import { Observable } from 'rxjs'; ? @Injectable({ ??providedIn: 'root' }) export class WeatherService { ? ??constructor(private httpClient: HttpClient) { } ? ??getWeather(): Observable<WeatherData[]> { ????return this.httpClient.get<WeatherData[]>('http://localhost:9000/api/weather'); ??} } |
在這個Service實現中,沒有加入異常處理部分,因為作為一個研究性質的項目,沒有必要進行異常處理,到瀏覽器的調試窗口查看錯誤信息就行。上面的代碼引用了一個類型,就是WeatherData,它其實非常簡單,對應著Weather API所返回的數據模型:
export class WeatherData { ????constructor(public temperatureF: number, ????????public temperatureC: number, ????????private summary: string, ????????private date: string) { } } |
現在,修改api.component.ts,通過調用這個WeatherService來獲取Weather API的數據:
1 | import { Component, OnInit } from '@angular/core'; import { WeatherService } from '../services/weather.service'; import { WeatherData } from '../models/weather-data'; ? @Component({ ??selector: 'app-api', ??templateUrl: './api.component.html', ??styleUrls: ['./api.component.css'] }) export class ApiComponent implements OnInit { ? ??data: WeatherData[]; ? ??constructor(private api: WeatherService) { } ? ??ngOnInit() { ????this.api.getWeather() ??????.subscribe(ret => this.data = ret); ??} } |
并顯示在前端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <div class="container" *ngIf="data"> ????<table class="table table-striped"> ????????<thead> ??????????<tr> ????????????<th scope="col">Summary</th> ????????????<th scope="col">TempF</th> ????????????<th scope="col">TempC</th> ????????????<th scope="col">Date</th> ??????????</tr> ????????</thead> ????????<tbody> ??????????<tr *ngFor="let d of data"> ????????????<td>{{d.summary}}</td> ????????????<td>{{d.temperatureF}}</td> ????????????<td>{{d.temperatureC}}</td> ????????????<td>{{d.date}}</td> ??????????</tr> ????????</tbody> ??????</table> </div> |
完成之后,啟動Weather API和Ocelot API網關,然后運行Angular單頁面應用,我們已經可以在API這個頁面顯示調用結果了:
開啟身份認證
在Ocelot API網關的配置中,打開被注釋掉的部分,重新啟用身份認證功能,再次刷新Angular頁面,發現頁面已經打不開了,在開發者工具的Console中輸出了錯誤信息:401 (Unauthorized),表示身份認證部分已經起作用了。
下面我們來解決這個問題。既然是需要身份認證才能訪問Weather API,那么我們就在Angular頁面上實現登錄功能。首先在Angular單頁面應用中安裝oidc-client,oidc-client是一款為Javascript應用程序提供OpenID Connect和OAuth2協議支持的框架,在Angular中使用也非常的方便。用npm install來安裝這個庫:
1 | npm install oidc-client |
然后,實現一個用于身份認證的Service:
import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { UserManager, UserManagerSettings, User } from 'oidc-client'; ? @Injectable({ ??providedIn: 'root' }) export class AuthService { ? ??private authStatusSource = new BehaviorSubject<boolean>(false); ??private userNameStatusSource = new BehaviorSubject<string>(''); ??private userManager = new UserManager(this.getUserManagerSettings()); ??private user: User | null; ? ??authStatus$ = this.authStatusSource.asObservable(); ??userNameStatus$ = this.userNameStatusSource.asObservable(); ? ??constructor() { ????this.userManager.getUser().then(user => { ??????this.user = user; ??????this.authStatusSource.next(this.isAuthenticated()); ??????this.userNameStatusSource.next(this.user.profile.name); ????}); ??} ? ??async login() { ????await this.userManager.signinRedirect(); ??} ? ??async logout() { ????await this.userManager.signoutRedirect(); ??} ? ??async completeAuthentication() { ????this.user = await this.userManager.signinRedirectCallback(); ????this.authStatusSource.next(this.isAuthenticated()); ????this.userNameStatusSource.next(this.user.profile.name); ??} ? ??isAuthenticated(): boolean { ????return this.user != null && !this.user.expired; ??} ? ??get authorizationHeaderValue(): string { ????return `${this.user.token_type} ${this.user.access_token}`; ??} ? ??private getUserManagerSettings(): UserManagerSettings { ????return { ??????authority: 'http://localhost:7889', ??????client_id: 'angular', ??????redirect_uri: 'http://localhost:4200/auth-callback', ??????post_logout_redirect_uri: 'http://localhost:4200/', ??????response_type: 'id_token token', ??????scope: 'openid profile email api.weather.full_access', ??????filterProtocolClaims: true, ??????loadUserInfo: true, ??????automaticSilentRenew: true, ??????silent_redirect_uri: 'http://localhost:4200/silent-refresh.html' ????}; ??} } |
AuthService為Angular應用程序提供了用戶身份認證的基本功能,比如登錄、注銷,以及判斷是否經過身份認證(isAuthenticated)等。需要注意的是getUserManagerSettings方法,它為oidc-client提供了基本的參數配置,其中的authority為Identity Service的URL;redirect_uri為認證完成后,Identity Service需要返回到哪個頁面上;post_logout_redirect_uri表示用戶注銷以后,需要返回到哪個頁面上;client_id和scope為Identity Service中為Angular應用所配置的Client的ClientId和Scope(參考Identity Service中的Config.cs文件)。
接下來,修改app.component.html,將原來的“登錄”按鈕改為:
<form class="form-inline my-2 my-md-0"> ????<ul class="navbar-nav mr-auto"> ????<a *ngIf="!isAuthenticated" class="nav-link" href="javascript:void(0)" (click)="onLogin()">登錄</a> ????<li *ngIf="isAuthenticated" class="nav-item dropdown"> ??????<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" ????????aria-haspopup="true" aria-expanded="false"> ????????{{userName}} ??????</a> ??????<div class="dropdown-menu" aria-labelledby="navbarDropdown"> ????????<a class="dropdown-item" href="javascript:void(0)" (click)="onLogOut()">注銷</a> ??????</div> ????</li> ????</ul> </form> |
然后,修改app.component.ts,完成登錄和注銷部分的代碼:
import { Component, OnInit, OnDestroy } from '@angular/core'; import { AuthService } from './services/auth.service'; import { Subscription } from 'rxjs'; ? @Component({ ??selector: 'app-root', ??templateUrl: './app.component.html', ??styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit, OnDestroy { ??title = 'identity-demo-spa'; ? ??isAuthenticated: boolean; ??authStatusSubscription: Subscription; ??userNameSubscription: Subscription; ??userName: string; ??? ??constructor(private authService: AuthService) { } ? ??ngOnDestroy(): void { ????this.authStatusSubscription.unsubscribe(); ????this.userNameSubscription.unsubscribe(); ??} ? ??ngOnInit(): void { ????this.authStatusSubscription = this.authService.authStatus$.subscribe(status => this.isAuthenticated = status); ????this.userNameSubscription = this.authService.userNameStatus$.subscribe(status => this.userName = status); ??} ? ??async onLogin() { ????await this.authService.login(); ??} ? ??async onLogOut() { ????await this.authService.logout(); ??} } |
我們還需要增加一個新的component:AuthCallbackComponent,用來接收登錄成功之后的回調,它會通知AuthService以更新登錄狀態和用戶信息:
import { Component, OnInit } from '@angular/core'; import { AuthService } from '../services/auth.service'; import { Router, ActivatedRoute } from '@angular/router'; ? @Component({ ??selector: 'app-auth-callback', ??templateUrl: './auth-callback.component.html', ??styleUrls: ['./auth-callback.component.css'] }) export class AuthCallbackComponent implements OnInit { ? ??constructor(private authService: AuthService, private router: Router, private route: ActivatedRoute) { } ? ??async ngOnInit() { ????await this.authService.completeAuthentication(); ????this.router.navigate(['/home']); ??} ? } |
最后將AuthCallbackComponent添加到Route中:
const appRoutes: Routes = [ ??{ path: 'about', component: AboutComponent }, ??{ path: 'home', component: HomeComponent }, ??{ path: 'api', component: ApiComponent }, ??{ path: 'auth-callback', component: AuthCallbackComponent }, ??{ path: '**', component: HomeComponent } ]; |
重新運行Angular應用,你會看到以下效果:
現在我們就可以在Angular的頁面中完成用戶登錄和注銷了。如你所見:
登錄界面來自Identity Service,本身也是由IdentityServer4提供的界面,開發者可以自己修改Identity Service來定制界面
登錄成功后,原本的“登錄”按鈕變成了顯示用戶名稱的下拉菜單,選擇菜單就可以點擊“注銷”按鈕退出登錄
此時訪問API頁面,仍然無法正確調用Weather API,因為我們還沒有將Access Token傳入API調用
登錄狀態下的API調用
接下來,我們將Access Token傳入,使得Angular應用可以使用登錄用戶獲取的Access Token正確調用Weather API。修改AuthService如下:
export class WeatherService { ? ??constructor(private httpClient: HttpClient, private authService: AuthService) { } ? ??getWeather(): Observable<WeatherData[]> { ????const authHeaderValue = this.authService.authorizationHeaderValue; ????const httpOptions = { ??????headers: new HttpHeaders({ ????????'Content-Type': 'application/json', ????????Authorization: authHeaderValue ??????}) ????}; ? ????return this.httpClient.get<WeatherData[]>('http://localhost:9000/api/weather', httpOptions); ??} } |
再次運行Angular應用,可以看到,已經可以在登錄的狀態下成功調用Weather API。你也可以試試,在退出登錄的狀態下,是否還能正確調用API。
小結
本文詳細介紹了Angular單頁面應用作為Ocelot API網關的客戶端,通過Identity Service進行身份認證和API調用的整個過程。當然,很多細節部分沒有做到那么完美,本身也是為了能夠演示開發過程中遇到的問題。從下一講開始,我會開始介紹基于Ocelot API網關的授權問題。
源代碼
訪問以下Github地址以獲取源代碼:
https://github.com/daxnet/identity-demo
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(三)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 聊聊面试的事(应聘方)
- 下一篇: 基于 Roslyn 实现一个简单的条件解