1. 概述
在上一篇 Spring Cloud 文章中,我们为应用添加了 Zipkin 支持。本文将把前端应用整合到我们的技术栈中。
到目前为止,我们一直在纯后端环境中构建云应用。但一个没有 UI 的 Web 应用有什么意义呢?本文将通过集成单页应用(SPA)解决这个问题。
我们将使用 Angular 和 Bootstrap 编写前端。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();
}
关键调整点:
- ✅ 添加默认成功 URL 指向
/home/index.html
(Angular 应用所在位置) - ✅ 配置路径匹配器允许所有请求通过网关(除 Eureka 资源外)
- ✅ 移除登出成功 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,将看到:
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 开发
本节构建页面认证机制,使用基本认证实现简单流程:
- 用户通过登录表单输入凭据
- 使用凭据创建 base64 认证令牌请求
/me
接口 - 接口返回包含用户角色的
Principal
对象 - 在客户端存储凭据和主体信息供后续请求使用
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. 构建与测试
执行以下步骤:
- 在网关目录运行
mvn compile
(编译 Java 并构建 Angular) - 启动其他云服务:
config
、discovery
、zipkin
- 启动网关项目
- 访问 http://localhost:8080
初始界面:
点击登录链接:
使用 user/password
登录后:
登出后使用 admin/admin
登录:
5. 总结
本文展示了将单页应用集成到云系统的简单方法。我们使用现代框架实现了完整的安全配置集成。
基于这些示例,你可以尝试调用 book-service
或 rating-service
。既然我们已经掌握了 HTTP 调用和数据绑定,这应该相对简单。
完整源代码可在 GitHub 获取。