领先一步
VMware 提供培训和认证,以加快您的进度。
了解更多注意:此博客的源代码和测试仍在不断发展,但此处未维护文本更改。请参阅 教程版本 获取最新内容。
在本文中,我们将展示 Spring Security、Spring Boot 和 AngularJS 协同工作以提供愉悦且安全的用户体验的一些优秀特性。对于 Spring 和 AngularJS 的初学者来说,它应该是易于理解的,但也包含许多对任何一方的专家都很有用的细节。这实际上是关于 Spring Security 和 AngularJS 的一系列文章中的第一篇,每一篇都会陆续介绍新的特性。我们将在 第二篇 及后续文章中改进应用程序,但此后主要更改是架构上的而非功能上的。
HTML5、丰富的基于浏览器的功能和“单页应用程序”对于现代开发人员来说是极其有价值的工具,但任何有意义的交互都将涉及后端服务器,因此除了静态内容(HTML、CSS 和 JavaScript)之外,我们还需要一个后端服务器。后端服务器可以扮演多种角色中的任何一种或所有角色:提供静态内容,有时(但现在不那么常见)渲染动态 HTML,验证用户身份,保护对受保护资源的访问,以及(最后但并非最不重要)通过 HTTP 和 JSON 与浏览器中的 JavaScript 交互(有时称为 REST API)。
Spring 一直以来都是构建后端功能(尤其是在企业中)的流行技术,并且随着 Spring Boot 的出现,事情从未如此简单。让我们看看如何使用 Spring Boot、AngularJS 和 Twitter Bootstrap 从零开始构建新的单页应用程序。选择该特定堆栈没有特别的理由,但它非常流行,尤其是在企业 Java 商店中的核心 Spring 用户中,因此这是一个值得的起点。
我们将详细介绍创建此应用程序的过程,以便任何不完全熟悉 Spring 和 Angular 的人都能理解正在发生的事情。如果您希望直接进入主题,可以 跳到最后,查看应用程序的工作方式以及所有组件如何协同工作。创建新项目有多种选择
我们将构建的完整项目的源代码位于 Github 这里,因此如果您愿意,可以直接克隆项目并直接从中工作。然后跳转到 下一节。
开始最简单的方法是通过 Spring Boot Initializr。例如,在类 Unix 系统上使用 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>
,以便在 AngularJS 有机会处理它之前隐藏动态内容(这可以防止初始页面加载期间出现“闪烁”)。
<body>
被标记为ng-app="hello"
,这意味着我们需要定义一个 JavaScript 模块,Angular 将其识别为名为“hello”的应用程序。
所有 CSS 类(除了“ng-cloak”)都来自 Twitter Bootstrap。一旦我们设置了正确的样式表,它们将使页面看起来更漂亮。
问候语中的内容使用 handlebars 进行标记,例如{{greeting.content}}
,稍后将由 Angular(根据<div>
周围的ng-controller
指令使用名为“home”的“控制器”)填充。
AngularJS(和 Twitter Bootstrap)包含在<body>
的底部,以便浏览器可以在处理之前处理所有 HTML。
我们还包含一个单独的“hello.js”,我们将在其中定义应用程序行为。
我们将在稍后创建脚本和样式表资源,但现在我们可以忽略它们不存在的事实。
添加主页文件后,您的应用程序即可在浏览器中加载(尽管它目前功能不多)。您可以在命令行中执行此操作
$ mvn spring-boot:run
然后在浏览器中访问 https://127.0.0.1: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 可能不是首选工具——他们可能会使用基于节点的工具链,以及 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 库作为依赖项包含在内(用于 CSS 和样式的 jquery 和 bootstrap,以及用于业务逻辑的 Angular JS)。这些 jar 文件中的一些静态资源将包含在我们生成的“angular-bootstrap.*”文件中,但 jar 文件本身不需要与应用程序一起打包。
Twitter Bootstrap 依赖于 jQuery,因此我们也将其包含在内。一个不使用 Bootstrap 的 Angular JS 应用程序不需要这个,因为 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”的空“controller”。当我们加载“index.html”时,将调用“home”控制器,因为我们已经使用ng-controller="home"
装饰了内容<div>
。
请注意,我们将一个神奇的$scope
注入到控制器函数中(Angular 通过命名约定进行依赖注入,并识别函数参数的名称)。然后在函数内部使用$scope
来设置此控制器负责的 UI 元素的内容和行为。
如果您将该文件添加到“src/main/resources/static/js”下,您的应用程序现在应该安全且功能齐全,并且会显示“Hello World!”。greeting
使用句柄占位符{{greeting.id}}
和{{greeting.content}}
在 HTML 中由 Angular 呈现。
到目前为止,我们有一个带有硬编码问候语的应用程序。这对于学习事物如何组合在一起很有用,但实际上我们期望内容来自后端服务器,因此让我们创建一个 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
。
运行该应用程序并尝试卷曲“/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 作为核心功能提供,并用它来获取我们的资源。Angular 将来自响应主体的 JSON 传递给我们,然后传递给成功回调函数。
再次运行应用程序(或只是重新加载浏览器中的主页),您将看到带有唯一 ID 的动态消息。因此,即使资源受到保护并且您无法直接卷曲它,浏览器也能够访问内容。我们在不到一百行代码中就拥有了一个安全的单页应用程序!
## 工作原理注意:您可能需要在更改静态资源后强制浏览器重新加载它们。在 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”的请求,这是因为存在 CORS 协商。
仔细查看请求,您会发现所有请求都包含一个“Authorization”标头,类似于:
Authorization: Basic dXNlcjpwYXNzd29yZA==
浏览器在每次请求中都发送用户名和密码(因此请记住在生产环境中仅使用 HTTPS)。这与 Angular 无关,因此它适用于您选择的 JavaScript 框架或非框架。
表面上看,我们做得很好,简洁易实现,所有数据都由秘密密码保护,即使我们更改前端或后端技术,它仍然可以工作。但有一些问题。
基本身份验证仅限于用户名和密码身份验证。
身份验证 UI 普遍存在但难看(浏览器对话框)。
没有针对 跨站请求伪造 (CSRF) 的保护。
就目前而言,CSRF 并不是我们应用程序中的一个真正问题,因为它只需要获取后端资源(即服务器中的状态不会更改)。一旦您的应用程序中包含 POST、PUT 或 DELETE 请求,按照任何合理的现代标准衡量,它就不再安全了。
在本系列的 下一篇文章 中,我们将扩展应用程序以使用基于表单的身份验证,这比 HTTP 基本身份验证灵活得多。一旦我们有了表单,我们就需要 CSRF 保护,Spring Security 和 Angular 都有一些不错的开箱即用功能可以帮助解决这个问题。剧透:我们将需要使用HttpSession
。
致谢:我要感谢所有帮助我开发本系列的人,特别是感谢 Rob Winch 和 Thorsten Spaeth 仔细审查文章和源代码,并教我一些我甚至不知道的技巧,即使是我认为自己最熟悉的那些部分。