1. 概述

在上一篇 Spring Cloud 文章中,我们为应用添加了 Zipkin 支持。本文将把前端应用整合到我们的技术栈中。

到目前为止,我们一直在纯后端环境中构建云应用。但一个没有 UI 的 Web 应用有什么意义呢?本文将通过集成单页应用(SPA)解决这个问题。

我们将使用 AngularBootstrap 编写前端。Angular 4 的代码风格与 Spring 应用开发非常相似,这对 Spring 开发者来说是个自然的过渡!虽然前端代码使用 Angular,但本文内容可以轻松扩展到任何前端框架。

本文将构建一个 Angular 4 应用并连接到云服务,演示 SPA 与 Spring Security 的登录集成,以及如何使用 Angular 的 HTTP 通信功能访问应用数据。

2. 网关调整

有了前端后,我们将切换到基于表单的登录方式,并对 UI 的部分内容进行权限控制。这需要修改网关的安全配置。

2.1. 更新 HttpSecurity

首先更新网关 SecurityConfig.java 类中的 configure(HttpSecurity http) 方法:

@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
    http.formLogin()
        .authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/home/index.html"))
        .and().authorizeExchange()
        .pathMatchers("/book-service/**", "/rating-service/**", "/login*", "/").permitAll()
        .pathMatchers("/eureka/**").hasRole("ADMIN")
        .anyExchange().authenticated()
        .and().logout().and().csrf().disable()
        .httpBasic(withDefaults());
    return http.build();
}

关键调整点:

  1. ✅ 添加默认成功 URL 指向 /home/index.html(Angular 应用所在位置)
  2. ✅ 配置路径匹配器允许所有请求通过网关(除 Eureka 资源外)
  3. ✅ 移除登出成功 URL(默认重定向到登录页已足够)

2.2. 添加用户信息接口

接下来添加一个返回认证用户信息的接口。Angular 应用将使用它进行登录并识别用户角色,从而控制站点操作权限。

在网关项目中添加 AuthenticationController 类:

@RestController
public class AuthenticationController {
 
    @GetMapping("/me")
    public Principal getMyUser(Principal principal) {
        return principal;
    }
}

这个控制器直接返回当前登录用户对象,为 Angular 应用提供完整的权限控制信息。

2.3. 添加着陆页

添加一个简单的着陆页,让用户访问应用根路径时能看到内容:

src/main/resources/static 下创建 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Book Rater Landing</title>
</head>
<body>
    <h1>Book Rater</h1>
    <p>So many great things about the books</p>
    <a href="/login">Login</a>
</body>
</html>

3. Angular CLI 和启动项目

开始前确保安装最新版 Node.js 和 npm

3.1. 安装 Angular CLI

使用 npm 安装 Angular 命令行工具:

npm install -g @angular/cli

3.2. 创建新项目

在网关项目的 gateway/src/main 目录下创建 angular 文件夹并进入,然后运行:

ng new ui

⚠️ 耐心等待:CLI 正在创建新项目并下载所有 JavaScript 依赖,这个过程可能需要几分钟。

3.3. 运行项目

进入创建的 ui 文件夹运行:

ng serve

构建完成后访问 http://localhost:4200,将看到:

angular2 start 1

3.4. 安装 Bootstrap

使用 npm 安装 Bootstrap:

npm install [email protected] --save

ui 目录下打开 .angular-cli.json,找到 apps > styles 属性并添加 Bootstrap CSS 路径:

"styles": [
    "styles.css",
    "../node_modules/bootstrap/dist/css/bootstrap.min.css"
],

3.5. 设置构建输出目录

告诉 Angular 将构建文件输出到 Spring Boot 可服务的位置。Spring Boot 支持两个资源目录:

  • src/main/resources/static
  • src/main/resource/public

由于 static 目录已被 Eureka 使用,且 Angular 每次构建会清空该目录,我们将构建输出到 public 文件夹:

修改 .angular-cli.json 中的 apps > outDir

"outDir": "../../resources/static/home",

3.6. Maven 自动化构建

配置 Maven 编译时自动构建 Angular 应用。在网关的 pom.xml 中添加:

<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        <execution>
            <phase>generate-resources</phase>
            <configuration>
                <tasks>
                    <exec executable="cmd" osfamily="windows"
                      dir="${project.basedir}/src/main/angular/ui">
                        <arg value="/c"/>
                        <arg value="ng"/>
                        <arg value="build"/>
                    </exec>
                    <exec executable="/bin/sh" osfamily="mac"
                      dir="${project.basedir}/src/main/angular/ui">
                        <arg value="-c"/>
                        <arg value="ng build"/>
                    </exec>
                </tasks>
            </configuration>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
</plugin>

⚠️ 注意:此配置要求 Angular CLI 在环境变量中可用,否则构建会失败。

4. Angular 开发

本节构建页面认证机制,使用基本认证实现简单流程:

  1. 用户通过登录表单输入凭据
  2. 使用凭据创建 base64 认证令牌请求 /me 接口
  3. 接口返回包含用户角色的 Principal 对象
  4. 在客户端存储凭据和主体信息供后续请求使用

4.1. 模板

在网关项目的 src/main/angular/ui/src/app 目录下打开 app.component.html,添加导航栏和登录表单:

<nav class="navbar navbar-toggleable-md navbar-inverse fixed-top bg-inverse">
    <button class="navbar-toggler navbar-toggler-right" type="button" 
      data-toggle="collapse" data-target="#navbarCollapse" 
      aria-controls="navbarCollapse" aria-expanded="false" 
      aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
    </button>
    <a class="navbar-brand" href="#">Book Rater 
        <span *ngIf="principal.isAdmin()">Admin</span></a>
    <div class="collapse navbar-collapse" id="navbarCollapse">
    <ul class="navbar-nav mr-auto">
    </ul>
    <button *ngIf="principal.authenticated" type="button" 
      class="btn btn-link" (click)="onLogout()">Logout</button>
    </div>
</nav>

<div class="jumbotron">
    <div class="container">
        <h1>Book Rater App</h1>
        <p *ngIf="!principal.authenticated" class="lead">
        Anyone can view the books.
        </p>
        <p *ngIf="principal.authenticated && !principal.isAdmin()" class="lead">
        Users can view and create ratings</p>
        <p *ngIf="principal.isAdmin()"  class="lead">Admins can do anything!</p>
    </div>
</div>

关键特性:

  • ✅ 使用 Bootstrap 类构建导航栏
  • ✅ 动态显示用户角色(*ngIf="principal.isAdmin()"
  • ✅ 根据认证状态显示不同提示信息

4.2. TypeScript

在相同目录打开 app.component.ts,添加支持模板的 TypeScript 代码:

import {Component} from "@angular/core";
import {Principal} from "./principal";
import {Response} from "@angular/http";
import {Book} from "./book";
import {HttpService} from "./http.service";

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {
    selectedBook: Book = null;
    principal: Principal = new Principal(false, []);
    loginFailed: boolean = false;

    constructor(private httpService: HttpService){}

    ngOnInit(): void {
        this.httpService.me()
          .subscribe((response: Response) => {
              let principalJson = response.json();
              this.principal = new Principal(principalJson.authenticated,
              principalJson.authorities);
          }, (error) => {
              console.log(error);
        });
    }

    onLogout() {
        this.httpService.logout()
          .subscribe((response: Response) => {
              if (response.status === 200) {
                  this.loginFailed = false;
                  this.principal = new Principal(false, []);
                  window.location.replace(response.url);
              }
           }, (error) => {
               console.log(error);
       });
    }
}

核心逻辑:

  • ngOnInit() 生命周期中调用 /me 接口获取用户权限
  • onLogout() 处理登出并重置页面状态
  • ✅ 通过构造函数注入 HttpService(类似 Spring 的依赖注入)

4.3. HttpService

创建 http.service.ts 文件:

import {Injectable} from "@angular/core";
import {Observable} from "rxjs";
import {Response, Http, Headers, RequestOptions} from "@angular/http";
import {Book} from "./book";
import {Rating} from "./rating";

@Injectable()
export class HttpService {

    constructor(private http: Http) { }

    me(): Observable<Response> {
        return this.http.get("/me", this.makeOptions())
    }

    logout(): Observable<Response> {
        return this.http.post("/logout", '', this.makeOptions())
    }

    private makeOptions(): RequestOptions {
        let headers = new Headers({'Content-Type': 'application/json'});
        return new RequestOptions({headers: headers});
    }
}

关键功能:

  • ✅ 封装 /me/logout 接口调用
  • ✅ 统一设置请求头(Content-Type: application/json)

app.module.ts 中注册服务:

providers: [HttpService],

4.4. 添加 Principal

创建 principal.ts 文件:

export class Principal {
    public authenticated: boolean;
    public authorities: Authority[] = [];
    public credentials: any;

    constructor(authenticated: boolean, authorities: any[], credentials: any) {
        this.authenticated = authenticated;
        authorities.map(
          auth => this.authorities.push(new Authority(auth.authority)))
        this.credentials = credentials;
  }

    isAdmin() {
        return this.authorities.some(
          (auth: Authority) => auth.authority.indexOf('ADMIN') > -1)
    }
}

export class Authority {
    public authority: String;

    constructor(authority: String) {
        this.authority = authority;
    }
}

设计要点:

  • Principal 类封装认证状态和权限信息
  • isAdmin() 方法快速判断管理员权限
  • ✅ 无需注册到 Angular DI 系统(类似 POJO)

4.5. 404 处理

在网关的 Java 代码中添加 ErrorPageConfig 类:

@Component
public class ErrorPageConfig implements ErrorPageRegistrar {
 
    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND,
          "/home/index.html"));
    }

}

作用:将所有 404 请求重定向到 /home/index.html,这是单页应用的标准路由处理方式。

4.6. 构建与测试

执行以下步骤:

  1. 在网关目录运行 mvn compile(编译 Java 并构建 Angular)
  2. 启动其他云服务:configdiscoveryzipkin
  3. 启动网关项目
  4. 访问 http://localhost:8080

初始界面:

Book Rater

点击登录链接:

ng2 login 1

使用 user/password 登录后:

Book Rater App

登出后使用 admin/admin 登录:

Book Rate App

5. 总结

本文展示了将单页应用集成到云系统的简单方法。我们使用现代框架实现了完整的安全配置集成。

基于这些示例,你可以尝试调用 book-servicerating-service。既然我们已经掌握了 HTTP 调用和数据绑定,这应该相对简单。

完整源代码可在 GitHub 获取。


原始标题:Spring Cloud – Adding Angular 4