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的實現

我們搭建一個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跑起來,得到一個具有標題欄的空頁面:

image

接下來,使用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中,加入:

<router-outlet></router-outlet>

再次運行站點,可以看到,我們已經可以通過菜單來切換component了:

angular-router

在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的數據:

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);
  }
}


並顯示在前端:

<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這個頁面顯示調用結果了:

identity-demo-angular-spa

開啟身份認證

在Ocelot API網關的配置中,打開被注釋掉的部分,重新啟用身份認證功能,再次刷新Angular頁面,發現頁面已經打不開了,在開發者工具的Console中輸出了錯誤信息:401 (Unauthorized),表示身份認證部分已經起作用了。

image

下面我們來解決這個問題。既然是需要身份認證才能訪問Weather API,那么我們就在Angular頁面上實現登錄功能。首先在Angular單頁面應用中安裝oidc-client,oidc-client是一款為Javascript應用程序提供OpenID Connect和OAuth2協議支持的框架,在Angular中使用也非常的方便。用npm install來安裝這個庫:

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應用,你會看到以下效果:

identity-demo-login

現在我們就可以在Angular的頁面中完成用戶登錄和注銷了。如你所見:

  1. 登錄界面來自Identity Service,本身也是由IdentityServer4提供的界面,開發者可以自己修改Identity Service來定制界面
  2. 登錄成功后,原本的“登錄”按鈕變成了顯示用戶名稱的下拉菜單,選擇菜單就可以點擊“注銷”按鈕退出登錄
  3. 此時訪問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。

image

小結

本文詳細介紹了Angular單頁面應用作為Ocelot API網關的客戶端,通過Identity Service進行身份認證和API調用的整個過程。當然,很多細節部分沒有做到那么完美,本身也是為了能夠演示開發過程中遇到的問題。從下一講開始,我會開始介紹基於Ocelot API網關的授權問題。

源代碼

訪問以下Github地址以獲取源代碼:

https://github.com/daxnet/identity-demo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM