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的页面中完成用户登录和注销了。如你所见:

  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。

小结

本文详细介绍了Angular单页面应用作为Ocelot API网关的客户端,通过Identity Service进行身份认证和API调用的整个过程。当然,很多细节部分没有做到那么完美,本身也是为了能够演示开发过程中遇到的问题。从下一讲开始,我会开始介绍基于Ocelot API网关的授权问题。

源代码

访问以下Github地址以获取源代码:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/311021.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[蓝桥杯][算法提高VIP]因式分解

解题思路&#xff1a; 水题&#xff01;&#xff01;&#xff01; 代码如下&#xff1a; #include <iostream> using namespace std; const int N 100010; int p[N]; int k; bool vis[N];void init() {for (int i 2;i<N-1;i){if (!vis[i])p[k] i;for (int j 2*i…

win10 64位操作系统安装mysql_win10,64位操作系统安装mysql-8.0.16经验总结(图文详细,保证一次安装成功)...

机器配置&#xff1a;win10&#xff0c;64位&#xff1b;mysql-8.0.161.mysql下载首先在mysql下载链接下载安装包&#xff1a;点击 Download 按钮进入下载页面&#xff0c;点击下图中的 No thanks, just start my download. 就可立即下载&#xff1a;2.解压及配置文件下载完后&…

基于 Roslyn 实现一个简单的条件解析引擎

基于 Roslyn 实现一个简单的条件解析引擎Intro最近在做一个勋章的服务&#xff0c;我们想定义一些勋章的获取条件&#xff0c;满足条件之后就给用户颁发一个勋章&#xff0c;定义条件的时候会定义需要哪些参数&#xff0c;参数的类型&#xff0c;获取勋章的时候会提供所需要的参…

BeetleX之vue-autoui自匹配UI插件

vue-autoui 是一款基于vue和element扩展的一个自动化UI控件&#xff0c;它主要提供两个控件封装分别是auto-form和auto-grid; 通过这两个控件可以完成大多数的信息输入和查询输出的需要.auto-form和auto-grid是通过json来描述展示的结构&#xff0c;在处理上要比写html标签来得…

protobufjs 命令执行_protobufjs简单使用

npm i protobufjs -D添加两个proto文件User.protosyntax "proto3";package login;message PBUser {string uid 1;string pwd 2;int64 age 3;}Login.protosyntax "proto3";package login;import "./User.proto";message LoginReq {PBUser us…

.NET Core开发实战(第27课:定义Entity:区分领域模型的内在逻辑和外在行为)--学习笔记...

27 | 定义Entity&#xff1a;区分领域模型的内在逻辑和外在行为上一节讲到领域模型分为两层一层是抽象层&#xff0c;定义了公共的接口和类另一层就是领域模型的定义层先看一下抽象层的定义1、实体接口 IEntitynamespace GeekTime.Domain {public interface IEntity{object[] G…

Abp vNext发布v2.3!

在全球范围内病毒笼罩的日子里,我们发布了ABP框架v2.3, 这篇文章将说明本次发布新增内容和过去的两周我们做了什么.关于新冠病毒和我们的团队关于冠状病毒的状况我们很难过.在Volosoft的团队,我们有不同国家的远程工作者在自己家里工作.从上周开始,我们已经完全开始在家远程工作…

ASP.NET Core 中间件分类

ASP.NET Core 中间件的配置方法可以分为以上三种&#xff0c;对应的Helper方法分别是&#xff1a;Run(), Use(), Map()。Run()&#xff0c;使用Run调用中间件的时候&#xff0c;会直接返回一个响应&#xff0c;所以后续的中间件将不会被执行了。Use()&#xff0c;它会对请求做一…

redis持久化到mysql的方案_redis进阶: 数据持久化

redis是内存数据库&#xff0c;即数据库状态都是存储于内存中&#xff0c;因此&#xff0c;当服务器重启或者断开后&#xff0c;数据便会丢失&#xff1b;为了解决数据丢失问题&#xff0c;便需要将数据从内存保持到磁盘中&#xff0c;这就是redis的数据持久化目前&#xff0c;…

如何创建一个自定义的`ErrorHandlerMiddleware`方法

在本文中&#xff0c;我将讲解如何通过自定义ExceptionHandlerMiddleware&#xff0c;以便在中间件管道中发生错误时创建自定义响应&#xff0c;而不是提供一个“重新执行”管道的路径。作者&#xff1a;依乐祝译文&#xff1a;https://www.cnblogs.com/yilezhu/p/12497937.htm…

mysql or中有空查询慢_MySQL 慢查询日志

1.定义2.相关参数2.开启3.原因4.慢查询日志工具mysqldumpslow1.定义作用:用来记录在MySQL中响应时间超过阀值的语句。2.相关参数mysql> show variables like %slow_query%;------------------------------------------------------------| Variable_name | Value …

从业务需求抽象成模型解决方案

从业务需求调研&#xff0c;通过抽象转换成模型技术方案&#xff0c;本文将对这个过程做个拆解&#xff0c;供大家参考。以下我所说的可能都是错的&#xff0c;只是一家之见&#xff0c;欢迎大家在留言区多提意见和看法&#xff0c;互相共勉。一、订单对象-信息需求公司的运营都…

《C++ Primer》7.1.4节练习

练习7.11: #include <iostream> #include <cstring> using namespace std;class Sales_data {public:Sales_data() default;Sales_data(const std::string &book): bookNo(book) {}Sales_data(const std::string &book, const unsigned num, const doubl…

DotNetCore Web应用程序中的Session管理

原文来自互联网&#xff0c;由长沙DotNET技术社区编译。如译文侵犯您的署名权或版权&#xff0c;请联系小编&#xff0c;小编将在24小时内删除。限于译者的能力有限&#xff0c;个别语句翻译略显生硬&#xff0c;还请见谅。作者简介&#xff1a;Jon&#xff08;Jonathan&#x…

css3是什么 ptml_CSS3

CSS3HTMLCSSJavaScript结构表项交互如何学习&#xff1f;CSS是什么CSS怎么用(快速入门)CSS选择器(重点难点)美化网页(文字、阴影、超链接、列表、渐变...)盒子模型浮动定位网页动画(特效效果)1、初识CSS1.1、什么是CSSCascading Style Sheet(层叠样式表)CSS&#xff1a;表现(美…

python docx runs_别再问我Python怎么操作Word了!

安装docx是一个非标准库&#xff0c;需要在命令行(终端)中使用pip即可安装pip install python-docx一定要注意&#xff0c;安装的时候是python-docx而实际调用时均为docx!前置知识Word中一般可以结构化成三个部分&#xff1a;文档Document段落Paragraph文字块Run也就是Document…

【要闻】如何基于K8s管理1600个微服务?某数字化银行秘诀公开

Cloud Foundry Foundation宣布KubeCF为新孵化项目Cloud Foundry Foundation是开放源代码项目的聚集地&#xff0c;简化了开发人员的体验&#xff0c;近日其宣布&#xff0c;KubeCF已成为该基金会的孵化项目&#xff0c;并已发布版本1.0.1。KubeCF是Cloud Foundry应用程序运行时…

如何用 Blazor 实现 Ant Design 组件库?

本文主要分享我创建 Ant Design of Blazor 项目的心路历程&#xff0c;已经文末有一个 Blazor 线上分享预告。Blazor WebAssembly 来了&#xff01;Blazor 这个新推出的前端 Web 框架&#xff0c;想必是去年 .NET Core 3.0 发布时才进入 .NET 开发者的视线的。但其实&#xff0…

.NET Core开发实战(第28课:工作单元模式(UnitOfWork):管理好你的事务)--学习笔记...

28 | 工作单元模式&#xff08;UnitOfWork&#xff09;&#xff1a;管理好你的事务工作单元模式有如下几个特性&#xff1a;1、使用同一上下文2、跟踪实体的状态3、保障事务一致性我们对实体的操作&#xff0c;最终的状态都是应该如实保存到我们的存储中&#xff0c;进行持久化…

《C++ Primer》7.3.2节练习

练习7.27: #include <iostream> #include <cstring> using namespace std;class Screen {private:unsigned height 0, width 0;unsigned cursor 0;string contents;public:Screen() default;Screen(unsigned ht, unsigned wd): height(ht), width(wd), conten…