抢先一步
VMware 提供培训和认证,助你加速进步。
了解更多注意:本博客的源代码和测试持续演进,但此处不维护文本的更改。请参阅教程版本以获取最新内容。
在本文中,我们将展示 Spring Security、Spring Boot 和 Angular JS 协同工作的一些出色特性,以提供愉快且安全的用户体验。本文对于 Spring 和 Angular JS 的初学者应该很容易理解,但对于其中任何一个的专家来说,也有很多有用的细节。这实际上是关于 Spring Security 和 Angular JS 系列文章中的第一篇,每篇都会陆续介绍新功能。我们将在第二篇及后续文章中改进此应用,但在此之后的主要更改是架构上的,而非功能上的。
HTML5、丰富的浏览器端特性以及“单页应用”是现代开发者极其宝贵的工具,但任何有意义的交互都会涉及后端服务器。因此,除了静态内容(HTML、CSS 和 JavaScript)之外,我们还需要一个后端服务器。后端服务器可以扮演多种角色中的任何一种或全部:提供静态内容、有时(但现在不那么频繁了)渲染动态 HTML、认证用户、保护对受保护资源的访问,以及(最后但同样重要)通过 HTTP 和 JSON 与浏览器中的 JavaScript 交互(有时被称为 REST API)。
Spring 一直以来都是构建后端特性(尤其是在企业领域)的流行技术,随着Spring Boot的出现,事情变得前所未有的简单。让我们看看如何使用 Spring Boot、Angular JS 和 Twitter Bootstrap 从零开始构建一个新的单页应用。选择这个特定的技术栈并没有特别的原因,但它相当流行,尤其是在企业 Java 开发团队中的核心 Spring 受众中,因此这是一个值得入手的起点。
我们将详细讲解如何创建这个应用,以便不完全熟悉 Spring 和 Angular 的任何人都能理解正在发生的事情。如果你想快速看到结果,可以跳到最后,那里有正在运行的应用,并了解它们是如何协同工作的。创建新项目有各种选项:
我们将要构建的完整项目的源代码位于此处的 Github 上,因此如果你愿意,可以直接克隆该项目并直接开始工作。然后跳到下一节。
创建新项目以快速入门的最简单方法是使用Spring Boot Initializr。例如,在类 UN*X 系统上使用 curl:
$ mkdir ui && cd ui
$ curl https://start.spring.io/starter.tgz -d style=web \
-d style=security -d name=ui | tar -xzvf -
然后你可以将该项目(默认是一个普通的 Maven Java 项目)导入到你喜欢的 IDE 中,或者只在命令行中使用文件和 "mvn" 命令。然后跳到下一节。
你可以使用Spring Boot CLI 创建相同的项目,如下所示:
$ spring init --dependencies web,security ui/ && cd ui
然后跳到下一节。
如果你愿意,也可以直接从Spring Boot Initializr 网站获取 .zip 文件形式的相同代码。只需在浏览器中打开它,选择依赖项“Web”和“Security”,然后点击“Generate Project”。该 .zip 文件在根目录中包含一个标准的 Maven 或 Gradle 项目,因此在解压之前,你可能需要创建一个空目录。然后跳到下一节。
在Spring Tool Suite(一套 Eclipse 插件)中,你也可以使用向导 File->New->Spring Starter Project
创建和导入项目。然后跳到下一节。
单页应用的核心是一个静态的 "index.html" 文件,所以让我们直接创建一个(在 "src/main/resources/static" 或 "src/main/resources/public" 目录下)。
<!doctype html>
<html>
<head>
<title>Hello AngularJS</title>
<link href="css/angular-bootstrap.css" rel="stylesheet">
<style type="text/css">
[ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important;
}
</style>
</head>
<body ng-app="hello">
<div class="container">
<h1>Greeting</h1>
<div ng-controller="home" ng-cloak class="ng-cloak">
<p>The ID is {{greeting.id}}</p>
<p>The content is {{greeting.content}}</p>
</div>
</div>
<script src="js/angular-bootstrap.js" type="text/javascript"></script>
<script src="js/hello.js"></script>
</body>
</html>
它相当简洁,因为它只会显示 "Hello World"。
显著特性包括:
在 <head>
中导入了一些 CSS,其中一个占位符文件尚不存在,但命名很有提示性("angular-bootstrap.css"),另一个内联样式表定义了 "ng-cloak" 类。
"ng-cloak" 类应用于内容 <div>
,以便在 Angular JS 处理动态内容之前将其隐藏(这可以防止初次页面加载时的“闪烁”)。
<body>
被标记为 ng-app="hello"
,这意味着我们需要定义一个 JavaScript 模块,Angular 会将其识别为一个名为 "hello" 的应用。
所有 CSS 类(除了 "ng-cloak")都来自 Twitter Bootstrap。一旦我们正确设置了样式表,它们会使页面看起来很美观。
问候语中的内容使用 handlebar 语法标记,例如 {{greeting.content}}
,这将在稍后由 Angular 填充(根据外层 <div>
上的 ng-controller
指令,使用一个名为 "home" 的“控制器”)。
Angular JS(和 Twitter Bootstrap)包含在 <body>
的底部,这样浏览器可以在它们被处理之前先处理所有 HTML。
我们还包含一个独立的 "hello.js" 文件,我们将在其中定义应用的行为。
我们稍后会创建脚本和样式表资源文件,但现在可以忽略它们尚不存在的事实。
添加首页文件后,你的应用就可以在浏览器中加载了(尽管它还没有太多功能)。在命令行中,你可以这样做:
$ mvn spring-boot:run
并在浏览器中访问 http://localhost:8080。加载首页时,应该会弹出一个浏览器对话框,要求输入用户名和密码(用户名是 "user",密码会在启动时的控制台日志中打印)。实际上还没有内容,所以成功认证后,你应该会看到一个空白页面,上面有“Greeting”标题。
提示:如果你不喜欢从控制台日志中抓取密码,只需将以下内容添加到 "application.properties" 文件(在 "src/main/resources" 目录下):
security.user.password=password
(并选择你自己的密码)。我们在示例代码中使用了 "application.yml" 来完成此操作。
在 IDE 中,只需运行应用类中的 main()
方法即可(如果上面你使用了 "curl" 命令,那么只有一个类,它被称为 UiApplication
)。
要打包并作为独立 JAR 运行,你可以这样做:
$ mvn package
$ java -jar target/*.jar
关于 Angular 和其他前端技术的入门教程通常直接从互联网引入库资源(例如,Angular JS 官网本身推荐从 Google CDN 下载)。但我们不会这样做,而是通过连接这些库中的多个文件来生成 "angular-bootstrap.js" 资源。这对于让应用工作起来并不是严格必需的,但对于生产应用来说,合并脚本以避免浏览器和服务器(或内容分发网络)之间的频繁通信是最佳实践。由于我们不会修改或定制 CSS 样式表,因此也无需生成 "angular-bootstrap.css",我们也可以直接使用 Google CDN 上的静态资源。然而,在实际应用中,我们几乎肯定会想要修改样式表,并且我们不会想手动编辑 CSS 源文件,所以我们会使用更高级别的工具(例如 Less 或 Sass),因此我们也将使用其中一个。
有很多不同的方法可以做到这一点,但出于本文的目的,我们将使用 wro4j,这是一个基于 Java 的工具链,用于预处理和打包前端资源。它可以在任何 Servlet 应用中用作 JIT(Just in Time,即时)Filter
,但也对 Maven 和 Eclipse 等构建工具有很好的支持,这就是我们打算使用它的方式。因此,我们将构建静态资源文件并将它们打包到我们的应用 JAR 中。
旁注:对于硬核前端开发者来说,wro4j 可能不是首选工具——他们可能会使用基于 Node 的工具链,配合 bower 和/或 grunt。这些工具无疑非常优秀,并且在互联网上有很多详细的介绍,所以如果你更喜欢它们,请随意使用。如果你只是将这些工具链的输出文件放在 "src/main/resources/static" 目录下,一切都会正常工作。我发现 wro4j 更方便,因为我不是硬核前端开发者,而且我知道如何使用基于 Java 的工具。
为了在构建时创建静态资源,我们向 Maven pom.xml
文件添加一些“魔法”配置(它相当冗长,但只是样板代码,因此可以提取到 Maven 中的父 pom 或 Gradle 中的共享任务或插件中)。
<build>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
<resource>
<directory>${project.build.directory}/generated-resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<!-- Serves *only* to filter the wro.xml so it can get an absolute
path for the project -->
<id>copy-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/wro</outputDirectory>
<resources>
<resource>
<directory>src/main/wro</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>ro.isdc.wro4j</groupId>
<artifactId>wro4j-maven-plugin</artifactId>
<version>1.7.6</version>
<executions>
<execution>
<phase>generate-resources</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<configuration>
<wroManagerFactory>ro.isdc.wro.maven.plugin.manager.factory.ConfigurableWroManagerFactory</wroManagerFactory>
<cssDestinationFolder>${project.build.directory}/generated-resources/static/css</cssDestinationFolder>
<jsDestinationFolder>${project.build.directory}/generated-resources/static/js</jsDestinationFolder>
<wroFile>${project.build.directory}/wro/wro.xml</wroFile>
<extraConfigFile>${basedir}/src/main/wro/wro.properties</extraConfigFile>
<contextFolder>${basedir}/src/main/wro</contextFolder>
</configuration>
<dependencies>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>angularjs</artifactId>
<version>1.3.8</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
你可以将其原封不动地复制到你的 POM 文件中,或者如果你正对照着Github 中的源码阅读,可以只浏览一下。主要要点是:
我们包含了一些 webjars 库作为依赖项(jquery 和 bootstrap 用于 CSS 和样式设计,Angular JS 用于业务逻辑)。这些 jar 文件中的一些静态资源将包含在我们生成的 "angular-bootstrap.*" 文件中,但 jar 文件本身不需要与应用打包在一起。
Twitter Bootstrap 依赖于 jQuery,所以我们也包含了它。不使用 Bootstrap 的 Angular JS 应用则不需要 jQuery,因为 Angular 有自己版本的从 jQuery 需要的功能。
生成的资源将位于 "target/generated-resources" 目录下,并且由于它在 <resources/>
部分中声明,它们将被打包到项目的输出 JAR 文件中,并在 IDE 的类路径中可用(只要我们使用 Maven 工具,例如 Eclipse 中的 m2e)。
wro4j-maven-plugin 插件具有一些 Eclipse 集成功能,你可以从 Eclipse Marketplace 安装(如果这是你第一次尝试,可以稍后再试——它不是完成应用所必需的)。如果你这样做,那么 Eclipse 将监视源文件并在它们更改时重新生成输出文件。如果在调试模式下运行,则更改可以立即在浏览器中重新加载。
wro4j 由一个 XML 配置文件控制,该文件不了解你的构建类路径,只理解绝对文件路径,因此我们必须创建一个绝对文件位置并将其插入到 wro.xml
文件中。为此,我们使用 Maven 资源过滤,这就是为什么有一个明确的 "maven-resources-plugin" 声明的原因。
这就是我们需要对 POM 文件进行的所有更改。剩下的就是添加 wro4j 构建文件了,我们指定它们将位于 "src/main/wro" 目录下。
如果你查看Github 中的源代码,会看到只有 3 个文件(其中一个为空,留待后续定制)。
wro.properties
是 wro4j 预处理和渲染引擎的配置文件。你可以用它来开启或关闭工具链的各个部分。在本例中,我们用它来从 Less 编译 CSS 并压缩 JavaScript,最终将所有所需库的源代码合并到两个文件中。
preProcessors=lessCssImport
postProcessors=less4j,jsMin
wro.xml
声明了一个名为 "angular-bootstrap" 的单一资源“组”,这将成为生成的静态资源的基础名称。它包含对我们添加的 webjars 中 <css>
和 <js>
元素的引用,以及对本地源文件 main.less
的引用。
<groups xmlns="http://www.isdc.ro/wro">
<group name="angular-bootstrap">
<css>webjar:bootstrap/3.2.0/less/bootstrap.less</css>
<css>file:${project.basedir}/src/main/wro/main.less</css>
<js>webjar:jquery/2.1.1/jquery.min.js</js>
<js>webjar:bootstrap/3.2.0/bootstrap.js</js>
<js>webjar:angularjs/1.3.8/angular.min.js</js>
</group>
</groups>
main.less
是空的,但可用于定制外观,更改 Twitter Bootstrap 中的默认设置。例如,要将颜色从默认蓝色更改为浅粉色,你可以添加一行代码:@brand-primary: #de8579;
。
将这些文件复制到你的项目中并运行 "mvn package",你应该会看到 "bootstrap-angular.*" 资源出现在你的 JAR 文件中。如果现在运行应用,应该会看到 CSS 生效,但业务逻辑和导航仍然缺失。
让我们创建 "hello" 应用(在 "src/main/resources/static/js/hello.js" 目录下),这样我们 "index.html" 底部的 <script/>
标签就能在正确的位置找到它。
一个最简单的 Angular JS 应用看起来像这样:
angular.module('hello', [])
.controller('home', function($scope) {
$scope.greeting = {id: 'xxx', content: 'Hello World!'}
})
应用的名称是 "hello",它有一个空的(且冗余的)"config" 和一个空的“控制器”,名为 "home"。当我们加载 "index.html" 时,"home" 控制器将被调用,因为我们在内容 <div>
上使用了 ng-controller="home"
指令。
注意,我们向控制器函数中注入了一个神奇的 $scope
(Angular 通过命名约定进行依赖注入,并识别你的函数参数名称)。然后,在函数内部使用 $scope
为该控制器负责的 UI 元素设置内容和行为。
如果你将该文件添加到 "src/main/resources/static/js" 目录下,你的应用现在应该是安全且功能齐全的,并且会显示 "Hello World!"。greeting
对象的内容由 Angular 使用 handlebar 占位符 {{greeting.id}}
和 {{greeting.content}}
在 HTML 中渲染。
到目前为止,我们有一个应用,其中的问候语是硬编码的。这对于学习各部分如何协同工作很有用,但实际上我们期望内容来自后端服务器,所以让我们创建一个 HTTP 端点,用于获取问候语。在你的应用类中(位于 "src/main/java/demo" 目录下),添加 @RestController
注解并定义一个新的 @RequestMapping
:
@SpringBootApplication
@RestController
public class UiApplication {
@RequestMapping("/resource")
public Map<String,Object> home() {
Map<String,Object> model = new HashMap<String,Object>();
model.put("id", UUID.randomUUID().toString());
model.put("content", "Hello World");
return model;
}
public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}
}
注意:根据你创建新项目的方式,它可能不叫
UiApplication
,并且可能包含@EnableAutoConfiguration @ComponentScan @Configuration
注解而不是@SpringBootApplication
。
运行该应用并尝试使用 curl 访问 "/resource" 端点,你会发现它默认是安全的。
$ curl localhost:8080/resource
{"timestamp":1420442772928,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/resource"}
那么,让我们在浏览器中获取该消息。修改 "home" 控制器,使用 XHR 加载受保护的资源:
angular.module('hello', [])
.controller('home', function($scope, $http) {
$http.get('/resource/').success(function(data) {
$scope.greeting = data;
})
});
我们注入了一个 $http
服务,这是 Angular 作为核心功能提供的,我们用它来 GET 我们的资源。成功时,Angular 将响应体中的 JSON 传递回一个回调函数。
再次运行应用(或者只需在浏览器中重新加载首页),你将看到带有唯一 ID 的动态消息。所以,即使资源受保护且你不能直接使用 curl 访问它,浏览器仍然能够访问内容。我们用不到一百行代码就构建了一个安全的单页应用!
## 工作原理注意:更改静态资源后,你可能需要强制浏览器重新加载。在 Chrome(以及安装插件的 Firefox)中,你可以使用“开发者工具”(F12),这可能就足够了。或者你可能需要使用 CTRL+F5。
如果你使用一些开发者工具(通常按 F12 打开,Chrome 默认支持,Firefox 需要插件),可以在浏览器中看到浏览器和后端之间的交互。以下是一个总结:
动词 | 路径 | 状态 | 响应 |
---|---|---|---|
GET | / | 401 | 浏览器提示认证 |
GET | / | 200 | index.html |
GET | /css/angular-bootstrap.css | 200 | Twitter bootstrap CSS |
GET | /js/angular-bootstrap.js | 200 | Bootstrap 和 Angular JS |
GET | /js/hello.js | 200 | 应用逻辑 |
GET | /resource | 200 | JSON 格式问候语 |
你可能不会看到 401 状态码,因为浏览器将首页加载视为一次单独的交互;你可能会看到 "/resource" 的 2 次请求,因为存在 CORS 协商。
仔细查看请求,你会发现所有请求都有一个 "Authorization" 请求头,类似这样:
Authorization: Basic dXNlcjpwYXNzd29yZA==
浏览器在每次请求时都会发送用户名和密码(所以记住在生产环境中只使用 HTTPS)。这与 "Angular" 没有关系,因此它适用于你选择的 JavaScript 框架或非框架。
表面上看,我们似乎做得相当不错,它简洁、易于实现,所有数据都受到秘密密码的保护,而且即使我们改变前端或后端技术,它也能继续工作。但存在一些问题。
基本认证仅限于用户名和密码认证。
认证界面无处不在,但很丑陋(浏览器弹窗)。
没有针对跨站请求伪造(CSRF)的保护措施。
CSRF 暂时对我们当前的应用来说不是问题,因为它只需要 GET 后端资源(即服务器端没有状态改变)。一旦你的应用中有 POST、PUT 或 DELETE 操作,按照任何合理的现代标准衡量,它就不再安全了。
在本系列的下一篇文章中,我们将扩展应用以使用基于表单的认证,这比 HTTP Basic 灵活得多。一旦有了表单,我们将需要 CSRF 防护,Spring Security 和 Angular 都提供了一些不错的开箱即用功能来帮助解决这个问题。剧透:我们将需要使用 HttpSession
。
致谢:我要感谢所有帮助我开发本系列文章的人,特别是 Rob Winch 和 Thorsten Spaeth,感谢他们仔细审阅了文章和源代码,并教给我一些我以前不知道的技巧,甚至包括我认为自己最熟悉的部分。