在所有 Web 开发的框架中,Anglar 和 Spring Boot 可以说是两个最流行的了。那么我们不妨看看如何在你的应用中使用它们。
现在技术进展得很快,跟上最新的趋势以及你喜欢的项目的最新发布版本是很有挑战性的。Anglar 和 Spring Boot 是我最喜欢的两个项目。因此我想我应该给你们写个指南,让你清楚如何使用它们最新、最完整的版本构建一个基本的应用程序。
对于 Spring Boot,在 2.0 版本中最重要的变化是它的全新 Web 框架:Spring WebFlux。在 Angular 5.0 中我们也在表格中有了一个新的 HttpClient。这个类代替了 Http,并且使用起来更简单一些,使用更少的样板(boilerplate)代码即可。但今天,我并不打算去探索 Spring WebFlux,因为在我们能够支持 Okta Spring Boot Starte 之前我们 还有一些工作要做 。
好消息是我们的 Angular SDK 能够很好地与 Angular 5 兼容,我将在这篇博文中展示如何使用它。说到 Angular,你知道 在 Stack Overflow 上,Angular 是最引人注目的问题之一 吗?你可能认为这意味着很多人都对 Angular 有相关的疑问。我更偏向于认为是使用人数庞大,开发者在使用新技术时经常有疑问(所导致)。这是一个健康的社区的明确标志。对于垂死的技术你很少会在 Stack Overflow 上看到很多的问题。
本文将讲解如何构建一个简单的 CRUD 应用来显示一个酷的汽车的列表。它允许你去编辑这个列表,并且它将显示一个与汽车名称相匹配的源于 GIPHY 的 gif 动画。你也会学习到如何使用 Okta’s Spring Boot starter 和 Angular SDK 来保护你的应用程序。
本教程中,你将会需要在电脑中安装 Java 8 和 Node.js 8 。
在一开始使用 Spring Boot 2.0 时,你可以使用最新的里程碑版本。访问 start.spring.io ,然后使用 Java、Spring Boot 2.0.0 M6 创建一个新项目,并选择创建一个简单的 API:JPA,H2,Rest Repositories,Lombok 和 Web。在这个例子中,我已经添加了Actuator(执行器),它是 Spring Boot 中 一个非常酷的功能 。
创建一个目录来存放你的服务器和客户端应用程序。我的目录命名为 okta-spring-boot-2-angular-5-example,你可以命名为你喜欢的任意名称。如果你只想看该应用程序运行而不是编写代码,那么你可以 在 GitHub 上查看示例 ,或使用以下命令进行本地克隆和运行。
git clone https://github.com/oktadeveloper/okta-spring-boot-2-angular-5-example.git cd okta-spring-boot-2-angular-5-example/client && npm install && ng serve & cd ../server && ./mvnw spring-boot:run
从 start.spring.io 下载了 demo.zip 后,将其解压并将 demo 文件复制到应用程序存放目录。将 demo 重命名为 server。用你喜欢的 IDE 打开项目,在 src/main/java/com/okta/developer/demo 目录下创建一个 Car.java 文件。 你可以使用 Lombok 注解来减少样板代码。
package com.okta.developer.demo; import lombok.*; import javax.persistence.Id; import javax.persistence.GeneratedValue; import javax.persistence.Entity; @Entity @Getter @Setter @NoArgsConstructor @ToString @EqualsAndHashCode public class Car { @Id @GeneratedValue private Long id; private @NonNull String name; }
创建 CarRepository 类以在 Car 实体上执行 CRUD(创建,读取,更新和删除)。
package com.okta.developer.demo; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.rest.core.annotation.RepositoryRestResource; @RepositoryRestResource interface CarRepository extends JpaRepository<Car, Long> { }
将 ApplicationRunner bean 添加到 DemoApplication 类(在 src/main/java/com/okta/developer/demo/DemoApplication.java 中),并使用它添加一些默认数据到数据库。
package com.okta.developer.demo; import org.springframework.boot.ApplicationRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import java.util.stream.Stream; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Bean ApplicationRunner init(CarRepository repository) { return args -> { Stream.of("Ferrari", "Jaguar", "Porsche", "Lamborghini", "Bugatti", "AMC Gremlin", "Triumph Stag", "Ford Pinto", "Yugo GV").forEach(name -> { Car car = new Car(); car.setName(name); repository.save(car); }); repository.findAll().forEach(System.out::println); }; } }
如果你在添加此代码后启动你的应用程序(使用 ./mvnw spring-boot:run),则会在启动时看到汽车列表显示在控制台中。
Car(id=1, name=Ferrari) Car(id=2, name=Jaguar) Car(id=3, name=Porsche) Car(id=4, name=Lamborghini) Car(id=5, name=Bugatti) Car(id=6, name=AMC Gremlin) Car(id=7, name=Triumph Stag) Car(id=8, name=Ford Pinto) Car(id=9, name=Yugo GV)
添加一个 CoolCarController 类(在 src/main/java/com/okta/developer/demo/CoolCarController.java 中),该类用于返回一个汽车列表,并在 Angular 客户端中显示。
package com.okta.developer.demo; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Collection; import java.util.stream.Collectors; @RestController class CoolCarController { private CarRepository repository; public CoolCarController(CarRepository repository) { this.repository = repository; } @GetMapping("/cool-cars") public Collection<Car> coolCars() { return repository.findAll().stream() .filter(this::isCool) .collect(Collectors.toList()); } private boolean isCool(Car car) { return !car.getName().equals("AMC Gremlin") && !car.getName().equals("Triumph Stag") && !car.getName().equals("Ford Pinto") && !car.getName().equals("Yugo GV"); } }
如果你重启服务器应用程序,并使用浏览器或命令行客户端键入 localhost:8080/cool-cars,则应该会看到过滤后的汽车列表。
http localhost:8080/cool-cars
HTTP/1.1 200 Content-Type: application/json;charset=UTF-8 Date: Sun, 19 Nov 2017 21:29:22 GMT Transfer-Encoding: chunked [ { "id": 1, "name": "Ferrari" }, { "id": 2, "name": "Jaguar" }, { "id": 3, "name": "Porsche" }, { "id": 4, "name": "Lamborghini" }, { "id": 5, "name": "Bugatti" } ]
Angular CLI 是一个命令行工具,可为你生成一个 Angular 项目。它不仅可以创建新项目,还可以生成代码。这是一个方便的工具,因为它还提供了命令用来构建和优化生产环境中使用的项目。它使用 covers 下的 webpack 用于构建。如果你想了解更多关于 webpack 的信息,推荐这个网站 —— webpack.academy 。
你可以通过 https://cli.angular.io 了解 Angular CLI 的基础知识。
安装最新版本的 Angular CLI,版本号是 1.5.2。
npm install -g @angular/cli@1.5.2
在你创建的伞形目录中新建一个项目。我的名字命名为 okta-spring-boot-2-angular-5-example。
ng new client
客户端创建后,导航到其目录并安装 Angular Material。
cd client npm install --save @angular/material @angular/cdk
你将使用 Angular Material 的组件来使 UI 看起来更好,特别是在手机上。安装 Angular 的动画库,因为其中的 Angular Material 组件有时会用到。
npm install --save @angular/animations
如果你想了解有关 Angular Material 的更多信息,请参阅 https://material.angular.io 。它有各种组件的大量文档以及如何使用它们。
使用 Angular CLI 生成可与 Cool Cars API 交互的汽车服务。
ng g s car
将生成的文件移动到 client/src/app/shared/car 目录。
mkdir -p src/app/shared/car mv src/app/car.service.* src/app/shared/car/.
更新 car.service.ts 中的代码以从服务器获取汽车列表。
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; @Injectable() export class CarService { constructor(private http: HttpClient) { } getAll(): Observable<any> { return this.http.get('//localhost:8080/cool-cars'); } }
在 src/app/app.module.ts 中将此服务作为提供者添加,并导入 HttpClientModule。
import { CarService } from './shared/car/car.service'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent, CarListComponent ], imports: [ BrowserModule, HttpClientModule ], providers: [CarService], bootstrap: [AppComponent] })
生成 car-list 组件以显示汽车列表。
ng g c car-list
更新 client/src/app/car-list/car-list.component.ts 以使用 CarService 获取列表并在本地 cars 变量中设置值。
import { CarService } from '../shared/car/car.service'; export class CarListComponent implements OnInit { cars: Array<any>; constructor(private carService: CarService) { } ngOnInit() { this.carService.getAll().subscribe(data => { this.cars = data; }); } }
更新 client/src/app/car-list/car-list.component.html 以显示汽车列表。
<h2>Car List</h2> <div *ngFor="let car of cars"> {{car.name}} </div>
更新 client/src/app/app.component.html 以拥有 app-car-list 元素。
<div style="text-align:center"> <h1>Welcome to {{title}}!</h1> </div> <app-car-list></app-car-list>
使用 ng serve 启动客户端应用程序。打开你喜欢的浏览器访问 http://localhost:4200。不过你目前还不会看到汽车列表,如果你打开开发者控制台,就会看到原因。
发生此错误是因为你尚未在服务器上启用 CORS 服务(跨源资源共享)。
要在服务器上启用 CORS,请将 @CrossOrigin 注释添加到 CoolCarController(在 server/src/main/java/com/okta/developer/demo/CoolCarController.java 中)。
import org.springframework.web.bind.annotation.CrossOrigin; ... @GetMapping("/cool-cars") @CrossOrigin(origins = "http://localhost:4200") public Collection<Car> coolCars() { return repository.findAll().stream() .filter(this::isCool) .collect(Collectors.toList()); }
另外,将它添加到 CarRepository 中,以便在添加/删除/编辑时可以与其端点进行通信。
@RepositoryRestResource @CrossOrigin(origins = "http://localhost:4200") interface CarRepository extends JpaRepository<Car, Long> { }
重新启动服务器,刷新客户端,然后就可以在浏览器中看到汽车列表。
你已经安装了 Angular Material,要使用它的组件,只需导入它们即可。打开 client/src/app/app.module.ts,并为动画,Material 的工具栏,按钮,输入,列表和卡片布局添加导入。
import { MatButtonModule, MatCardModule, MatInputModule, MatListModule, MatToolbarModule } from '@angular/material'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ ... imports: [ BrowserModule, HttpClientModule, BrowserAnimationsModule, MatButtonModule, MatCardModule, MatInputModule, MatListModule, MatToolbarModule, ], ... })
更新 client/src/app/app.component.html 以使用工具栏组件。
<mat-toolbar color="primary"> <span>Welcome to {{title}}!</span> </mat-toolbar> <app-car-list></app-car-list>
更新 client/src/app/car-list/car-list.component.html 以使用卡片布局和列表组件。
<mat-card> <mat-card-header>Car List</mat-card-header> <mat-card-content> <mat-list> <mat-list-item *ngFor="let car of cars"> <img mat-list-avatar src="{{car.giphyUrl}}" alt="{{car.name}}"> <h3 mat-line>{{car.name}}</h3> </mat-list-item> </mat-list> </mat-card-content> </mat-card>
修改 client/src/styles.csss 来指定主题和图标。
@import "~@angular/material/prebuilt-themes/pink-bluegrey.css"; @import '~https://fonts.googleapis.com/icon?family=Material+Icons'; body { margin: 0; font-family: Roboto, sans-serif; }
如果你用 ng serve 运行你的客户端并访问 http://localhost:4200,你会看到汽车列表,但没有与它们关联的图像。
要将 giphyUrl 属性添加到汽车,请创建 client/src/app/shared/giphy/giphy.service.ts 并用下面的代码填充它。
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import 'rxjs/add/operator/map'; @Injectable() export class GiphyService { // Public beta key: https://github.com/Giphy/GiphyAPI#public-beta-key giphyApi = '//api.giphy.com/v1/gifs/search?api_key=dc6zaTOxFJmzC&limit=1&q='; constructor(public http: HttpClient) { } get(searchTerm) { const apiLink = this.giphyApi + searchTerm; return this.http.get(apiLink).map((response: any) => { if (response.data.length > 0) { return response.data[0].images.original.url; } else { return 'https://media.giphy.com/media/YaOxRsmrv9IeA/giphy.gif'; // dancing cat for 404 } }); } }
在 client/src/app/app.module.ts 中添加 GiphyService 作为提供者。
import { GiphyService } from './shared/giphy/giphy.service'; @NgModule({ ... providers: [CarService, GiphyService], bootstrap: [AppComponent] })
更新 client/src/app/car-list/car-list.component.ts 中的代码以设置每辆车的 giphyUrl 属性。
import { GiphyService } from '../shared/giphy/giphy.service'; export class CarListComponent implements OnInit { cars: Array<any>; constructor(private carService: CarService, private giphyService: GiphyService) { } ngOnInit() { this.carService.getAll().subscribe(data => { this.cars = data; for (const car of this.cars) { this.giphyService.get(car.name).subscribe(url => car.giphyUrl = url); } }); } }
现在你的浏览器应该会显示汽车名称列表,以及旁边的头像图片。
有一个汽车名称和图像的列表显得十分美观,但如果能和它进行交互就更好了!要添加编辑功能,首先生成一个 car-edit 组件。
ng g c car-edit
更新 client/src/app/shared/car/car.service.ts 以拥有添加、删除和更新汽车的方法。这些方法与 CarRepository 和 @RepositoryRestResource 注释提供的端点进行交互。
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; @Injectable() export class CarService { public API = '//localhost:8080'; public CAR_API = this.API + '/cars'; constructor(private http: HttpClient) { } getAll(): Observable<any> { return this.http.get(this.API + '/cool-cars'); } get(id: string) { return this.http.get(this.CAR_API + '/' + id); } save(car: any): Observable<any> { let result: Observable<Object>; if (car['href']) { result = this.http.put(car.href, car); } else { result = this.http.post(this.CAR_API, car); } return result; } remove(href: string) { return this.http.delete(href); } }
在 client/src/app/car-list/car-list.component.html 中,添加一个到编辑组件的链接。另外,在底部添加一个按钮来添加一辆新车。
<mat-card> <mat-card-header>Car List</mat-card-header> <mat-card-content> <mat-list> <mat-list-item *ngFor="let car of cars"> <img mat-list-avatar src="{{car.giphyUrl}}" alt="{{car.name}}"> <h3 mat-line> <a mat-button [routerLink]="['/car-edit', car.id]">{{car.name}}</a> </h3> </mat-list-item> </mat-list> </mat-card-content> <button mat-fab color="primary" [routerLink]="['/car-add']">Add</button> </mat-card>
在 client/src/app/app.module.ts 中,添加路由并导入 FormsModule。
import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; const appRoutes: Routes = [ { path: '', redirectTo: '/car-list', pathMatch: 'full' }, { path: 'car-list', component: CarListComponent }, { path: 'car-add', component: CarEditComponent }, { path: 'car-edit/:id', component: CarEditComponent } ]; @NgModule({ ... imports: [ ... FormsModule, RouterModule.forRoot(appRoutes) ], ... })
修改 client/src/app/car-edit/car-edit.component.ts 以从 URL 上传递的 id 获取汽车的信息,并添加保存和删除的方法。
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs/Subscription'; import { ActivatedRoute, Router } from '@angular/router'; import { CarService } from '../shared/car/car.service'; import { GiphyService } from '../shared/giphy/giphy.service'; import { NgForm } from '@angular/forms'; @Component({ selector: 'app-car-edit', templateUrl: './car-edit.component.html', styleUrls: ['./car-edit.component.css'] }) export class CarEditComponent implements OnInit, OnDestroy { car: any = {}; sub: Subscription; constructor(private route: ActivatedRoute, private router: Router, private carService: CarService, private giphyService: GiphyService) { } ngOnInit() { this.sub = this.route.params.subscribe(params => { const id = params['id']; if (id) { this.carService.get(id).subscribe((car: any) => { if (car) { this.car = car; this.car.href = car._links.self.href; this.giphyService.get(car.name).subscribe(url => car.giphyUrl = url); } else { console.log(`Car with id '${id}' not found, returning to list`); this.gotoList(); } }); } }); } ngOnDestroy() { this.sub.unsubscribe(); } gotoList() { this.router.navigate(['/car-list']); } save(form: NgForm) { this.carService.save(form).subscribe(result => { this.gotoList(); }, error => console.error(error)) } remove(href) { this.carService.remove(href).subscribe(result => { this.gotoList(); }, error => console.error(error)) } }
更新 client/src/app/car-edit/car-edit.component.html 中的 HTML 以使用汽车名称的表格,以及显示来自 Giphy 的图像。
<mat-card> <form #carForm="ngForm" (ngSubmit)="save(carForm.value)"> <mat-card-header> <mat-card-title><h2>{{car.name ? 'Edit' : 'Add'}} Car</h2></mat-card-title> </mat-card-header> <mat-card-content> <input type="hidden" name="href" [(ngModel)]="car.href"> <mat-form-field> <input matInput placeholder="Car Name" [(ngModel)]="car.name" required name="name" #name> </mat-form-field> </mat-card-content> <mat-card-actions> <button mat-raised-button color="primary" type="submit" [disabled]="!carForm.form.valid">Save</button> <button mat-raised-button color="secondary" (click)="remove(car.href)" *ngIf="car.href" type="button">Delete</button> <a mat-button routerLink="/car-list">Cancel</a> </mat-card-actions> <mat-card-footer> <div class="giphy"> <img src="{{car.giphyUrl}}" alt="{{car.name}}"> </div> </mat-card-footer> </form> </mat-card>
将下面的 CSS 添加到 client/src/app/car-edit/car-edit.component.css 中,以在图片周围添加一些填充。
.giphy { margin: 10px; }
修改 client/src/app/app.component.html 并用 <router-outlet></router-outlet> 替换 <app-car-list></app-car-list>。这种更改是必要的,或者组件之间的路由不起作用。
<mat-toolbar color="primary"> <span>Welcome to !</span> </mat-toolbar> <router-outlet></router-outlet>
完成所有这些更改后,你应该可以添加、编辑或删除任何汽车。 以下是包含添加按钮的显示列表的屏幕截图。
以下屏幕截图显示了编辑你添加的汽车的状态。
使用 Okta 添加验证是一个极好的你可以添加到此应用的功能。如果你想为你的应用程序添加审核或个性化功能(例如评级功能),那么知道对方是谁可以派得上用场。
在服务器端,你可以使用 Okta Spring Boot starter 来锁定一些事物。打开 server/pom.xml 并添加以下依赖项。
<dependency> <groupId>com.okta.spring</groupId> <artifactId>okta-spring-boot-starter</artifactId> <version>0.2.0</version> </dependency>
现在你需要配置服务器以使用 Okta 进行认证,为此你将需要在 Okta 中创建一个 OIDC 应用。
登录你的 Okta 开发者帐户(如果没有帐户的话点此进行 注册 ),然后导航到 Applications > Add Application。 点击 Single-page App ,再点击 Next, 并给程序取个你能记住的名字。更改本地主机的所有实例 localhost:8080 到 localhost:4200,并点击 Done。
拷贝 client ID 到你的 server/src/main/resources/application.properties 文件中。当你在里面的时候,添加一个与你的 Okta 域匹配的 okta.oauth2 issuer 属性。例如:
okta.oauth2.issuer=https://{你的Okta域名}.com/oauth2/default okta.oauth2.clientId={clientId}
升级 server/src/main/java/com/okta/developer/demo/DemoApplication.java 来启用它作为资源服务器。
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; @EnableResourceServer @SpringBootApplication
在作出这些变更后,你应该能够重启你的 app 并且当你尝试导航到 http://localhost:8080 时看到拒绝访问。
不幸的是,你可能会看到一个带有以下错误的堆栈跟踪。
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor
原因是 Spring Boot 2.0.0.M6 包括了 Spring Security 5.0.0.RC1,它不包括 Resource Server 的支持。如果你想知道这个问题何时解决,你可以在 GitHub 上订阅 Okta Spring Boot Starter issue #30 。
为了解决这个问题,你可以将 Okta Spring Boot starter 降级为 0.1.0。一定要将它的名称从 spring-boot 改为 spring-security!
<dependency> <groupId>com.okta.spring</groupId> <artifactId>okta-spring-security-starter</artifactId> <version>0.1.0</version> </dependency>
你还需要更改应用程序中的属性名称,属性是 oauth 而不是 oauth2。
okta.oauth.issuer=https://{你的Okta域名}.com/oauth2/default okta.oauth.clientId={clientId}
现在,当你重新启动服务器时,你应该在浏览器中看到如下所示的消息。
很好,你的服务器已被锁定,但是现在你需要配置你的客户端来与之对话。这就是 Okta 对 Angluar 的支持派上用场的地方。
Okta Angular SDK 是在 Okta Auth JS 上的一个封装,它构建在 OIDC 之上。更多关于 Okta Angular 库的信息可在 npmjs.com 上找到。
要安装它,请在客户端目录下执行以下命令:
npm install --save @okta/okta-angular
在 client/src/app/app.module.ts 中,添加一个用于配置你的 OIDC 应用的 config 变量。
const config = { issuer: 'https://{yourOktaDomain}.com/oauth2/default', redirectUri: 'http://localhost:4200/implicit/callback', clientId: '{clientId}' };
在同一个文件中,你同样需要为 redirectUri 添加一个新的路由,它将指向 OktaCallbackComponent。
import { OktaCallbackComponent, OktaAuthModule } from '@okta/okta-angular'; const appRoutes: Routes = [ ... { path: 'implicit/callback', component: OktaCallbackComponent } ];
接下来,初始化并导入 OktaAuthModule。
@NgModule({ ... imports: [ ... OktaAuthModule.initAuth(config) ], ... })
这里有三个你在使用 Okta 时需要配置 Angular 应用的步骤。为了方便,在 HTTP 请求中添加不记名令牌时,可以使用 HttpInterceptor 。
创建 client/src/app/shared/okta/auth.interceptor.ts,并在其中添加以下代码。
import { Injectable } from '@angular/core'; import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { OktaAuthService } from '@okta/okta-angular'; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private oktaAuth: OktaAuthService) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // Only add to localhost requests since Giphy's API fails when the request include a token if (request.url.indexOf('localhost') > -1) { request = request.clone({ setHeaders: { Authorization: `Bearer ${this.oktaAuth.getAccessToken().accessToken}` } }); } return next.handle(request); } }
为了注册此拦截器,在 client/src/app/app.module.ts 中将其添加为 provider。
import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { AuthInterceptor } from './shared/okta/auth.interceptor'; @NgModule({ ... providers: [CarService, GiphyService, {provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true} ], ... })
修改 client/src/app/app.component.html 以添加 login 和 logout 按钮。
<mat-toolbar color="primary"> <span>Welcome to {{title}}!</span> <span class="toolbar-spacer"></span> <button mat-raised-button color="accent" *ngIf="oktaAuth.isAuthenticated()" (click)="oktaAuth.logout()">Logout </button> </mat-toolbar> <mat-card *ngIf="!oktaAuth.isAuthenticated()"> <mat-card-content> <button mat-raised-button color="accent" (click)="oktaAuth.loginRedirect()">Login </button> </mat-card-content> </mat-card> <router-outlet></router-outlet>
你可能已经注意到有个支持工具栏类的 span 存在。为了使其按照预期工作,更新 client/src/app/app.component.css 以包含以下类。
.toolbar-spacer { flex: 1 1 auto; }
这里也存在一个指向 oktaAuth 用于检查已认证状态的引用。为了使其有效,在 client/src/app/app.component.ts 中将其作为依赖项添加到构造函数中。
import { OktaAuthService } from '@okta/okta-angular'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'app'; constructor(private oktaAuth: OktaAuthService) { } }
现在如果你重启客户端,就可以看到登录按钮了。
注意这里使用 car-list 组件来显示元素。为了修正这种依赖,你可以创建一个 home 组件,并把其作为默认路由。
ng g c home
修改 client/src/app/app.module.ts 以更新路由。
const appRoutes: Routes = [ {path: '', redirectTo: '/home', pathMatch: 'full'}, { path: 'home', component: HomeComponent }, ... }
将 Login 按钮相关的 HTML 从 app.component.html 移动到 client/src/app/home/home.component.html中。
<mat-card> <mat-card-content> <button mat-raised-button color="accent" *ngIf="!oktaAuth.isAuthenticated()" (click)="oktaAuth.loginRedirect()">Login </button> <button mat-raised-button color="accent" *ngIf="oktaAuth.isAuthenticated()" [routerLink]="['/car-list']">Car List </button> </mat-card-content> </mat-card>
在 client/src/app/home/home.component.ts 中将 oktaAuth 作为依赖项添加。
import { OktaAuthService } from '@okta/okta-angular'; export class HomeComponent { constructor(private oktaAuth: OktaAuthService) { } }
更新 client/src/app/app.component.html,这样 Logout 按钮在点击之后将重定向到 home。
<mat-toolbar color="primary"> <span>Welcome to {{title}}!</span> <span class="toolbar-spacer"></span> <button mat-raised-button color="accent" *ngIf="oktaAuth.isAuthenticated()" (click)="oktaAuth.logout()" [routerLink]="['/home']">Logout </button> </mat-toolbar> <router-outlet></router-outlet>
现在你应该能够在你的浏览器打开 http://localhost:4200 并且点击 Login 按钮。如果您已经正确配置了所有内容,你将会跳转到 Okta 的登录界面。
输入您用于注册帐户的凭据,应该会重定向回到你的 app。然而,由于 CORS 错误,你的车辆列表并不会加载。出现这个情况是因为 Spring 的 @CrossOrigin 与 Spring Security 不兼容导致。
通过将一个 bean 添加到处理 CORS 的 DemoApplication.java 来修复它。
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.core.Ordered; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import java.util.Collections; ... @Bean public FilterRegistrationBean simpleCorsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.setAllowedOrigins(Collections.singletonList("http://localhost:4200")); config.setAllowedMethods(Collections.singletonList("*")); config.setAllowedHeaders(Collections.singletonList("*")); source.registerCorsConfiguration("/**", config); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(Ordered.HIGHEST_PRECEDENCE); return bean; }
重启你的服务,庆祝一切顺利!
你可以在 GitHub 上的 https://github.com/oktadeveloper/okta-spring-boot-2-angular-5-example 看到本教程中开发的应用程序的完整源代码。