Spring 和 AngularJS:安全的单页应用程序

工程 | Dave Syer | 2015年1月12日 | ...

注意:此博客的源代码和测试仍在不断发展,但此处未维护文本更改。请参阅 教程版本 获取最新内容。

在本文中,我们将展示 Spring Security、Spring Boot 和 AngularJS 协同工作以提供愉悦且安全的用户体验的一些优秀特性。对于 Spring 和 AngularJS 的初学者来说,它应该是易于理解的,但也包含许多对任何一方的专家都很有用的细节。这实际上是关于 Spring Security 和 AngularJS 的一系列文章中的第一篇,每一篇都会陆续介绍新的特性。我们将在 第二篇 及后续文章中改进应用程序,但此后主要更改是架构上的而非功能上的。

Spring 和单页应用程序

HTML5、丰富的基于浏览器的功能和“单页应用程序”对于现代开发人员来说是极其有价值的工具,但任何有意义的交互都将涉及后端服务器,因此除了静态内容(HTML、CSS 和 JavaScript)之外,我们还需要一个后端服务器。后端服务器可以扮演多种角色中的任何一种或所有角色:提供静态内容,有时(但现在不那么常见)渲染动态 HTML,验证用户身份,保护对受保护资源的访问,以及(最后但并非最不重要)通过 HTTP 和 JSON 与浏览器中的 JavaScript 交互(有时称为 REST API)。

Spring 一直以来都是构建后端功能(尤其是在企业中)的流行技术,并且随着 Spring Boot 的出现,事情从未如此简单。让我们看看如何使用 Spring Boot、AngularJS 和 Twitter Bootstrap 从零开始构建新的单页应用程序。选择该特定堆栈没有特别的理由,但它非常流行,尤其是在企业 Java 商店中的核心 Spring 用户中,因此这是一个值得的起点。

创建一个新项目

我们将详细介绍创建此应用程序的过程,以便任何不完全熟悉 Spring 和 Angular 的人都能理解正在发生的事情。如果您希望直接进入主题,可以 跳到最后,查看应用程序的工作方式以及所有组件如何协同工作。创建新项目有多种选择

我们将构建的完整项目的源代码位于 Github 这里,因此如果您愿意,可以直接克隆项目并直接从中工作。然后跳转到 下一节

使用 Curl

开始最简单的方法是通过 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 Boot CLI 创建相同的项目,如下所示

$ spring init --dependencies web,security ui/ && cd ui

然后跳转到 下一节

使用 Initializr 网站

如果您愿意,也可以从 Spring Boot Initializr 直接获取相同的代码作为 .zip 文件。只需在浏览器中打开它并选择依赖项“Web”和“Security”,然后单击“Generate Project”。.zip 文件在根目录中包含一个标准的 Maven 或 Gradle 项目,因此您可能需要在解压缩之前创建一个空目录。然后跳转到 下一节

使用 Spring Tool Suite

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 源代码,因此我们将使用更高级的工具(例如 LessSass),所以我们也将会使用一个。

有很多不同的方法可以做到这一点,但出于本文的目的,我们将使用 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”中。

Wro4j 源文件

如果您查看 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 生效,但业务逻辑和导航仍然缺失。

创建 Angular 应用程序

让我们创建“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"}

从 Angular 加载动态资源

因此,让我们在浏览器中获取该消息。修改“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 WinchThorsten Spaeth 仔细审查文章和源代码,并教我一些我甚至不知道的技巧,即使是我认为自己最熟悉的那些部分。

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加快您的进度。

了解更多

获取支持

Tanzu Spring在一个简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部