在你的Angular应用程序中添加认证


单页应用程序(SPA)是一个网络应用程序或网站,它通过重写当前页面而不是从服务器上加载全新的页面来与用户互动。在构建SPA时,开发者迟早需要考虑认证和授权问题。认证通常需要一个登录页面,以验证用户是否是他或她声称的人。在用户登录后,授权的作用是授予或限制特定的资源。  

今天,我将指导你完成建立一个带认证的Angular应用程序的步骤。你将使用 Okta 进行认证和访问控制。请务必利用Okta的 Angular专用 库,使整个过程更加容易。  

用Login构建Angular SPA

在本教程中,我将纯粹专注于客户端的安全。我不会深入研究服务器端认证或授权的话题。你要实现的应用是一个简单的无服务器在线计算器。对该计算器的访问将被限制在已登录的用户身上。自然地,现实生活中的应用程序将与服务器进行通信,并向服务器验证自己,以获得对受限资源的访问。

我将假设你已经在你的系统上安装了Node,并且你对node数据包管理器 npm 有些熟悉。本教程将使用Angular 7。要安装Angular命令行工具,请打开终端并输入命令。

npm install -g @angular/cli@7.1.4


这将安装全局 ng 命令。在一些系统中,Node将全局命令安装在普通用户无法写入的目录中。在这种情况下,你必须使用 sudo 来运行上面的命令。接下来,创建一个新的Angular应用程序。导航到一个你选择的目录,并发出以下命令。

ng new AngularCalculator


这将启动一个向导,引导你创建一个新的应用程序。该向导将提示你两个问题。当被问及是否要在你的应用程序中添加Angular路由时,回答   。接下来,你可以选择CSS技术。在这里选择普通CSS,因为你要开发的应用程序需要少量的样式。下一步是安装一些库,这将使你更容易创建一个愉快的、响应式的设计。换到你刚刚创建的 AngularCalculator 目录中,并运行命令。

ng add @angular/material


这将提示你有几个选项。在第一个问题中,你可以选择颜色主题。(我选择了   深紫/琥珀 。) 对于接下来的两个问题,对使用手势识别和浏览器动画都回答   。在Material Design的基础上,我将使用Flex Layout组件。运行该命令。

npm install @angular/flex-layout@7.0.0-beta.22


接下来,给应用程序添加一些一般的样式。打开 src/style.css ,用下面的内容替换。

body {
  margin: 0;
  font-family: sans-serif;
}

h1, h2 {
  text-align: center;
}


下一步是让Material Design组件在Angular应用程序中可用。用以下代码替换文件 src/app/app.module.ts 的内容。

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FlexLayoutModule } from "@angular/flex-layout";

import { MatToolbarModule,
         MatMenuModule,
         MatIconModule,
         MatCardModule,
         MatButtonModule,
         MatTableModule,
         MatDividerModule } from '@angular/material';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    BrowserAnimationsModule,
    FlexLayoutModule,
    MatToolbarModule,
    MatMenuModule,
    MatIconModule,
    MatCardModule,
    MatButtonModule,
    MatTableModule,
    MatDividerModule,
    AppRoutingModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


文件 src/app/app.component.html 包含主应用程序组件的模板。这个组件充当了应用程序及其所有组件的容器。我喜欢在这个组件中创建一个基本的工具条布局。打开该文件,用以下内容替换。

<mat-toolbar color="primary" class="expanded-toolbar">
  <span>
    <button mat-button routerLink="/">{{title}}</button>
    <button mat-button routerLink="/"><mat-icon>home</mat-icon></button>
  </span>
  <button mat-button routerLink="/calculator"><mat-icon>dialpad</mat-icon></button>
</mat-toolbar>
<router-outlet></router-outlet>


为了添加一些样式,打开 src/app/app.component.css 并添加以下几行。

.expanded-toolbar {
  justify-content: space-between;
  align-items: center;
}


HTML模板中的 <router-outlet> 标签作为由路由器管理的组件的占位符。接下来创建这些组件。该应用程序将由两个视图组成。主视图显示一个简单的闪屏,包含关于应用程序的信息。计算器组件包含实际的计算器。为了创建这些视图的组件,在应用程序的主目录下再次打开终端,为每个视图运行 "ng generate "命令。

ng generate component home
ng generate component calculator


这将在 src/app 下创建两个目录,每个组件一个。你将只在闪屏上添加两个简单的标题。打开 src/app/home/home.component.html ,用下面的内容替换。

<h1>Angular Calculator</h1>
<h2>A simple calculator with login</h2>


计算器组件包含应用程序的主要内容。首先,在 src/app/calculator/calculator.component.html 中为计算器的按钮和显示创建布局模板。

<h1 class="h1">Calculator</h1>
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" class="products">
  <mat-card class="mat-elevation-z1 calculator">
    <p class="display">{{display}}</p>
    <div>
      <button mat-raised-button color="warn" (click)="acPressed()">AC</button>
      <button mat-raised-button color="warn" (click)="cePressed()">CE</button>
      <button mat-raised-button color="primary" (click)="percentPressed()">%</button>
      <button mat-raised-button color="primary" (click)="operatorPressed('/')">÷</button>
    </div>
    <div>
      <button mat-raised-button (click)="numberPressed('7')">7</button>
      <button mat-raised-button (click)="numberPressed('8')">8</button>
      <button mat-raised-button (click)="numberPressed('9')">9</button>
      <button mat-raised-button color="primary" (click)="operatorPressed('*')">x</button>
    </div>
    <div>
      <button mat-raised-button (click)="numberPressed('4')">4</button>
      <button mat-raised-button (click)="numberPressed('5')">5</button>
      <button mat-raised-button (click)="numberPressed('6')">6</button>
      <button mat-raised-button color="primary" (click)="operatorPressed('-')">-</button>
    </div>
    <div>
      <button mat-raised-button (click)="numberPressed('1')">1</button>
      <button mat-raised-button (click)="numberPressed('2')">2</button>
      <button mat-raised-button (click)="numberPressed('3')">3</button>
      <button mat-raised-button color="primary" class="tall" (click)="operatorPressed('+')">+</button>
    </div>
    <div>
      <button mat-raised-button (click)="numberPressed('0')">0</button>
      <button mat-raised-button (click)="numberPressed('.')">.</button>
      <button mat-raised-button color="primary" (click)="equalPressed()">=</button>
    </div>
  </mat-card>
</div>


你会注意到按钮上的 (click) 属性。这个属性允许你指定组件类的成员函数,当按钮被点击时将被调用。在你着手实现该类之前,在 src/app/calculator/calculator.component.css 中添加一点样式。

.display {
  background-color: #f8f8f8;
  line-height: 24px;
  padding: 5px 8px;
}

.calculator button {
  margin: 5px;
  width: 64px;
}

.calculator button.tall {
  float: right;
  height: 82px;
}


该组件的类存在于文件 src/app/calculator/calculator.component.ts 中。打开这个文件,用以下代码替换其内容。

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-calculator',
  templateUrl: './calculator.component.html',
  styleUrls: ['./calculator.component.css']
})
export class CalculatorComponent implements OnInit {
  private stack: (number|string)[];
  display: string;

  constructor() { }

  ngOnInit() {
    this.display = '0';
    this.stack = ['='];
  }

  numberPressed(val: string) {
    if (typeof this.stack[this.stack.length - 1] !== 'number') {
      this.display = val;
      this.stack.push(parseInt(this.display, 10));
    } else {
      this.display += val;
      this.stack[this.stack.length - 1] = parseInt(this.display, 10);
    }
  }

  operatorPressed(val: string) {
    const precedenceMap = {'+': 0, '-': 0, '*': 1, '/': 1};
    this.ensureNumber();
    const precedence = precedenceMap[val];
    let reduce = true;
    while (reduce) {
      let i = this.stack.length - 1;
      let lastPrecedence = 100;

      while (i >= 0) {
        if (typeof this.stack[i] === 'string') {
          lastPrecedence = precedenceMap[this.stack[i]];
          break;
        }
        i--;
      }
      if (precedence <= lastPrecedence) {
        reduce = this.reduceLast();
      } else {
        reduce = false;
      }
    }

    this.stack.push(val);
  }

  equalPressed() {
    this.ensureNumber();
    while (this.reduceLast()) {}
    this.stack.pop();
  }

  percentPressed() {
    this.ensureNumber();
    while (this.reduceLast()) {}
    const result = this.stack.pop() as number / 100;
    this.display = result.toString  ;
  }

  acPressed() {
    this.stack = ['='];
    this.display = '0';
  }

  cePressed() {
    if (typeof this.stack[this.stack.length - 1] === 'number') { this.stack.pop(); }
    this.display = '0';
  }

  private ensureNumber() {
    if (typeof this.stack[this.stack.length - 1] === 'string') {
      this.stack.push(parseInt(this.display, 10));
    }
  }

  private reduceLast() {
    if (this.stack.length < 4) { return false; }
    const num2 = this.stack.pop() as number;
    const op = this.stack.pop() as string;
    const num1 = this.stack.pop() as number;
    let result = num1;
    switch (op) {
      case '+': result = num1 + num2;
        break;
      case '-': result = num1 - num2;
        break;
      case '*': result = num1 * num2;
        break;
      case '/': result = num1 / num2;
        break;
    }
    this.stack.push(result);
    this.display = result.toString  ;
    return true;
  }
}


这段代码包含一个完整的计算器。请看HTML模板中的按钮的回调函数是如何被实现为成员函数的。这个计算器知道四个基本操作 +-*/ ,而且它知道运算符的优先级。我不会去研究这些是如何实现的细节。我将把这个问题留给你作为一个练习来弄清楚。

在你开始测试应用程序之前,你需要在路由器上注册新的组件。打开 src/app/app-routing.module.ts ,编辑其内容,使之与以下内容相匹配。

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { CalculatorComponent } from './calculator/calculator.component';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'calculator', component: CalculatorComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }


演示程序现在已经完成,你可以启动服务器了。Angular命令行工具附带了一个开发服务器,是测试应用程序的理想选择。每当你对代码做任何修改,它都会自动使浏览器重新加载应用程序。要启动开发服务器,只需运行以下命令。

ng serve


打开你的浏览器,导航到 http://localhost:4200 ,并点击右上角的计算器图标。你应该看到如下图所示的东西。

Angular calculator

Angular计算器


为你的Angular应用程序添加认证

在这一部分,我将向你展示如何为你的应用程序添加认证。Okta为安全认证提供了一个简单的解决方案,可以轻松集成到Angular应用程序中。现成的路由保护可以让你限制对选定路由的访问,只需将其放入路由规范中即可。应用程序的流程如下。每当用户请求一个受保护的资源时,路由保护将检查用户是否已经登录。如果用户没有登录,防护装置将把用户重定向到Okta服务器上的托管登录页面。另外,用户也可以选择直接点击一个登录链接。在这种情况下,认证服务将把用户重定向到登录页面。一旦用户登录,登录页面将把用户重定向到应用程序中的一个特殊路由,通常称为 implicit/callback 。这个路由是由Okta回调组件管理的。回调组件将根据原始请求和用户的认证状态来决定将用户重定向到哪里。

在您开始之前,您需要在Okta注册一个免费的开发者账户。打开浏览器,浏览   developer.okta.com ,然后点击创建免费账户按钮。在接下来出现的表格中,输入您的详细信息。点击 "开始 "按钮,完成注册。

Create Okta Developer Account

创建Okta开发者账户

一旦您完成了注册过程,您将被带到您的Okta仪表板。选择顶部菜单中的应用程序链接。在这里,您可以看到您与Okta账户链接的所有应用程序的概况。如果您是一个新会员,这个列表将是空的。要创建您的第一个应用,请点击绿色按钮,上面写着添加应用。在接下来出现的屏幕上,选择单页应用程序并点击下一步。在下一个屏幕上,你可以编辑设置。Base URI应该指向你的应用程序的位置。由于你使用的是Angular开发服务器的标准设置,这应该被设置为 http://localhost:4200/ 。登录重定向URI是用户在成功登录后将被重定向到的位置。这应该与你的应用程序中的回调路线相匹配,所以你需要把它设置为 http://localhost:4200/implicit/callback 。当你完成后,点击完成按钮。由此产生的屏幕将为你提供一个客户ID,你需要将其复制并粘贴到你的应用程序中。

Angular app on Okta


Okta上的Angular应用程序

要开始在你的应用程序中实施认证,你需要安装Okta Angular库。在应用程序目录下打开终端,运行命令。

npm install @okta/okta-angular@1.0.7


打开 src/app/app.module.ts ,在文件的顶部添加以下导入。

import { OktaAuthModule } from '@okta/okta-angular';


在同一文件中,在 @NgModule 注释的 imports 部分,添加以下代码。

OktaAuthModule.initAuth({
  issuer: 'https://okta.okta.com/oauth2/default',
  redirectUri: 'http://localhost:4200/implicit/callback',
  clientId: '{yourClientId}'
})


{yourClientId} 的占位符应该用你在Okta仪表板上获得的客户ID替换。打开 src/app/app.component.ts ,用以下内容替换该文件的内容。

import { Component, OnInit } from '@angular/core';
import { OktaAuthService } from '@okta/okta-angular';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'AngularCalculator';
  isAuthenticated: boolean;

  constructor(public oktaAuth: OktaAuthService) {
    this.oktaAuth.$authenticationState.subscribe(
      (isAuthenticated: boolean)  => this.isAuthenticated = isAuthenticated
    );
  }

  ngOnInit() {
    this.oktaAuth.isAuthenticated().then((auth) => {
      this.isAuthenticated = auth;
    });
  }

  login() {
    this.oktaAuth.loginRedirect();
  }

  logout() {
    this.oktaAuth.logout('/');
  }
}


OktaAuthService 被注入到主应用程序组件中。主组件包含一个 isAuthenticated 标志,用于跟踪用户的认证状态。通过订阅 $authenticationState 的可观察性,每当状态发生变化时,这个标志就会保持最新。该标志在 ngOnInit 函数中被初始化。 login 成员函数简单地调用 OktaAuthService.loginRedirect ,它将用户重定向到托管的登录页面。同样地, logout 成员函数调用 OktaAuthService.logout ,它擦除任何用户标记并将用户重定向到主路径。

接下来,打开 src/app/app.component.html ,在第一个 <span> 元素的关闭标签后的 <mat-toolbar> 中添加以下代码。

<span>
  <button mat-button *ngIf="!isAuthenticated" (click)="login()"> Login </button>
  <button mat-button *ngIf="isAuthenticated" (click)="logout()"> Logout </button>
</span>


通过利用 isAuthenticated 标志,显示一个 Login 或一个 Logout 按钮。每个按钮都会调用应用组件的相应方法来记录用户的进入或退出。在最后一步,你需要修改路由器的设置。打开 src/app/app-routing.module.ts ,在文件的顶部添加以下导入。

import { OktaCallbackComponent, OktaAuthGuard } from '@okta/okta-angular';


在上面的代码和Okta仪表板设置中,你已经指定 implicit/callback 路由应该处理登录回调。要将 OktaCallbackComponent 与该路由注册,请在 routes 设置中添加以下条目。

{ path: 'implicit/callback', component: OktaCallbackComponent }


OktaAuthGuard 可以用来限制对任何受保护路由的访问。要保护 calculator 路由,请按以下方式添加 canActivate 属性来修改其条目。

{ path: 'calculator', component: CalculatorComponent, canActivate: [OktaAuthGuard] }


现在,如果你试图访问应用程序中的计算器,你会被重定向到Okta登录页面。只有在登录成功后,你才会被重定向到计算器上。另一方面,闪屏则无需任何认证即可访问。

使用Angular认证的Login Widget

在某些情况下,将用户重定向到一个外部登录页面是可以的。在其他情况下,你不希望用户离开你的网站。这就是登录小部件的一个用例。它让你直接将登录表格嵌入到你的应用程序中。要使用这个小部件,你首先要安装它。在应用程序目录下打开终端,安装以下软件包。

npm install @okta/okta-signin-widget@2.14.0 rxjs-compat@6.3.3


接下来,生成一个承载登录表格的组件。这个组件将不需要任何额外的样式,HTML模板将只包括一个标签,可以在组件定义中内联。

ng generate component login --inline-style=true --inline-template=true


打开新创建的 src/app/login/login.component.ts ,将以下内容粘贴到其中。

import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart} from '@angular/router';

import { OktaAuthService } from '@okta/okta-angular';
import * as OktaSignIn from '@okta/okta-signin-widget';

@Component({
  selector: 'app-login',
  template: `<div id="okta-signin-container"></div>`,
  styles: []
})
export class LoginComponent implements OnInit {
  widget = new OktaSignIn({
    baseUrl: 'https://okta.okta.com'
  });

  constructor(private oktaAuth: OktaAuthService, router: Router) {
    // Show the widget when prompted, otherwise remove it from the DOM.
    router.events.forEach(event => {
      if (event instanceof NavigationStart) {
        switch(event.url) {
          case '/login':
          case '/calculator':
            break;
          default:
            this.widget.remove();
            break;
        }
      }
    });
  }

  ngOnInit() {
    this.widget.renderEl({
      el: '#okta-signin-container'},
      (res) => {
        if (res.status === 'SUCCESS') {
          this.oktaAuth.loginRedirect('/', { sessionToken: res.session.token });
          // Hide the widget
          this.widget.hide();
        }
      },
      (err) => {
        throw err;
      }
    );
  }
}


src/index.html 中,在 <head> 标签内添加以下两行,以包括默认的部件样式。

<link href="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/2.14.0/css/okta-sign-in.min.css" type="text/css" rel="stylesheet"/>
<link href="https://ok1static.oktacdn.com/assets/js/sdk/okta-signin-widget/2.14.0/css/okta-theme.css" type="text/css" rel="stylesheet"/>


现在,在路由配置中添加登录路由。打开 src/app/app-routing.module.ts . 在文件的顶部为 LoginComponent 添加一个导入。

import { LoginComponent } from './login/login.component';


接下来,添加一个函数,告诉路由器在要求用户登录时该做什么。

export function onAuthRequired({ oktaAuth, router }) {
  router.navigate(['/login']);
}


确保该函数是导出的。在 routes 规范中,添加登录组件的路由。

{ path: 'login', component: LoginComponent }


最后,修改 calculator 路由的规范,包括对 onAuthRequired 函数的引用。

{
  path: 'calculator',
  component: CalculatorComponent,
  canActivate: [OktaAuthGuard],
  data: { onAuthRequired }
}


下一步是确保用户在按下顶栏的登录按钮时被引导到 login 页面。打开 src/app/app.component.html ,将包含登录按钮的一行改为以下内容。

<button mat-button *ngIf="!isAuthenticated" routerLink="/login"> Login </button>


你也可以删除 src/app/app.component.ts 中的 login 功能,因为它不再需要了。

就这样了! 你的应用程序现在拥有了由Okta提供的自己的登录表。下面是一个登录小部件的截图。

Angular Calculator with Sign-In Widget

带有登录小部件的Angular计算器


了解更多关于在Angular中构建安全登录和注册的信息

在这个教程中,我向你展示了如何在一个基于Angular的单页应用程序中实现认证和基本授权。你可以选择一个托管的登录页面和一个嵌入你的应用程序的登录小部件。当你知道有多个应用程序与一个用户账户相连时,托管登录是理想的选择。在这种情况下,托管解决方案传达了这样一种理念:用户正在一个中央位置登录所有的应用程序。当你想在一个单一的品牌应用中提供一个无缝的体验时,登录小部件是理想的解决方案。

下面是一些链接,你可以在那里找到更多关于单页应用程序、Angular和认证的信息。

本教程的代码可以在GitHub上找到   oktadeveloper/okta-angular-calculator-example

你喜欢这个教程吗?想了解更多很酷的东西,请在Twitter   @oktadev YouTube 关注我们。

为你的Angular App构建安全登录 最初于2019年2月12日发表在Okta开发者博客上。