领先一步
VMware 提供培训和认证,助您加速进步。
了解更多注意:本文的源代码和测试将继续演进,但文本的更改在此处不再维护。请参阅教程版本以获取最新内容。
在本文中,我们将展示 Spring Security、Spring Boot 和 Angular JS 协同工作的一些优秀功能,以提供令人愉悦且安全的iram体验。它应该对 Spring 和 Angular JS 的初学者来说是易于理解的,但也有很多细节对两者的专家都将很有用。这实际上是关于 Spring Security 和 Angular JS 系列文章的第一篇,每一篇都会逐步介绍新功能。我们将在第二篇及后续篇章中改进应用程序,但在此之后的主要更改将是架构性的而非功能性的。
HTML5、丰富的浏览器功能和“单页应用程序”是现代开发者的极其宝贵的工具,但任何有意义的交互都将涉及后端服务器,因此除了静态内容(HTML、CSS 和 JavaScript)之外,我们还需要一个后端服务器。后端服务器可以扮演许多角色中的任何一个或所有角色:提供静态内容、有时(如今不那么频繁)渲染动态 HTML、对用户进行身份验证、保护对受保护资源的访问,以及(最后但同样重要的是)通过 HTTP 和 JSON(有时称为 REST API)与浏览器中的 JavaScript 进行交互。
Spring 一直是构建后端功能(尤其是在企业中)的流行技术,并且随着 Spring Boot 的出现,一切都变得前所未有的简单。让我们来看看如何使用 Spring Boot、Angular JS 和 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>,以便在 Angular JS 有机会处理动态内容之前隐藏它(这可以防止初始页面加载时的“闪烁”)。
<body> 被标记为 ng-app="hello",这意味着我们需要定义一个 JavaScript 模块,Angular 会将其识别为一个名为“hello”的应用程序。
所有 CSS 类("ng-cloak" 除外)都来自 Twitter Bootstrap。一旦我们设置好正确的样式表,它们将使一切看起来都很漂亮。
问候语中的内容使用 handlebars 标记,例如 {{greeting.content}},它稍后将由 Angular 填充(根据周围 <div> 上的 ng-controller 指令,使用一个名为“home”的“控制器”)。
Angular JS(和 Twitter Bootstrap)包含在 <body> 的底部,以便浏览器可以在处理之前处理所有 HTML。
我们还包含一个单独的 "hello.js",我们将在此定义应用程序行为。
我们将在稍后创建脚本和样式表资源,但现在我们可以忽略它们不存在的事实。
添加主页文件后,您的应用程序即可在浏览器中加载(即使它目前功能不多)。在命令行上,您可以这样做
$ mvn spring-boot:run
并在浏览器中访问 https://: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",我们也可以像 CSS 一样直接使用 Google CDN 的静态资源。但是,在实际应用程序中,我们几乎肯定会想要修改样式表,并且不希望手动编辑 CSS 源文件,因此我们将使用一个更高级别的工具(例如 Less 或 Sass),所以我们也打算使用一个。
有许多不同的方法可以做到这一点,但就本文而言,我们将使用 wro4j,这是一个基于 Java 的工具链,用于预处理和打包前端资源。它可以作为 JIT(即时)Filter 用于任何 Servlet 应用程序,但它也对 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>
您可以将此 verbatim 复制到您的 POM 中,或者如果您正在从 Github 中的源代码跟进,只需扫描它即可。主要要点是
我们将一些 webjars 库作为依赖项包含(jquery 和 bootstrap 用于 CSS 和样式,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" 的单一资源“组”,最终成为生成的静态资源的基础名称。它包含对我们添加的 webjar 中的 <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",您应该会在 JAR 文件中看到 "bootstrap-angular.*" 资源。如果您现在运行应用程序,您应该会看到 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 来为该控制器负责的用户界面元素设置内容和行为。
如果您将该文件添加到 "src/main/resources/static/js" 下,您的应用程序现在应该安全且功能完善,并且会显示 "Hello World!"。greeting 由 Angular 在 HTML 中使用 handlebars 占位符 {{greeting.id}} 和 {{greeting.content}} 进行渲染。
到目前为止,我们的应用程序有一个硬编码的问候语。这对于学习事物如何组合在一起很有用,但实际上我们期望内容来自后端服务器,所以让我们创建一个 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" 的请求,因为有一个 CORS 协商。
更仔细地查看请求,您会发现所有请求都包含一个 "Authorization" 标头,如下所示
Authorization: Basic dXNlcjpwYXNzd29yZA==
浏览器将用户名和密码随每个请求一起发送(因此请记住在生产环境中仅使用 HTTPS)。那里面没有“Angular”的东西,所以它适用于您选择的 JavaScript 框架或非框架。
表面上看,我们似乎做得相当好,它简洁、易于实现,我们的所有数据都受秘密密码保护,并且即使我们更改了前端或后端技术,它仍然有效。但存在一些问题。
基本身份验证仅限于用户名和密码验证。
身份验证 UI 无处不在但很难看(浏览器对话框)。
没有针对跨站请求伪造(CSRF)的保护。
CSRF 对我们现有的应用程序来说实际上不是问题,因为它只需要 GET 后端资源(即,服务器中没有改变状态)。一旦您的应用程序中有 POST、PUT 或 DELETE,按照任何合理的现代标准,它就不再安全了。
在本系列下一篇文章中,我们将扩展应用程序以使用基于表单的身份验证,这比 HTTP Basic 更加灵活。一旦有了表单,我们就需要 CSRF 保护,Spring Security 和 Angular 都有一些很好的开箱即用的功能来帮助实现这一点。剧透:我们将需要使用 HttpSession。
致谢:我想感谢所有帮助我开发本系列文章的人,特别是 Rob Winch 和 Thorsten Spaeth,感谢他们仔细审阅文章和源代码,并教会了我一些我不知道的技巧,即使是在那些我以为我最熟悉的方面。