Spring-Security5-反应式应用实用指南-全-

Spring Security5 反应式应用实用指南(全)

原文:zh.annas-archive.org/md5/6DEAFFE8EE2C8DC4EDE2FE79BBA87B88

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

安全是创建应用程序最困难和高压的问题之一。当您必须将其与现有代码、新技术和其他框架集成时,正确保护应用程序的复杂性会增加。本书将向读者展示如何使用经过验证的 Spring Security 框架轻松保护他们的 Java 应用程序,这是一个高度可定制和强大的身份验证和授权框架。

Spring Security 是一个著名的、成熟的 Java/JEE 框架,可以为您的应用程序提供企业级的安全功能,而且毫不费力。它还有一些模块,可以让我们集成各种认证机制,我们将在本书中使用实际编码来深入研究每一个认证机制。

许多示例仍将使用 Spring MVC web 应用程序框架来解释,但仍将具有响应式编程的特色。

响应式编程正在受到关注,本书将展示 Spring Security 与 Spring WebFlux web 应用程序框架的集成。除了响应式编程,本书还将详细介绍其他 Spring Security 功能。

最后,我们还将介绍市场上可用的一些产品,这些产品可以与 Spring Security 一起使用,以实现现代应用程序中所需的一些安全功能。这些产品提供了新的/增强的安全功能,并且在各个方面与 Spring Security 协同工作。其中一些产品也得到了 Spring 社区的全力支持。

这本书适合谁

这本书适合以下任何人:

  • 任何希望将 Spring Security 集成到他们的应用程序中的 Spring Framework 爱好者

  • 任何热衷的 Java 开发人员,希望开始使用 Spring Framework 的核心模块之一,即 Spring Security

  • 有经验的 Spring Framework 开发人员,希望能够亲自动手使用最新的 Spring Security 模块,并且也想开始使用响应式编程范式编写应用程序的人

本书涵盖了什么

[第一章],Spring 5 和 Spring Security 5 概述,向您介绍了新的应用程序要求,然后介绍了响应式编程概念。它涉及应用程序安全以及 Spring Security 在应用程序中解决安全问题的方法。该章节随后更深入地介绍了 Spring Security,最后解释了本书中示例的结构。

[第二章],深入研究 Spring Security,深入探讨了核心 Spring Security 的技术能力,即身份验证和授权。然后,该章节通过一些示例代码让您动手实践,我们将使用 Spring Security 设置一个项目。然后,在适当的时候,向您介绍了本书中将解释代码示例的方法。

[第三章],使用 SAML、LDAP 和 OAuth/OIDC 进行身份验证,向您介绍了三种身份验证机制,即 SAML、LDAP 和 OAuth/OIDC。这是两个主要章节中的第一个,我们将通过实际编码深入研究 Spring Security 支持的各种身份验证机制。我们将使用简单的示例来解释每种身份验证机制,以涵盖主题的要点,并且我们将保持示例简单以便易于理解。

第四章,使用 CAS 和 JAAS 进行身份验证,向您介绍了企业中非常普遍的另外两种身份验证机制——CAS 和 JAAS。这是两个主要章节中的第二个,类似于第三章使用 SAML、LDAP 和 OAuth/OIDC 进行身份验证,最初将涵盖这些身份验证机制的理论方面。本章通过使用 Spring Security 实现一个完整的示例来结束这个主题。

第五章,与 Spring WebFlux 集成,向您介绍了作为 Spring 5 的一部分引入的新模块之一——Spring WebFlux。Spring WebFlux 是 Spring 生态系统中的 Web 应用程序框架,从头开始构建,完全是响应式的。本章将介绍 Spring Security 的响应式部分,并详细介绍 Spring WebFlux 框架本身。首先,我们将通过一个示例向您介绍 Spring WebFlux,然后我们将在基础应用程序上构建额外的技术能力。

第六章,REST API 安全,首先介绍了有关 REST 和 JWT 的一些重要概念。然后介绍了 OAuth 的概念,并使用实际编码示例解释了简单和高级的 REST API 安全,重点是利用 Spring Framework 中的 Spring Security 和 Spring Boot 模块。示例将使用 OAuth 协议,并将使用 Spring Security 充分保护 REST API。除此之外,JWT 将用于在服务器和客户端之间交换声明。

第七章,Spring 安全附加组件,介绍了许多产品(开源和付费版本),可以考虑与 Spring Security 一起使用。这些产品是强有力的竞争者,可以用来实现您在应用程序中寻找的技术能力,以满足各种安全要求。我们将通过概述应用程序中需要解决的技术能力的要点来向您介绍产品,然后再看一下相关产品,并解释它如何提供您需要的解决方案。

为了充分利用本书

  1. 本书包含许多示例,全部在 Macintosh 机器上使用 IDE(IntelliJ)编码和执行。因此,为了轻松跟随示例,使用 macOS 和 IntelliJ 将会大有帮助。但是,所有代码都可以在 Macintosh、Windows 和 Linux 系统上执行。

  2. 需要具备基本到中级的使用 Java 和 Spring Framework 构建应用程序的经验,才能轻松阅读本书。

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packtpub.com

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩软件解压或提取文件夹。

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Spring-Security-5-for-Reactive-Applications。如果代码有更新,将在现有的 GitHub 存储库中进行更新。

我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/HandsOnSpringSecurity5forReactiveApplications_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:“Flux<T>是一个带有基本流操作并支持0..n个元素的Publisher<T>。”

代码块设置如下:

public abstract class Flux<T>
    extends Object
    implements Publisher<T>

任何命令行输入或输出都是这样写的:

curl http://localhost:8080/api/movie -v -u admin:password

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会在文本中出现。这是一个例子:“输入用户名为admin,密码为password,然后点击“登录”。”

警告或重要说明看起来像这样。

提示和技巧看起来像这样。

第一章:Spring 5 和 Spring Security 5 概述

本书希望读者熟悉 Spring 框架(任何版本)和 Spring Security(任何版本)。这是一个引子章节,介绍了一些最重要的概念;我们将在后续章节中扩展这些概念。

本章将向你介绍新的应用需求,然后介绍反应式编程概念。它涉及应用安全以及 Spring Security 如何解决应用程序中的安全问题。

我们将继续使用 Spring Security,然后通过解释本章中示例的结构来结束本章。这非常重要,因为我希望读者在引入新概念时感到舒适。

在本章中,我们将涵盖以下主题:

  • 新一代应用需求

  • 反应式编程

  • 反应式应用

  • Spring 框架

  • Java 中的反应式景观

  • Spring 框架和反应式应用程序

  • 应用安全

  • Spring Security

  • Spring Security 的核心功能

  • Spring Security 5 的新功能

  • Spring Security 的工作原理

  • 核心 Spring Security 模块

示例的结构

重要的是,你要理解我们在这本书中将如何使用示例。由于本书试图详细介绍 Spring Security 5 及其反应性方面,我们不会在整本书中只有一个用例。相反,我们将不断创建小型项目,以帮助你理解所涵盖的每个核心概念。以下是本书中代码库的一些重要方面:

  • 大多数概念将使用独立的 Spring Boot 项目进行介绍。

  • 有时,我们将使用著名的 Spring Initializr (start.spring.io/)来启动我们的示例 Spring Boot 应用程序。在其他情况下,我们将从我们已经拥有的基础项目开始,并通过代码引入更多概念。

  • 通常,我们将使用 Java 配置。有时,我们可能会使用基于 XML 的配置。

  • 我们将尽可能简化示例,以便不会偏离引入的核心概念。

  • 尽管本书侧重于反应式应用程序,但我们不会在每次引入时都进行覆盖。有时,我们只会进行普通的命令式编程,因为了解反应式编程并在需要时使用它更为重要。并不是说我们必须在所有可能的地方都使用反应式代码,只需在适当的地方使用即可。

  • 我们将为所有项目使用 VS Code,并充分利用 VS Code 中的扩展。我们还将使用 Spring Initializr 扩展,而不是使用在线 Spring Initializr。

  • 在本书中,我们将大部分时间使用 Maven。可能会有一种情况,我们会尝试使用 Gradle。

  • 有时,我们可能会使用 IntelliJ IDE,你会看到一些屏幕截图显示这一点。

  • 我们将使用最新的 Spring Boot 发布版本,即2.0.0. RELEASE。这是撰写本书时 Spring Boot 的最新发布版本。

新一代应用需求

以下是一些核心的新应用需求:

  • 高度可扩展:社交平台在过去十年里呈指数级增长,人们比以往任何时候都更懂技术。

  • 弹性、容错和高可用性:在现代时代,企业不愿意接受应用程序的停机时间;即使是几秒钟的停机时间也会给许多大型企业带来巨大的损失。

  • 高性能:如果你的网站速度慢,人们就有离开并寻找替代方案的倾向。人们的注意力很短,如果你的网站表现不佳,他们就不会停留或回来。

  • 超个性化:用户需要个性化的网站而不是通用的网站,这给服务器带来了巨大的压力,需要实时进行许多密集的分析。

随着技术进入了每个人的手中(以某种形式,大多数人都在使用技术),用户对隐私政策和应用程序安全非常熟悉。他们了解大多数安全要求,公司花时间教育用户安全的重要性以及他们应该如何寻找应用程序中的安全漏洞。你可能已经知道,如果一个网站使用 HTTP 而不是 HTTPS(SSL)和 Chrome 标签,这些网站在地址栏中会清楚地显示给用户为不安全。随着越来越多的人对技术有了更多了解,这些方面在大多数用户中都是众所周知的,安全已成为 IT 领域中最受关注的话题之一。

另一个重要方面是数据隐私。一些用户不担心分享他们的数据,但有些用户则非常谨慎。许多政府意识到了这种担忧,并开始在这个领域制定许多规则和法规。其中一个数据隐私规则就是著名的通用数据保护条例GDPR),自 2018 年 5 月 25 日起生效。

欧洲联盟EU)GDPR 取代了《数据保护指令 95/46/EC》,旨在协调欧洲各地的数据隐私法律,保护和赋予所有欧盟公民数据隐私权,并重塑该地区组织处理数据隐私的方式。更多信息,请查看此链接:gdpr-info.eu/art-99-gdpr/

现代浏览器也为我们提供了足够的工具,以更详细的方式查看 Web 应用程序的许多方面,特别是安全方面。此外,浏览器还增加了越来越多的功能(例如,曾经 cookie 是存储数据的选项之一,但现在我们有其他选项,比如localStorageindexedDB),使其更容易受到来自一直在观望的黑客的安全漏洞和攻击。

为了满足这些各种应用程序要求,组织会选择公共云提供商而不是自己的本地数据中心。这使应用程序处于更加脆弱的状态,安全方面成为首要问题。构成应用程序的各个组件需要高度安全和不可被黑客攻击。

技术领域不断发展,新技术不断涌现并被开发者社区所采纳。由于这个原因和它带来的各种技术改进,许多组织不得不采用这些技术来在市场中竞争。这再次给安全带来了巨大压力,因为这些闪亮的新技术可能没有足够的努力将安全作为主要要求。

全面而言,在应用程序中具有严格的安全性是一个不言而喻的要求,组织和最终用户都很清楚这一事实。

响应式编程

在过去几年中,JavaScript 已成为最常用的语言之一,你可能已经在 JavaScript 的世界中听说过reactive这个术语,无论是在后端还是前端的上下文中。

那么,什么是响应式编程?—这是一种以异步数据流为核心的编程范式。数据以消息的形式在程序的各个部分之间流动。消息由Producer产生,并以一种“发出即忘记”的方式工作,程序产生一条消息然后忘记它。已订阅(表现出兴趣)此类消息的Subscriber会收到消息,处理它,并将输出作为消息传递给程序的其他部分来消费。

在数据库领域,NoSQL 从关系数据库中产生了巨大变革。同样,这种编程范式是从传统的编程范式(命令式编程)中产生了巨大变革。好消息是,即使不太了解,您在日常编码生活中已经编写了一些反应式代码。只要您看到这个词,您就间接地使用了一小部分反应式代码。这种编程有自己的名称,并且这一方面在行业中变得更加主流。许多语言都理解了这带来的优势,并开始原生支持这种编程范式。

反应式应用

在本章的前一部分,我们讨论了过去十年应用程序需求的巨大变化。为了满足这一需求,出现了一种名为反应式应用的应用开发概念。

了解反应式编程和反应式应用之间的区别很重要。采用反应式编程并不会产生反应式应用,但是反应式编程的概念肯定可以帮助构建反应式应用。

了解反应式宣言将有助于您理解反应式应用/系统,因为宣言清楚地规定了反应式应用的每个方面。

反应式宣言

宣言是一项公开宣布的意图、观点、目标或动机,如政府、主权国家或组织发布的宣言(www.dictionary.com/browse/manifesto)。

反应式宣言清楚地阐述了发布者的观点,根据这一宣言可以开发出反应式应用。

根据反应式宣言(www.reactivemanifesto.org/),反应式系统应该是响应式的、弹性的、具有弹性和消息驱动的。

让我们更详细地了解这些术语。本节大部分内容来自在线反应式宣言,稍作修改以便更容易理解。

响应式

在出现问题的情况下,响应系统可以快速检测到问题并有效处理。这些系统还能够提供一致的响应时间,并建立上限,保证最低的服务质量QoS)。由于这些特点,这些系统能够建立终端用户的信心,简化错误处理,并鼓励终端用户更多的互动。

弹性

在失败的情况下,弹性系统保持响应和可交互。应用程序中的弹性可以通过以下方式实现:

  • 复制:在多个地方运行相同的组件,以便如果一个失败,另一个可以处理,并且应用程序可以正常运行。

  • 封装/隔离:特定组件的问题被包含和隔离在该组件内部,并且不会干扰其他组件或作为复制的其他相似组件。

  • 委托:在组件出现问题的情况下,控制会立即转移到另一个运行在完全不同上下文中的相似组件。

弹性

弹性系统可以在输入速率增加或减少时轻松自动扩展(增加或减少资源)。这种系统没有任何争用点,并且可以随意复制组件,分发负载增加。这些系统的设计方式确保了在需要扩展时,可以通过增加更多的商品硬件和软件平台来以非常具有成本效益的方式进行,而不是使用昂贵的硬件和许可软件平台。

消息驱动

在响应式应用中,主要方面之一是使用异步消息将数据从一个组件传递到另一个组件。这带来了组件之间的松耦合,并有助于实现位置透明性(只要组件是可到达/可发现的,它可以位于任何地方的单个节点或节点集群中)。创建消息,发布并忘记。注册的订阅者接收消息,处理它,并广播消息以便其他订阅者完成其工作。这是响应式编程的核心方面之一,也是响应式系统所需的基本方面之一。这种“发射和忘记”的概念带来了一种非阻塞的通信方式,从而产生了高度可扩展的应用程序。

以下图表(图 1)清楚地以图形方式展示了响应式宣言。它还清楚地展示了响应式宣言中主要概念之间的关系:

图 1:响应式宣言

由于响应式应用是响应式、弹性、可伸缩和消息驱动的,这些应用本质上是高度灵活、高度可扩展、松耦合和容错的。

Mateusz Gajewski 在www.slideshare.net上分享的一个演示中,以非常好的方式总结了响应式宣言:

图 2:Mateusz Gajewski 构想的响应式宣言

Spring 框架

Spring 框架是构建 Java 应用程序的事实标准。在过去的十年中,它随着每个主要版本的发布而不断成熟。 Spring 框架 5 于 2017 年 9 月作为 5.0.0 版正式发布;这是自 2013 年发布的上一个版本以来对框架的重要(主要)发布。

Spring 5 的一个重大新增功能是引入了一个基于核心响应式基础构建的功能性 Web 框架 Spring WebFlux。响应式编程正在悄悄地渗透到框架中,并且框架内的许多核心模块在很大程度上都在本质上支持响应式编程。由于框架已经开始原生支持响应式编程,因此这种编程的核心方面已经得到完全实现,并且许多模块都遵循了这种编程方式。此外,许多响应式概念已经成为框架内的通用语言。

需要注意的是,Spring 的响应式概念是直接从 Java 8 的Reactor Core 库中提取的,该库实现了响应式编程范式。 Reactor Core 是建立在Reactive Streams 规范之上的,这是在 Java 世界中构建响应式应用的行业标准。

另一个重要特性是包括了一种新的方式来测试这种应用程序。我们在(第五章,与 Spring WebFlux 集成)中有一个专门的章节介绍 Spring WebFlux,其中将更详细地介绍这些方面。

作为一个重大发布,它增加或增强了大量内容。但我们不打算列出其所有功能。完整列表可以在此链接找到:github.com/spring-projects/spring-framework/wiki/What%27s-New-in-Spring-Framework-5.x.

Java 中的响应式景观

当你从传统的编程模型转变过来时,很难理解响应式概念。随后的一些部分旨在向您介绍响应式概念以及它们如何演变为现在的状态。

响应式流和响应式流规范

Reactive Streams 的官方文档(www.reactive-streams.org/)表示:Reactive Streams 是提供异步流处理和非阻塞背压的标准的一个倡议。这包括针对运行时环境(JVM 和 JavaScript)以及网络协议的努力。

它始于 2013 年一群公司的倡议。2015 年 4 月,1.0 版规范发布,同时有多个实现(如 Akka Streams 和 Vert.x)可用。该规范的目标是将其纳入官方 Java 标准库,并在 2017 年,随着 JDK9 的发布,它正式进入其中。与任何规范一样,最终目标是有多个符合规范的实现,并随着时间的推移,规范会不断发展。规范包括一些核心接口,围绕这些接口的一些规则,以及一个技术兼容性测试套件TCK)。

TCK 是一套测试,用于检查Java 规范请求JSR)实现的正确性/符合性。在Java 社区流程JCP)中,TCK 是批准 JSR 所需的三个组成部分之一。另外两个是 JSR 规范和 JSR 参考实现。Java 平台的 TCK 称为Java 兼容性测试套件JCK)。

作为一项规范,它使得尊重规范的任何实现都能相互合作和互操作。例如,使用 Akka 编写的实现可以在不出现问题的情况下通过反应流协议与 Vert.x 实现进行通信。采用情况正在增加,目前,符合规范的更多实现正在以不同语言编写的形式发布:

图 3:反应流规范/API

前述图清楚地显示了反应流规范。以下是一些重要的规范规则:

  • “发布者”到“订阅者”和“订阅者”到“发布者”的调用不应该是并发的。

  • “订阅者”可以同步或异步执行其工作,但始终必须是非阻塞的。

  • 从“发布者”到“订阅者”应该定义一个上限。在定义的边界之后,缓冲区溢出会发生,并可能导致错误。

  • 除了NullPointerExceptionNPE)之外,不会引发其他异常。在 NPE 的情况下,“发布者”调用onError方法,“订阅者”取消“订阅”。

在前述对反应流的定义中,有一些非常重要的术语,即非阻塞反压,我们将更深入地探讨一下,以了解反应流的核心概念。

非阻塞

非阻塞意味着线程永远不会被阻塞。如果线程需要阻塞,代码会以一种使线程在正确时间得到通知并继续进行的方式编写。反应式编程让您实现非阻塞、声明式和事件驱动的架构。

写非阻塞应用程序的一种方法是使用消息作为发送数据的手段。一个线程发送请求,然后很快,该线程被用于其他事情。当响应准备好时,它会使用另一个线程传递回来,并通知请求方,以便进一步处理可以继续进行:

图 4:非阻塞

非阻塞概念已经被众所周知的框架实现,如 Node.js 和 Akka。Node.js 使用的方法是单个线程以多路复用的方式发送数据。

在电信和计算机网络中,多路复用(有时缩写为 muxing)是一种将多个模拟或数字信号合并成一个信号的方法,通过共享介质。其目的是共享昂贵的资源。有关多路复用的更多信息,您可以访问以下链接:www.icym.edu.my/v13/about-us/our-news/general/722-multiplexing.html

反压

在理想情况下,生产者产生的每条消息都会在产生时立即传递给订阅者,而不会有任何延迟。有可能订阅者无法以与产生速率相同的速度处理消息,这可能会使其资源受到压制。

背压是一种方法,通过该方法订阅者可以告诉生产者以较慢的速度发送消息,以便给订阅者时间来正确处理这些消息,而不会对其资源施加太大压力。

由于这是第一章,我们只是向您介绍了这些重要的响应式概念。代码示例将在后续章节中介绍。

现在我们对响应式流和响应式流规范有了一个简要的了解,我们将进入 Java 中的下一个重要的响应式概念,即响应式扩展。

响应式扩展

响应式扩展Rx 或 ReactiveX)(msdn.microsoft.com)是一个使用可观察序列和 LINQ 风格查询操作来组合异步和基于事件的程序的库。数据序列可以采用多种形式,例如来自文件或网络服务的数据流、网络服务请求、系统通知或一系列事件,例如用户输入。

如前述定义所述,这些是允许使用观察者模式进行流组合的 API。在继续之前,我有责任向您介绍观察者模式。以下是这种模式的定义,它非常直观:

观察者模式定义了一个提供者(也称为主题或可观察者)和零个、一个或多个观察者(订阅者)。观察者向提供者注册,每当预定义的条件、事件或状态发生变化时,提供者会自动通过调用观察者的方法来通知所有观察者。有关观察者模式的更多信息,您可以参考此链接:docs.microsoft.com/en-us/dotnet/standard/events/observer-design-pattern

数据可以以多种形式流动,例如流或事件。响应式扩展让您将这些数据流转换为可观察对象,并帮助您编写响应式代码。

Rx 在多种语言中实现,包括 Java(RxJava)。可以在reactivex.io/找到已实现的语言的完整列表和有关 Rx 的更多详细信息。

RxJava

RxJava是 ReactiveX 的 Java VM 实现,它是通过使用可观察序列来组合异步和基于事件的程序的库。

RxJava 是由 Netflix 将.NET 移植到 Java 世界的。经过近两年的开发,API 的稳定版本于 2014 年发布。此稳定版本针对 Java(版本 6 及以上)、Scala、JRuby、Kotlin 和 Clojure。

RxJava 是一个单一的 JAR 轻量级库,专注于 Observable 抽象。它便于与各种外部库集成,使库与响应式原则保持一致。一些例子是rxjava-jdbc(使用 RxJava Observables 进行数据库调用)和 Camel RX(使用 RxJava 的 Reactive Extensions 支持 Camel)。

响应式流和 RxJava

RxJava 2.x 是从其前身 RxJava 1.x 进行了完全重写。

RxJava 1.x 是在 Reactive Streams 规范之前创建的,因此它没有实现它。另一方面,RxJava 2.x 是基于 Reactive Streams 规范编写的,并完全实现了它,还针对 Java 8+。RxJava 1.x 中的类型已经完全调整以符合规范,并在重写时经历了重大变化。值得注意的是,存在一个桥接库(github.com/ReactiveX/RxJavaReactiveStreams),它在 RxJava 1.x 类型和 Reactive Streams 之间建立桥梁,使 RxJava 1.x 能够通过 Reactive Streams TCK 兼容性测试。

在 RxJava 2.x 中,许多概念保持不变,但名称已更改以符合规范。

我们不会深入研究 RxJava,因为这是一个庞大的主题,有很多书籍可以深入了解 RxJava。

JDK 9 的新增内容

作为 JDK 9 的并发更新的一部分(JEP 266),Reactive Streams 被添加到了 Java 标准库中。Reactive Streams 于 2013 年由一些知名组织发起,他们希望标准化异步数据在软件组件之间交换的方法。很快,这个概念被行业采纳,并出现了许多实现,它们都有类似的核心概念,但缺乏标准的命名和术语,特别是接口和包命名方面。为了避免多种命名方式,并实现不同实现之间的互操作性,JDK 9 包含了基本接口作为 Flow Concurrency 库的一部分。这使得应用程序想要实现 Reactive Streams 依赖于这个库,而不是将特定的实现包含到代码库中。因此,很容易在不产生任何麻烦的情况下在不同实现之间切换。

这些接口被编码为java.util.concurrent.Flow类中的静态接口。

重要接口

Java 9 中的 Reactive Streams 规范围仅涉及四个接口——PublisherSubscriberSubscriptionProcessor。该库还包括一个Publisher实现——SubmissionPublisher。所有这些都包含在 Java 标准库的java.util.concurrent包中。我们将在以下子章节中介绍这些接口。

发布者接口

这个接口的定义如下:

public interface Publisher<T> {
  public void subscribe(Subscriber<? super T> s);
}

正如你所看到的,Publisher允许Subscriber接口订阅它,以便在Publisher产生消息时接收消息。

订阅者接口

这个接口的定义如下:

public interface Subscriber<T> {
  public void onSubscribe(Subscription s);
  public void onNext(T t);
  public void onError(Throwable t);
  public void onComplete();
}

正如你所看到的,Subscriber接口的onSubscribe方法允许SubscriberPublisher接受Subscription时得到通知。当新项目发布时,onNext方法被调用。正如其名称所示,当出现错误时,将调用onError方法,当Publisher完成其功能时,将调用onComplete方法。

订阅接口

这个接口的定义如下:

public interface Subscription {
  public void request(long n);
  public void cancel();
}

请求方法用于接受项目的请求,取消方法用于取消Subscription

处理器接口

这个接口的定义如下:

public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {
}

它继承自PublisherSubscriber接口,因此继承了这些接口的所有方法。主要的方面是Publisher可以产生一个项目,但Subscriber可以消耗与Publisher产生的项目不同的项目。

Spring 框架和响应式应用

Spring 框架在 2013 年采用了响应式(与响应式诞生并变得更加主流的同时),发布了 Reactor 1.0 版本。这是 Spring 框架 4.0 版本发布并与 Pivotal 合作的时候。2016 年,Spring 的 4.3 版本与 Reactor 的 3.0 版本一起发布。在这个时期,Spring 5.0 版本的开发也在积极进行中。

随着新一代应用程序的需求,许多传统的编码实践受到了挑战。其中一个主要方面是摆脱阻塞 IO,并找到替代传统命令式编程的方法。

由 Servlet 容器支持的 Web 应用程序在本质上是阻塞的,Spring 5 通过引入基于响应式编程的全新 Web 应用程序框架 Spring WebFlux,在 Web 应用程序开发方面做出了很大贡献。

Spring 也采用了 Rx,并在 Spring 5 中以多种方式使用了它。在 Spring 5 中,响应式特性在许多方面都已经内置,帮助开发人员以渐进的方式轻松采用响应式编程。

Pivotal 在 Reactor 上投入了大量资源,但也暴露了 API,允许开发人员在 Reactor 和 RxJava 之间选择他们喜欢的库。

以下图示了 Spring 5 对响应式编程的支持:

图 5:Spring Framework + Reactor + Rx

Reactor 是 Pivotal(SpringSource)对实现 Reactive Streams 规范的回应。如前所述,Spring 在 Reactor 上投入了大量资源,本节旨在深入了解 Reactor。

Reactor 是第四代基于 Reactive Streams 规范在 JVM 上构建非阻塞应用程序的响应式库。

Project Reactor历史概述可以用以下图示表示:

图 6:Project Reactor 历史

上图显示了 Project Reactor 的主要发布版本。该项目于 2013 年启动(1.x 版本),3.x 的主要发布版本于 2016 年发布。截至撰写本书时,该框架的核心模块版本为 3.1.8.RELEASE。

现在我们对 Spring Framework 及其与响应式编程的关系有了简要的了解,让我们深入了解一下 Project Reactor。

Reactor 中的模块

随着 Reactor 3.0 的最新发布,该项目已经考虑到了模块化。Reactor 3.0 由四个主要组件组成,分别是 Core、IO、Addons 和 Reactive Streams Commons。

  • Reactor Core (github.com/reactor/reactor-core):Reactor 中的主要库。它提供了基础的、非阻塞的 JVM 兼容的 Reactive Streams 规范实现。它还包含了 Reactor 类型的代码,如FluxMono

  • Reactor IO (github.com/reactor/reactor-ipc):它包含了支持背压的组件,可用于编码、解码、发送(单播、多播或请求/响应),然后服务连接。它还包含了对Kafka (kafka.apache.org/)、Netty (netty.io/)和Aeron (github.com/real-logic/aeron)的支持。

  • Addons (github.com/reactor/reactor-addons):顾名思义,这些是由三个组件组成的附加组件:

  • reactor-adapter:包含了与 RxJava 1 或 2 类型的桥接,如 Observable、Completable、Single、Maybe 和 Mono/Flux 来回转换。

  • reactor-logback:支持异步 reactor-core 处理器上的 logback。

  • reactor-extra:包含了Flux的更多操作,包括求和和平均值等数学运算。

  • Reactive Streams Commons (github.com/reactor/reactive-streams-commons):Spring 的 Reactor 和 RxJava 之间的协作实验项目。它还包含了两个项目都实现的 Reactor-Streams 兼容操作符。在一个项目上修复的问题也会在另一个项目上修复。

Reactor Core 中的响应式类型

Reactor 提供了两种响应式类型,FluxMono,它们广泛实现了 Rx。它们可以被表示为一个时间线,其中元素按照它们到达的顺序进行排序。重要的是要掌握这两种类型。让我们在以下小节中做到这一点。

Flux 响应式类型

一个具有 Rx 操作符的 Reactive Streams 发布者,它会发出0N个元素,然后完成(成功或出现错误)。更多信息,请查看以下链接:projectreactor.io

Flux<T>是一个带有基本流操作的Publisher<T>,支持0..n个元素。

Flux的定义如下:

public abstract class Flux<T>
 extends Object
 implements Publisher<T>

Flux文档中所示的以下图示更详细地解释了Flux的工作原理:

图 7:Flux的工作原理

Flux 支持在 Spring 5 和其他重要模块中,包括 Spring Security。对Flux进行操作将创建新的发布者。

有关更多信息,请参阅 Reactor Flux 文档:projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html

现在,让我们看一些代码示例,展示了Flux的用法:

  • 创建空的Flux
Flux<String> emptyFlux = Flux.empty();
  • 创建带有项目的Flux
Flux<String> itemFlux = Flux.just("Spring”, "Security”, "Reactive”);
  • 从现有列表创建Flux
List<String> existingList = Arrays.asList("Spring”, "Security”, "Reactive”);
Flux<String> listFlux = Flux.fromIterable(existingList);
  • 创建以无限方式每隔x毫秒发出的Flux
Flux<Long> timer = Flux.interval(Duration.ofMillis(x));
  • 创建发出异常的Flux
Flux.error(new CreatedException());

Mono反应式类型

一个具有基本 Rx 运算符的 Reactive Streams Publisher,通过发出一个元素或出现错误来成功完成。

  • Mono JavaDoc

Mono<T>是支持0..1个元素的Publisher<T>

Mono的定义如下:

public abstract class Mono<T>
    extends Object
    implements Publisher<T>

如文档中所述,以下图显示了Mono的工作原理:

图 08:Mono的工作原理

Mono<Void>应该用于没有值完成的Publisher。文档使用了一个自解释的大理石图解释了每种方法及其工作原理。同样,这种类型也受到 Spring 5 和 Spring Security 的支持。

Mono的 JavaDoc 包含更多信息:projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html

让我们看一些例子:

  • 创建空的Mono
Mono<String> emptyMono = Mono.empty();
  • 创建带有值的Mono
Mono<String> itemMono = Mono.just("Spring Security Reactive”);
  • 创建发出异常的Mono
Mono.error(new CreatedException());

数据流类型

广义上,数据流可以分为两种类型:

  • 冷数据流:这有许多名称,比如冷源冷可观察对象冷发布者。它们只在有人订阅时才发出数据,因此从开始产生的所有消息都会传递给订阅者。如果新的Subscriber连接到它,消息将按升序重放,对于任何新的Subscriber也是如此。Subscriber还可以规定Publisher应该发出消息的速率。这些数据流是应用反应式背压(request(n))的良好候选者,例如数据库游标或文件流(读取文件)。

  • 热数据流:这又有许多不同的名称,比如热源热可观察对象热发布者。它们发出数据,而不管是否连接了任何订阅者。当新的Subscriber连接时,它只会从那个时间点开始发出消息,并且不能重放从头开始的消息。它们不能暂停消息的发出,因此需要另一种机制来控制流量,比如缓冲区。这种流的例子包括鼠标事件和股票价格。

重要的是要注意,流上的运算符可以改变它们的属性,从冷到热,反之亦然。此外,有时会发生热和冷之间的合并,它们的属性也会改变。

Reactor 和 RxJava

两者之间的主要区别之一是 RxJava 2.x 兼容 Java 6+,而 Reactor 兼容 Java 8+。如果您选择 Spring 5,我建议您使用 Reactor。如果您对 RxJava 2.x 感到满意,就没有必要迁移到 Reactor。Reactor 是 Reactive Streams 规范的实现,因此您可以保持对底层实现的不可知性。

反应式 Web 应用程序

Spring 5 将反应式概念引入了 Web 应用程序开发的世界,并包括了许多重要组件。让我们在这里介绍它们。

Spring WebFlux

Spring 5 内置了一个响应式堆栈,使用它可以构建基于 Reactive Streams 的 Web 应用程序,可以在新的非阻塞服务器上运行,例如 Netty、Undertow 和 Servlet 容器,运行在大于 3.1 的 Servlet 规范上。

现有的 Web 应用程序框架,如 Spring MVC,从一开始就是为 Servlet 容器构建的,但是 Spring 5 带来了一个新的 Web 应用程序框架,Spring WebFlux,专为响应式而创建。本书中有一个专门的章节涵盖了 Spring WebFlux(第五章,与 Spring WebFlux 集成),所以我不会在这里深入讨论。值得知道的是,Spring 5 对响应式有着严肃的思考,并且这在所有这些新的添加中都得到了清晰的体现。

Spring WebFlux 需要将 Reactor 作为其核心依赖之一。但是,与往常一样,如果需要,它确实可以让您轻松切换实现。

Reactive Spring Web

Spring Web 模块github.com/spring-projects/spring-framework/tree/master/spring-web)有许多用于构建响应式 Web 应用程序的基础组件。它允许您执行与服务器和客户端相关的操作。

它在服务器端提供的功能分为两个方面:

  • HTTP:包含在spring-weborg.springframework.http包中,包含用于受支持服务器的 HTTP 请求处理的各种 API

  • Web:包含在spring-weborg.springframework.web包中,包含用于请求处理的各种 API

该模块还包含在客户端上工作的消息编解码器,用于对请求和响应进行编码和解码。这些编解码器也可以在服务器上使用。

WebClient

org.springframework.web.reactive.function.client.WebClient接口是 Spring 5 中引入的一种响应式 Web 客户端,可用于执行 Web 请求。类似地,还有org.springframework.test.web.reactive.server.WebTestClient接口,它是一个特殊的WebClient,用于在应用程序中编写单元测试。WebClientRestTemplate的响应式版本,它使用 HTTP/1.1 协议。它们作为spring-webflux模块的一部分打包。

WebSockets

spring-webflux模块还具有响应式 WebSocket 实现。WebSocket允许我们在客户端和服务器之间建立双向连接,这种用法在新一代应用程序中变得越来越普遍。

应用程序安全

应用程序安全由各种流程组成,旨在发现、修复和防止应用程序中的安全漏洞。

我们生活在开发+运维DevOps)的世界中,在这里我们将工程和运营人员聚集在一起。DevOps 倡导在各个层面进行自动化和监控。随着安全变得非常重要,一个新术语DevSecOps变得突出——这是我们将安全作为一等公民的地方。

对于一个应用程序,安全属于非功能性要求。由于它在应用程序中的重要性,大多数组织都有专门的团队来测试潜在的安全漏洞。这是一个非常重要的方面需要考虑,因为在这个现代世界中,安全漏洞可能严重破坏组织的品牌。

安全是一个非常广泛的术语,涵盖了许多方面。在本书中,我们将使用 Spring Framework 模块 Spring Security 来查看一些基本的安全问题。在涵盖了一些核心安全问题之后,我们还将看一些低级安全问题以及 Spring Security 如何帮助解决这些问题。

由于我们将专注于 Spring,我们将深入探讨与 Java Web 应用程序开发相关的安全问题。

Spring Security

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。

– Spring by Pivotal

Spring Security 5 是该框架的新版本,也是本书的主要关注点。Spring Security 使您能够全面处理应用程序的身份验证和授权。它还有顶级项目,专门处理多种身份验证机制,如LDAPOAuthSAML。Spring Security 还提供了足够的机制来处理常见的安全攻击,如会话固定点击劫持跨站点请求伪造。此外,它与许多 Spring Framework 项目(如 Spring MVC、Spring WebFlux、Spring Data、Spring Integration 和 Spring Boot)有很好的集成。

Spring Security 术语

了解一些最重要的 Spring Security 术语非常重要。让我们来看看其中一些:

  • 主体:希望与您的应用程序交互的任何用户、设备或系统(应用程序)。

  • 身份验证:确保主体是其所声称的过程

  • 凭据:当主体尝试与您的应用程序交互时,身份验证过程开始并挑战主体传递一些值。一个例子是用户名/密码组合,这些值称为凭据。身份验证过程验证主体传递的凭据与数据存储中的凭据是否匹配,并回复适当的结果。

  • 授权:成功认证后,将再次检查主体在应用程序上可以执行的操作。这个检查主体权限并授予必要权限的过程称为授权。

  • 受保护的项目/资源:标记为受保护并要求主体(用户)成功完成身份验证和授权的项目或资源。

  • GrantedAuthority:Spring Security 对象(org.springframework.security.core.GrantedAuthority接口),包含/保存主体的权限/访问权限详细信息。

  • SecurityContext:Spring Security 对象,保存主体的身份验证详细信息。

Spring Security 的核心功能

Spring Security 为您的应用程序提供了许多安全功能。Spring Security 以其对各种身份验证和授权方法的支持而闻名。在本节中,我们将更详细地深入探讨这些核心功能。

身份验证

Spring Security 提供了多种方法,您的应用程序可以进行身份验证。它还允许您编写自定义身份验证机制,如果这些提供的默认方法不符合您的要求。由于这种可扩展性,甚至可以使用旧应用程序进行身份验证。本书有专门的章节(第三章、使用 SAML、LDAP 和 OAuth/OIDC 进行身份验证和第四章、使用 CAS 和 JAAS 进行身份验证),我们将更详细地介绍各种身份验证机制,如 OAuth、LDAP 和 SAML。

授权

Spring Security 允许您作为应用程序开发人员选择多种方式来授权用户访问应用程序的各个部分。以下是一些方法:

  • Web URL:基于 URL 或 URL 模式,您可以控制访问

  • 方法调用:如果需要,甚至可以对 Java Bean 中的方法进行访问控制

  • 领域实例:通过在应用程序中控制对特定数据的访问,可以控制对某些需要的领域对象的访问控制。

  • Web 服务:允许您保护应用程序中暴露的 Web 服务

在下一章中,我们将更详细地讨论这些方面,并提供更多的代码片段。

Spring Security 5 的新功能

Spring Security 5 提供了许多新功能,同时支持 Spring 5。作为此版本的一部分引入的一些重要新功能包括:

  • 支持 OAuth 2.0 和 OpenID Connect(OIDC)1.0:允许用户使用其现有的 OAuth 提供程序(例如 GitHub)或 OIDC 提供程序(例如 Google)登录到您的应用程序。OAuth 是使用授权码流实现的。我们将在后续章节中深入探讨这个问题。

  • 响应式支持:Spring 5 引入了一个新的响应式 Web 应用程序框架——Spring WebFlux。Spring Security 确保在所有方面(身份验证和授权)完全支持这个 Web 应用程序框架,使用响应式概念。

  • 改进的密码编码:引入密码编码委托允许使用多种算法对各种密码进行编码。Spring 识别算法的方式是通过读取编码密码的前缀,其中包含用于编码密码的算法。格式为{algorithm}encoded_password

Spring Security 的工作

在本节中,我们将看看 Spring Security 的工作原理。我们将首先解释核心概念,然后看看请求经过的各种类来执行安全性。

Servlet 过滤器

了解 Servlet 过滤器非常重要,这样您就可以了解 Spring Security 的内部工作。下图清楚地解释了 Servlet 过滤器的工作原理。它在请求到达实际资源之前以及在响应返回给消费者之前起作用。它是一个可插拔的组件,可以随时在 Web 配置文件(web.xml)中进行配置。

图 9:Servlet 过滤器的工作

过滤器链

您可以在到达实际资源之前嵌入任意数量的 Servlet 过滤器。根据它们在web.xml中声明的顺序触发过滤器。这种 Servlet 过滤器的链接称为过滤器链。Spring Security 依赖于一系列作为过滤器链排列的 Servlet 过滤器,每个过滤器执行单一的责任,然后将其交给下一个过滤器,依此类推。大多数内置过滤器对大多数应用程序来说已经足够好了。如果需要,您可以编写自己的过滤器,并将它们放在希望它们执行的位置。

安全拦截器(DelegatingFilterProxy)

当任何请求到达使用 Spring Security 进行保护的应用程序时,请求会经过一个门。这个拦截器完成所有的魔术,如果情况不妙,它会出错并返回给调用者,如下图所示:

图 10:安全拦截器的工作

安全拦截器确保根据为应用程序设置的各种安全配置,将工作委托给适当的方,并确保在实际到达调用者请求的资源之前,每个人都满意。为了执行实际工作,安全拦截器使用了许多管理器,每个管理器都负责执行单一的工作。下图列出了安全拦截器与之合作执行功能的一些重要管理器:

图 11:安全拦截器和相关管理器

在 Spring Security 中,安全拦截器由DelegatingFilterProxy完成。对于到达 Web 应用程序的任何请求,此代理确保将请求委托给 Spring Security,并且当事情顺利进行时,它确保将请求传递到 Web 应用程序中的正确资源。

DelegatingFilterProxy是一个 Servlet 过滤器,必须在您的web.xml文件中进行配置,然后委托给一个实现ServletFilter接口的 Spring 管理的 bean(@Bean)。

以下代码片段显示了如何在web.xml中配置DelegatingProxyFilter

<?xml version="1.0" encoding="UTF-8"?>
 <web-app>
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>
            org.springframework.web.filter.DelegatingFilterProxy
        </filter-class>
    </filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
 </web-app>

在上述代码中,所有对 Web 应用程序(/* mapping)的请求都将通过DelegatingProxyFilter过滤器进行。重要的是要注意,这个过滤器的名称应该是springSecurityFilterChain,因为 Spring Security 会寻找这个默认的过滤器名称来配置自己。代理过滤器只是将控制权传递/委托给一个名为springSecuirtyFilterChain的 bean。如果您正在使用默认的 Spring Security 设置,请求将被FilterChainProxy接收。FilterChainProxy负责将请求通过配置为 Spring Security 的一部分的各种 Servlet 过滤器传递。springSecuirtyFilterChain bean 不需要显式声明,而是由框架处理,对开发人员透明。

现在我们已经看过了 Spring Security 的所有核心概念,让我们回到以下图表中以图形方式表示的 Spring Security 的工作方式。它包含两个重要的安全方面-身份验证和授权:

图 12:Spring Security 的工作方式

来自调用者的请求到达DelegatingFilterProxy,它委托给FilterChainProxy(Spring Bean),后者通过多个过滤器传递请求,并在成功执行后,授予调用者对所请求的受保护资源的访问权限。

有关 Servlet 过滤器及其功能的完整列表,请参阅 Spring Security 参考文档:docs.spring.io/spring-security/site/docs/current/reference/html/security-filter-chain.html

有了所有这些细节,下图总结了 Spring Security 如何为您的 Web 应用程序处理身份验证和授权:

图 13:Spring Security 在使用数据库进行身份验证和授权

当调用者向受 Spring Security 保护的 Web 应用程序发送请求时,首先经过安全拦截器管理器,如身份验证管理器(负责身份验证)和访问决策管理器(负责授权),并在成功执行这些操作后,允许调用者访问受保护的资源。

对于响应式应用程序,这些概念都是有效的。有等效的响应式类,我们编码的方式是唯一改变的。这些都很容易理解和实现。

在第二章中,深入了解 Spring Security,我们将介绍身份验证,在第三章中,使用 SAML、LDAP 和 OAuth/OIDC 进行身份验证,我们将详细介绍授权,并深入了解其内部情况。

核心 Spring Security 模块

在 Spring Framework 中,Spring Security 是一个顶级项目。在 Spring Security 项目(github.com/spring-projects/spring-security)中,有许多子模块:

  • Corespring-security-core):Spring 安全的核心类和接口在这里进行身份验证和访问控制。

  • Remotingspring-security-remoting):如果您需要 Spring Remoting,这是具有必要类的模块。

  • Aspectspring-security-aspects):Spring Security 内的面向方面的编程AOP)支持。

  • Configspring-security-config):提供 XML 和 Java 配置支持。

  • 密码学(spring-security-crypto):包含密码学支持。

  • 数据(spring-security-data):与 Spring Data 集成。

  • 消息传递(spring-security-messaging

  • OAuth2:在 Spring Security 中支持 OAuth 2.x。

  • 核心(spring-security-oauth2-core

  • 客户端(spring-security-oauth2-client

  • JOSE(spring-security-oauth2-jose

  • OpenID(spring-security-openid):OpenID Web 身份验证支持。

  • CAS(spring-security-cas):CAS(中央认证服务)客户端集成。

  • TagLib(spring-security-taglibs):关于 Spring Security 的各种标签库。

  • 测试(spring-security-test):测试支持。

  • Web(spring-security-web):包含 Web 安全基础设施代码,如各种过滤器和其他 Servlet API 依赖项。

这些是与 Spring Security 密切相关的 Spring Framework 中的顶级项目:

  • spring-ldap:简化 Java 中的轻量级目录访问协议(LDAP)编程。

  • spring-security-oauth:使用 OAuth 1.x 和 OAuth 2.x 协议进行轻松编程。

  • spring-security-saml:为 Spring 应用程序提供 SAML 2.0 服务提供者功能。

  • spring-security-kerberos:将 Spring 应用程序与 Kerberos 协议轻松集成。

安全断言标记语言(SAML)是一种基于 XML 的框架,用于确保传输通信的安全性。SAML 定义了交换身份验证、授权和不可否认信息的机制,允许 Web 服务具有单一登录功能。

轻量级目录访问协议(LDAP)是在 TCP/IP 协议栈的一层上运行的目录服务协议。它基于客户端-服务器模型,并提供了用于连接、搜索和修改 Internet 目录的机制。

Kerberos 是一种网络身份验证协议。它旨在通过使用秘密密钥加密为客户端/服务器应用程序提供强身份验证。麻省理工学院提供了该协议的免费实现,并且它也可以在许多商业产品中使用。

有关 SAML、LDAP 和 Kerberos 的更多信息,您可以查看以下链接:

摘要

在本章中,我们向您介绍了新的应用程序要求,然后转向了一些核心的响应式概念。我们看了看响应式宣言和响应式编程。然后,我们将注意力转向了 Spring 5 和 Spring Security 5,并触及了其中的一些新功能,特别是关于响应式编程的。然后,我们简要地介绍了 Spring 的响应式编程工作,通过向您介绍 Project Reactor。之后,我们更详细地探讨了 Spring Security,以便您能够重新思考这个主题。最后,我们通过向您介绍本书中示例的结构以及我们将使用的编码实践,来结束了本章。

现在,您应该对响应式编程以及 Spring Security 及其工作原理有了很好的了解。您还应该清楚地了解如何浏览其余章节,特别是示例代码。

第二章:深入了解 Spring Security

这是一本实用的书,但我们的第一章是理论性的(应该是这样),因为它是一个介绍性的章节。

在本章中,我们将深入探讨 Spring Security 的技术能力,特别是认证和授权,使用代码。然而,在进入编码之前,我们将简要解释理论。我们这样做是因为在深入编码之前理解概念是很重要的。

安全的两个最重要方面如下:

  • 查找用户的身份

  • 查找该用户可以访问的资源

认证是找出用户是谁的机制,授权是允许应用程序找出用户对应用程序可以做什么的机制:

图 01:安全的基本方面——认证和授权

在本章中,我们将涵盖以下内容:

  • 认证

  • 认证机制

  • 授权

认证

保护资源的一个基本方法是确保调用者是其所声称的身份。检查凭据并确保它们是真实的过程称为认证

以下图表显示了 Spring Security 用于解决这一核心安全需求的基本过程。该图是通用的,可用于解释框架支持的各种认证方法:

图 02:认证架构

如第一章中所述,Spring 5 和 Spring Security 5 概述(在Spring Security 的工作方式部分),Spring Security 具有一系列 Servlet 过滤器(过滤器链)。当请求到达服务器时,它会被这一系列过滤器拦截(在前面的图中的Step 1)。

在响应式世界中(使用新的 Spring WebFlux web 应用程序框架),过滤器的编写方式与传统过滤器(例如 Spring MVC web 应用程序框架中使用的过滤器)有很大不同。尽管如此,对于两者来说,基本机制仍然保持不变。我们有一个专门的章节来解释如何将 Spring Security 应用程序转换为 Spring MVC 和 Spring WebFlux,在那里我们将更详细地涵盖这些方面。

在过滤器链中,Servlet 过滤器代码执行会一直跳过,直到达到正确的过滤器。一旦到达基于使用的认证机制的正确认证过滤器,它会从调用者中提取提供的凭据(通常是用户名和密码)。使用提供的值(在这里,我们有用户名和密码),过滤器(UsernamePasswordAuthenticationFilter)创建一个Authentication对象(在前面的图中,使用Step 2中提供的用户名和密码创建了UsernamePasswordAuthenticationToken)。然后,Step 2中创建的Authentication对象用于调用AuthenticationManager接口中的authenticate方法:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) 
        throws AuthenticationException;
}

实际的实现由ProviderManager提供,它具有配置的AuthenticationProvider列表。

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication)
        throws AuthenticationException;
    boolean supports(Class<?> authentication);
}

请求通过各种提供者,并最终尝试对请求进行认证。作为 Spring Security 的一部分,有许多AuthenticationProvider

在本章开头的图表中,AuthenticationProvider需要用户详细信息(一些提供者需要这个,但有些不需要),这些信息在UserDetailsService中提供:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws           
        UsernameNotFoundException;
}

UserDetailsService 使用提供的用户名检索 UserDetails(并实现User接口)。

如果一切顺利,Spring Security 将创建一个完全填充的Authentication对象(authenticate: true,授予的权限列表和用户名),其中将包含各种必要的详细信息。过滤器将Authentication对象存储在SecurityContext对象中以供将来使用。

AuthenticationManager 中的 authenticate 方法可以返回以下内容:

  • Authentication 对象,如果 Spring Security 能够验证提供的用户凭据,则 authenticated=true

  • AuthenticationException,如果 Spring Security 发现提供的用户凭据无效

  • null,如果 Spring Security 无法确定它是真还是假(混乱状态)

设置 AuthenticationManager

Spring Security 中有许多内置的 AuthenticationManager 可以在您的应用程序中轻松使用。Spring Security 还有许多辅助类,使用这些类可以设置 AuthenticationManager。其中一个辅助类是 AuthenticationManagerBuilder。使用这个类,可以很容易地设置 UserDetailsService 对数据库、内存、LDAP 等进行身份验证。如果需要,您还可以拥有自己的自定义 UserDetailsService(也许您的组织中已经有自定义的单点登录解决方案)。

您可以使 AuthenticationManager 全局化,这样它将可以被整个应用程序访问。它将可用于方法安全性和其他 WebSecurityConfigurerAdapter 实例。WebSecurityConfigurerAdapter 是您的 Spring 配置文件扩展的类,使得将 Spring Security 引入 Spring 应用程序变得非常容易。这是如何使用 @Autowired 注解设置全局 AuthenticationManager

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    public void confGlobalAuthManager(AuthenticationManagerBuilder auth) throws 
            Exception {
        auth
            .inMemoryAuthentication()
                .withUser("admin").password("admin@password").roles("ROLE_ADMIN");
    }
}

您还可以通过覆盖 configure 方法,在特定的 WebSecurityConfigurerAdapter 中创建本地 AuthenticationManager,如下面的代码所示:

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("admin").password("admin@password").roles("ROLE_ADMIN");
    }
}

另一个选项是通过覆盖 authenticationManagerBean 方法来公开 AuthenticationManager bean,如下所示:

@Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
}

您还可以将各种 AuthenticationManagerAuthenticationProviderUserDetailsService 公开为 bean,这将覆盖默认的 bean。

在前面的代码示例中,我们使用 AuthenticationManagerBuilder 来配置内存中的身份验证。AuthenticationManagerBuilder 类的更多机制将在本章的后续示例中使用。

AuthenticationProvider

AuthenticationProvider 提供了一种获取用户详细信息的机制,可以进行身份验证。Spring Security 提供了许多 AuthenticationProvider 实现,如下图所示:

图 03:Spring Security 内置的 AuthenticationProvider

在接下来的章节中,我们将详细介绍每个部分,并提供更多的代码示例。

自定义 AuthenticationProvider

如果需要,我们可以通过实现 AuthenticationProvider 接口来编写自定义 AuthenticationProvider。我们将需要实现两个方法,即 authenticate(Authentication)supports(Class<?> aClass)

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws     
            AuthenticationException {
      String username = authentication.getName();
      String password = authentication.getCredentials().toString();
      if ("user".equals(username) && "password".equals(password)) {
        return new UsernamePasswordAuthenticationToken
          (username, password, Collections.emptyList());
      } else {
        throw new BadCredentialsException("Authentication failed");
      }
    }
    @Override
    public boolean supports(Class<?> aClass) {
      return aClass.equals(UsernamePasswordAuthenticationToken.class);
    }
}

我们的 authenticate 方法非常简单。我们只需将用户名和密码与静态值进行比较。我们可以在这里编写任何逻辑并对用户进行身份验证。如果出现错误,它会抛出一个 AuthenticationException 异常。

在书的 GitHub 页面上,导航到 jetty-in-memory-basic-custom-authentication 项目,查看这个类的完整源代码。

多个 AuthenticationProvider

Spring Security 允许您在应用程序中声明多个 AuthenticationProvider。它们根据在配置中声明它们的顺序执行。

jetty-in-memory-basic-custom-authentication 项目进一步修改,我们使用新创建的 CustomAuthenticationProvider 作为 AuthenticationProviderOrder 1),并将现有的 inMemoryAuthentication 作为第二个 AuthenticationProviderOrder 2):

@EnableWebSecurity
@ComponentScan(basePackageClasses = CustomAuthenticationProvider.class)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CustomAuthenticationProvider customAuthenticationProvider;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()
                .and()
                .authorizeRequests()
                .antMatchers("/**")
                .authenticated(); // Use Basic authentication
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // Custom authentication provider - Order 1
        auth.authenticationProvider(customAuthenticationProvider);
        // Built-in authentication provider - Order 2
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password("{noop}admin@password")
                //{noop} makes sure that the password encoder doesn't do anything
                .roles("ADMIN") // Role of the user
                .and()
                .withUser("user")
                .password("{noop}user@password")
                .credentialsExpired(true)
                .accountExpired(true)
                .accountLocked(true)
                .roles("USER");
    }
}

每当 authenticate 方法执行时没有错误,控制权就会返回,此后配置的 AuthenticationProvider 将不会被执行。

示例应用程序

让我们开始编写一些代码。我们将从最常见的身份验证机制开始,然后进入可以与 Spring Security 一起使用的其他身份验证机制。

基本项目设置

除了实际的身份验证机制外,应用程序的许多方面都是相似的。在本节中,我们将设置示例,然后详细介绍特定的身份验证机制。

我们将使用默认的 Spring Security DB 模式来验证用户。我们将创建一个完整的 Spring MVC Web 应用程序,每个组件都是从头开始创建的。使用 Spring Boot 创建一个示例 Spring Security 应用程序非常容易。该应用程序将通过许多隐藏在开发人员背后的东西来运行。但在这种情况下,我们将逐个创建这个应用程序组件,以便您可以看到构建在 Spring MVC 上的 Web 应用程序的实际代码。

Spring Security 使用的默认 DB 模式如下图所示。但是,您可以根据自己的应用程序对其进行自定义。我们将在这里使用UsersAuthorities表进行设置:

图 04:Spring Security 默认数据库模式

现在让我们开始开发我们的示例应用程序。

步骤 1—在 IntelliJ IDEA 中创建一个 Maven 项目

在 IntelliJ 中,选择文件 | 新建 | 项目。这将打开新项目向导,如下截图所示。现在选择 Maven 并单击下一步按钮:

图 05:IntelliJ 中的新 Maven 项目

在新项目向导的下一个屏幕(步骤 2)中,输入 GroupId、ArtifactId 和 Version,如下截图所示:

图 06:IntelliJ 中的 Maven 项目设置—输入 GroupId、ArtifactId 和 Version

在新项目向导的下一个屏幕(步骤 3)中,输入项目名称和项目位置,如下截图所示:

图 07:Maven 项目设置—设置项目名称和项目位置

IntelliJ 将提示您进行操作,如下截图所示。要在pom.xml中进行任何更改时自动导入项目,请单击启用自动导入链接:

图 08:在 IntelliJ 中启用自动导入

步骤 2—pom.xml 更改

打开pom.xml文件,并在项目标签(<project></project>)中添加以下代码:

<!-- Spring dependencies -->
<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-web</artifactId>
   <version>5.0.4.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-config</artifactId>
   <version>5.0.4.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-crypto</artifactId>
   <version>5.0.4.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-webmvc</artifactId>
   <version>5.0.5.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-jdbc</artifactId>
   <version>5.0.4.RELEASE</version>
</dependency>
<!-- Servlet and JSP related dependencies -->
<dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>javax.servlet-api</artifactId>
   <version>3.1.0</version>
   <scope>provided</scope>
</dependency>
<dependency>
   <groupId>javax.servlet.jsp</groupId>
   <artifactId>javax.servlet.jsp-api</artifactId>
   <version>2.3.1</version>
   <scope>provided</scope>
</dependency>
<dependency>
   <groupId>javax.servlet.jsp.jstl</groupId>
   <artifactId>javax.servlet.jsp.jstl-api</artifactId>
   <version>1.2.1</version>
</dependency>
<dependency>
   <groupId>taglibs</groupId>
   <artifactId>standard</artifactId>
   <version>1.1.2</version>
</dependency>
<!-- For datasource configuration -->
<dependency>
   <groupId>org.apache.commons</groupId>
   <artifactId>commons-dbcp2</artifactId>
   <version>2.1.1</version>
</dependency>
<!-- We will be using MySQL as our database server -->
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>6.0.6</version>
</dependency>

pom.xml中构建一个设置,我们将使用 jetty 来运行创建的应用程序。

<build>
   <plugins>
       <!-- We will be using jetty plugin to test the war file -->
       <plugin>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-maven-plugin</artifactId>
           <version>9.4.8.v20171121</version>
       </plugin>
   </plugins>
</build>

步骤 3—MySQL 数据库模式设置

使用以下脚本创建默认数据库模式,并插入一些用户:

create table users(
    username varchar(75) not null primary key,
    password varchar(150) not null,
    enabled boolean not null
);
create table authorities (
    username varchar(75) not null,
    authority varchar(50) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);

使用以下脚本将数据插入上述表中:

insert into users(username, password, enabled)
    values('admin', '$2a$04$lcVPCpEk5DOCCAxOMleFcOJvIiYURH01P9rx1Y/pl.wJpkNTfWO6u', true);
insert into authorities(username, authority) 
    values('admin','ROLE_ADMIN');
insert into users(username, password, enabled)
    values('user', '$2a$04$nbz5hF5uzq3qsjzY8ZLpnueDAvwj4x0U9SVtLPDROk4vpmuHdvG3a', true);
insert into authorities(username,authority) 
    values('user','ROLE_USER');

password是使用在线工具www.devglan.com/online-tools/bcrypt-hash-generator进行单向哈希处理的。为了比较password,我们将使用PasswordEncoderBcrypt)。

凭据如下:

  • 用户 = admin 和密码 = admin@password

  • 用户 = user 和密码 = user@password

重要的是要注意,即使角色被命名为ROLE_ADMIN,实际名称是ADMIN,这是我们的代码在传递时将使用的名称。

步骤 4—在项目中设置 MySQL 数据库属性

src/main/resources文件夹中创建一个名为mysqldb.properties的文件,内容如下:

mysql.driver=com.mysql.cj.jdbc.Driver
mysql.jdbcUrl=jdbc:mysql://localhost:3306/spring_security_schema?useSSL=false
mysql.username=root
mysql.password=<your-db-password>

步骤 5—Spring 应用程序配置

com.packtpub.book.ch02.springsecurity.config包中创建一个名为ApplicationConfig的 Java 类,其中包含以下代码:

@Configuration
@PropertySource("classpath:mysqldb.properties")
public class ApplicationConfig {

   @Autowired
   private Environment env;

   @Bean
   public DataSource getDataSource() {
       BasicDataSource dataSource = new BasicDataSource();
       dataSource.setDriverClassName(env.getProperty("mysql.driver"));
       dataSource.setUrl(env.getProperty("mysql.jdbcUrl"));
       dataSource.setUsername(env.getProperty("mysql.username"));
       dataSource.setPassword(env.getProperty("mysql.password"));
       return dataSource;
   }
}

步骤 6—Web 应用程序配置

在这个例子中,我们将使用 Spring MVC 作为我们的 Web 应用程序框架。让我们创建 Web 应用程序配置文件:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages= {"com.packtpub.book.ch02.springsecurity.controller"})
public class WebApplicationConfig implements WebMvcConfigurer {
   @Override
   public void configureViewResolvers(ViewResolverRegistry registry) {
       registry.jsp().prefix("/WEB-INF/views/").suffix(".jsp");
   }
}

@EnableWebMvc注解确保您的应用程序基于 Spring MVC。

第 7 步-设置 Spring MVC

在 Spring MVC 中,请求会落在DispatcherServlet上。DispatcherServlet可以在web.xml中声明,或者如果您的 Servlet 容器是 3.0+,则可以作为 Java 配置。请创建一个虚拟的SpringSecurityConfig.java文件。当我们解释第一个身份验证机制,即基本身份验证时,我们将构建这个类。

public class SpringMvcWebApplicationInitializer
       extends AbstractAnnotationConfigDispatcherServletInitializer {

   @Override
   protected Class<?>[] getRootConfigClasses() {
       return new Class[] { ApplicationConfig.class, SpringSecurityConfig.class };
   }

   @Override
   protected Class<?>[] getServletConfigClasses() {
       return new Class[] { WebApplicationConfig.class };
   }

   @Override
   protected String[] getServletMappings() {
       return new String[] { "/" };
   }

}

第 8 步-控制器设置

让我们为受保护的 JSP 页面(home.jsp)创建一个基本控制器(HomeController)。请注意,映射方法的返回值应该是一个字符串,并且应该映射到 JSP 文件的实际名称。在我们的情况下,它是home.jsp,这是一个在用户登录时调用者导航到的受保护资源:

@Controller
public class HomeController {

   @GetMapping("/")
   public String home(Model model, Principal principal) {
       if(principal != null)
           model.addAttribute("msg", "Welcome " + principal.getName());
       return "home";
   }
}

第 9 步-JSP 创建

我们的主页是一个非常简单的 JSP 文件,如下面的代码片段所示。这个 JSP 只是显示我们在HomeController类中构造的消息:

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
        pageEncoding="ISO-8859-1"%>
<!DOCTYPE html>
<html>
<head>
   <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
   <title>Spring Security</title>
</head>
<body>
<h1>Spring Security Sample</h1>
<h2>${msg}</h2>
</body>
</html>

这是现在的基本 Spring MVC 应用程序,我们将尝试设置各种身份验证机制。

Spring 安全设置

为了解释 Spring 安全,我们将在之前创建的 Spring MVC 项目上实现基本身份验证。在第三章中,我们将使用 Spring 安全来实现其他身份验证机制,如 SAML、LDAP 和 OAuth/OIDC。为了在您的应用程序中执行基本身份验证,让我们执行本节中概述的附加步骤。

第 1 步-设置 Spring 安全配置

我们现在将创建非常重要的 Spring 安全配置类,并确保为 Spring 安全设置默认的过滤器链以保护所有资源:

@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
   @Autowired
   private DataSource dataSource;
   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.jdbcAuthentication().dataSource(dataSource)
               .usersByUsernameQuery("select username, password, enabled"
                       + " from users where username = ?")
               .authoritiesByUsernameQuery("select username, authority "
                       + "from authorities where username = ?")
               .passwordEncoder(new BCryptPasswordEncoder());
   }
   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests().anyRequest().hasAnyRole("ADMIN", "USER")
               .and()
               .httpBasic(); // Use Basic authentication
   }
}

在 Spring 安全配置中,我们首先告诉 Spring 安全,您将使用定义的用户查询对用户进行身份验证,并使用定义的权限查询检查用户的权限。

然后我们设置身份验证机制以检索用户的凭据。在这里,我们使用基本身份验证作为捕获用户凭据的机制。请注意,用于检查的角色名称没有前缀ROLE_

第 2 步-为 Web 应用程序设置 Spring 安全

我们知道我们必须指示应用程序开始使用 Spring 安全。一个简单的方法是在web.xml中声明 Spring 安全过滤器。如果您想避免使用 XML 并使用 Java 执行操作,那么创建一个类,它继承AbstractSecurityWebApplicationInitializer;这将初始化过滤器并为您的应用程序设置 Spring 安全:

public class SecurityWebApplicationInitializer
       extends AbstractSecurityWebApplicationInitializer {

}

通过这样,我们已经完成了查看基本身份验证所需的所有设置。

运行应用程序

通过执行mvn jetty:run命令运行项目。一旦您看到以下截图中显示的日志,打开浏览器并转到http://localhost:8080

图 09:Jetty 服务器运行-控制台日志

一旦访问 URL,浏览器会提示默认的基本身份验证对话框,如下截图所示。输入用户名和密码为admin/admin@password,然后点击登录:

图 10:浏览器中的基本身份验证对话框

如果您的凭据正确,并且用户具有ADMINUSER角色之一,您应该看到如下的主页:

图 11:成功登录后的主页

完整的项目代码可以在该书的 GitHub 页面上找到(github.com/PacktPublishing/Hands-On-Spring-Security-5-for-Reactive-Applications),在jetty-db-basic-authentication项目中。

内存用户存储

如前所述,出于各种测试目的,最好将用户凭据存储在内存中,然后进行身份验证,而不是使用诸如 MySQL 之类的真正数据库。为此,只需通过添加以下方法来更改 Spring Security 配置文件(SpringSecurityConfig.java):

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   auth
.inMemoryAuthentication()
           .withUser("admin")
           .password("{noop}admin@password") 
//{noop} makes sure that the password encoder doesn't do anything
           .roles("ADMIN") // Role of the user
           .and()
           .withUser("user")
           .password("{noop}user@password")
           .credentialsExpired(true)
           .accountExpired(true)
           .accountLocked(true)
           .roles("USER");
}

重要的是要注意,密码有一个前缀{noop},附加在其前面。这确保在验证密码时不进行编码。这是避免在运行项目时出现密码编码错误的一种方法。

完整的源代码作为一个完整的项目,可以在本书的 GitHub 页面中的jetty-in-memory-basic-authentication项目中找到。

作为 Spring Boot 运行

前面的示例可以通过遵循以下额外步骤轻松转换为 Spring Boot 应用程序。这个过程不会涵盖我们之前做过的许多琐碎步骤。您需要有另一个配置文件SpringSecurityConfig.java,其详细信息如下。

您可以创建一个新文件,通常命名为Run.java,其中包含以下代码:

@SpringBootApplication
public class Run {
   public static void main(String[] args) {
       SpringApplication.run(Run.class, args);
   }
}

这是一个非常简单的文件,其中有一个重要的注解@SpringBootApplication。我们去掉了 Spring MVC 配置类,并将以下属性放入application.properties文件中。这只是避免创建新的 Spring MVC 配置文件的另一种方法,而是使用属性文件:

spring.mvc.view.prefix: /WEB-INF/views/
spring.mvc.view.suffix: .jsp

与之前一样,其他一切保持不变。有关完整项目,请参考书籍的 GitHub 页面中的spring-boot-in-memory-basic-authentication项目。

打开命令提示符并输入以下命令:

mvn spring-boot:run

打开浏览器,导航到http://localhost:8080,然后应该提供基本身份验证对话框。成功登录后,应该被带到用户主页,如前所示。

授权

一旦用户在其声称的身份方面得到验证,下一个方面就是确定用户有权访问什么。确保用户在应用程序中被允许做什么的过程称为授权。

与身份验证架构一致,如前所述,授权也有一个管理器AccessDecisionManager。Spring Security 为此提供了三种内置实现:AffirmativeBasedConsensusBasedUnanimousBasedAccessDecisionManager通过委托给一系列AccessDecisionVoter来工作。授权相关的 Spring Security 类/接口如下图所示:

图 12:Spring Security 授权类/接口

在 Spring Security 中,对受保护资源的授权是通过调用选民然后统计收到的选票来授予的。三种内置实现以不同的方式统计收到的选票:

  • AffirmativeBased:如果至少有一个选民投票,用户将被授予对受保护资源的访问权限

  • ConsensusBased:如果选民和他们的选票之间达成明确的共识,那么用户将被授予对受保护资源的访问权限

  • UnanimousBased:如果所有选民投票,那么用户将被授予对受保护资源的访问权限

Spring Security 提供了两种授权方法:

  • Web URL:基于传入 URL(特定 URL 或正则表达式)的授权

  • Method:基于方法签名来控制访问的方法

如果您的服务层仅公开 RESTful 端点,并且应用程序中的数据被正确分类为资源(符合 REST 原则),则可以考虑使用 Web URL 方法。如果您的应用程序只是公开端点(基于 REST 的,我会称之为),并不真正符合 REST 原则,您可以考虑使用基于方法的授权。

Web URL

Spring Security 可以用于设置基于 URL 的授权。可以使用配置的 HTTP Security 与 Spring Security 配置来实现所需的授权。在我们迄今为止已经介绍的许多示例中,我们已经看到了模式匹配授权。以下是一个这样的例子:

  • AntPathRequestMatcher:使用 Ant 风格的模式进行 URL 匹配:
http
    .antMatcher("/rest/**")
    .httpBasic()
        .disable()
    .authorizeRequests()
        .antMatchers("/rest/movie/**", "/rest/ticket/**", "/index")
            .hasRole("ROLE_USER");

在上面的代码片段中,/rest URL 的基本身份验证被禁用,对于其他 URL(/rest/movie/rest/ticket/index),具有USER角色的用户可以访问。该片段还展示了单个匹配(使用antMatcher)和多个匹配(使用antMatchers)。

  • MvcRequestMatcher:这使用 Spring MVC 来匹配路径,然后提取变量。匹配是相对于 servlet 路径的。

  • RegexRequestMatcher:这使用正则表达式来匹配 URL。如果需要的话,它也可以用来匹配 HTTP 方法。匹配是区分大小写的,采用(servletPath + pathInfo + queryString)的形式:

http
    .authorizeRequests()
    .regexMatchers("^((?!(/rest|/advSearch)).)*$").hasRole("ADMIN")
    .regexMatchers("^((?!(/rest|/basicSearch)).)*$").access("hasRole(USER)")
        .anyRequest()
    .authenticated()
    .and()
    .httpBasic();

方法调用

Spring Security 允许用户使用面向方面的编程AOP)在后台访问控制方法执行。这可以使用 XML 配置或使用 Java 配置来完成。由于我们在本书中一直在使用 Java 配置,因此我们将在这里介绍 Java 配置和注解来解释方法安全性。最佳实践是选择一种特定的方法调用授权方法,并在整个应用程序中保持一致。选择适合您的应用程序的方法,因为没有关于何时选择何种方法的特定文档。

如果您想在应用程序中启用方法安全性,首先要用@EnableMethodSecurity对类进行注解。有三种类型的注解可以用于注解方法并对其进行授权。这些类型如下:

  • 基于投票的注解:Spring Security 中最常用的注解。Spring Security 的@Secured注解属于这个类别。要使用这些注解,首先必须启用它们,如下面的代码片段所示:
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // ...
}

一旦启用了注解的使用,就可以使用@Secured注解,如下面的代码片段所示:

@RestController
@RequestMapping("/movie")
public class MovieController {

    @GetMapping("public")
    @Secured("ROLE_PUBLIC")
    public String publiclyAvailable() {
        return "Hello All!";
    }

    @GetMapping("admin")
    @Secured("ROLE_ADMIN")
    public String adminAccessible() {
        return "Hello Admin!";
    }
}
  • JSR-250 安全注解:这也被称为企业 JavaBeans 3.0EJB 3)安全注解。同样,在使用这些注解之前,必须使用@EnableGlobalMethodSecurity(jsr250Enabled = true)来启用它们。以下片段展示了 JSR-250 安全注解的使用:
@RestController
@RequestMapping("/movie")
public class MovieController {

    @GetMapping("public")
    @PermitAll
    public String publiclyAvailable() {
        return "Hello All!";
    }

    @GetMapping("admin")
    @RolesAllowed({"ROLE_ADMIN"})
    public String adminAccessible() {
        return "Hello Admin!";
    }
}
  • 基于表达式的注解:基于@Pre@Post的注解属于这个类别。它们可以通过@EnableGlobalMethodSecurity(prePostEnabled = true)来启用:
@RestController
@RequestMapping("/movie")
public class MovieController {
    @GetMapping("public")
    @PreAuthorize("permitAll()")
    public String publiclyAvailable() {
        return "Hello All!";
    }
    @GetMapping("admin")
    @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
    public String adminAccessible() {
        return "Hello Admin!";
    }
}

在上面的例子中,hasAnyAuthority被称为Spring 表达式语言SpEL)。与所示的示例类似,还有许多预定义的表达式可用于安全性。

域实例

Spring Security 提供了访问控制各种附加到任何对象的权限的方法。Spring Security 访问控制列表ACL)存储与域对象关联的权限列表。它还将这些权限授予需要对域对象执行不同操作的各种实体。为了使 Spring Security 工作,您需要设置四个数据库表,如下图所示:

图 13:Spring Security ACL 数据库架构

以下是上图中表格的简要解释:

  • ACL_CLASS表:顾名思义,它存储域对象的类名。

  • ACL_SID表:安全身份SID)存储用户名(testuser)或角色名(ROLE_ADMIN)。PRINCIPAL列存储 0 或 1,如果 SID 是用户名,则为 0,如果是角色名,则为 1。

  • ACL_OBJECT_IDENTITY表:它负责存储与对象相关的信息并链接其他表。

  • ACL_ENTRY 表:它存储了每个 OBJECT_IDENTITY 的每个 SID 被授予的权限。

为了使 Spring Security ACL 工作,它还需要一个缓存。其中一个最容易与 Spring 集成的是 EhCache。

Spring Security ACL 支持以下权限:

  • READ

  • WRITE

  • CREATE

  • DELETE

  • ADMINISTRATION

为了使其工作,我们必须使用 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) 来启用它。现在我们已经准备好放置注解来开始访问控制域对象。使用 Spring ACL 的代码片段如下:

@PostFilter("hasPermission(filterObject, 'READ')")
List<Record> findRecords();

在查询记录(后过滤)之后,结果(列表)会被审查,并进行过滤,只返回用户具有 READ 权限的对象。我们也可以使用 @PostAuthorize 如下:

@PostAuthorize("hasPermission(returnObject, 'READ')")

在方法执行之后(@Post),如果用户对对象具有 READ 访问权限,它会返回。否则,它会抛出 AccessDeniedException 异常:

@PreAuthorize("hasPermission(#movie, 'WRITE')")
Movie save(@Param("movie")Movie movie);

在方法被触发之前(@Pre),它会检查用户是否对对象具有 WRITE 权限。在这里,我们使用传递给方法的参数来检查用户权限。如果用户有 WRITE 权限,它执行该方法。否则,它会抛出异常。

我们可以有一个完整的示例,但这本书可以涵盖的主题已经很多了。所以我就在这里留下它,我相信你现在已经有足够的信息来进行完整的实现了。

一些关于安全的常见内置 Spring 表达式如下:

表达式 描述
hasRole([role_name]) 如果当前用户具有 role_name,它返回 true
hasAnyRole([role_name1, role_name2]) 如果当前用户具有列表中的任何角色名称,它返回 true
hasAuthority([authority]) 如果当前用户具有指定权限,它返回 true
hasAnyAuthority([authority1, authority2]) 如果当前用户具有指定列表中的任何权限,它返回 true
permitAll 总是等同于 true
denyAll 总是等同于 false
isAnonymous() 如果当前用户是匿名的,它返回 true
isRememberMe() 如果当前用户已设置记住我,它返回 true
isAuthenticated() 如果当前用户不是匿名用户,它返回 true
isFullyAuthenticated() 如果当前用户不是匿名用户或记住我用户,它返回 true
hasPermission(Object target, Object permission) 如果当前用户对目标对象有权限,它返回 true
hasPermission(Object targetId, Object targetType, Object permission) 如果当前用户对目标对象有权限,它返回 true

其他 Spring Security 功能

Spring Security 除了核心安全功能、认证和授权之外还具有许多功能。以下是一些最重要的功能。在第七章 Spring Security Add-Ons 中,我们将通过实际编码更详细地介绍这些功能。我们将在本章创建的示例基础上构建,并解释这些非常重要的 Spring Security 功能:

  • 记住我认证:这也被称为持久登录,它允许网站在多个会话之间记住用户的身份。Spring Security 提供了一些实现(基于哈希令牌和持久令牌),使这变得容易。

  • 跨站请求伪造CSRF):这是黑客常用的一种安全漏洞,用于执行不道德的操作,未经授权地代表用户发送命令。Spring Security 允许我们通过配置轻松修复这个漏洞。

  • 跨域资源共享CORS):这是一种机制,通过添加额外的 HTTP 头,使运行在特定域上的 Web 应用程序可以访问在另一个域中公开的资源。这是确保只有合法代码可以访问域公开资源的安全机制之一。

  • 会话管理:适当的用户会话管理对于任何应用程序的安全性至关重要。以下是 Spring Security 轻松处理的一些重要的与会话相关的功能:

  • 会话超时:这确保用户会话在配置的值处于超时状态,且无法被黑客攻击。

  • 并发会话:这可以防止用户在服务器上有多个(配置值)会话处于活动状态。

  • 会话固定:这是一种安全攻击,允许攻击者劫持有效用户的会话,然后开始将其用于不道德的操作。

这些是 Spring Security 带来的一些重要功能。在涵盖与 Spring Security 相关的其他主题后,我们将对它们进行彻底探讨。

总结

本章旨在介绍两个重要的安全概念,即身份验证和授权,以及它们如何由 Spring Security 支持。

我们首先详细解释了这些概念,然后通过一个示例应用程序深入探讨了它们。我们使用 Spring MVC 应用程序作为基础,帮助您理解 Spring Security 概念。第四章,使用 CAS 和 JAAS 进行身份验证,旨在解释响应式 Web 应用程序框架 Spring WebFlux。

在下一章中,我们将通过扩展本章中构建的示例,了解 Spring Security 支持的其他身份验证机制。

第三章:使用 SAML、LDAP 和 OAuth/OIDC 进行身份验证

在本章中,我们将研究 Spring Security 支持的认证机制,即 SAML、LDAP 和 OAuth/OIDC。 这将是一个完全动手编码的章节。 我们将构建小型应用程序,其中大多数应用程序都是从我们在第二章中构建的基本应用程序开始的,深入 Spring Security

本章的主要目标是使您能够实现组织中最常用的认证机制,并展示 Spring Security 模块的功能。

每个认证机制都有一个项目,您可以在书的 GitHub 页面上看到。 但是,在书中,我们只会涵盖示例代码的重要方面,以减少章节内的混乱。

在本章中,我们将涵盖以下主题:

  • 安全断言标记语言

  • 轻量级目录访问协议

  • OAuth2 和 OpenID Connect

安全断言标记语言

安全断言标记语言SAML),由 OASIS 的安全服务技术委员会开发,是用于通信用户身份验证、权限和属性信息的基于 XML 的框架。 SAML 允许业务实体对主体(通常是人类用户)的身份、属性和权限向其他实体(例如合作伙伴公司或其他企业)做出断言。

模块application.SAML也是:

  • 一组基于 XML 的协议消息

  • 一组协议消息绑定

  • 一组配置文件(利用上述所有内容)

身份提供者IdP)是创建、维护和管理主体(用户、服务或系统)身份信息,并为联合或分布式网络中的其他服务提供商(应用程序)提供主体认证的系统。

服务提供者SP)是提供服务的任何系统,通常是用户寻求认证的服务,包括 Web 或企业应用程序。 一种特殊类型的服务提供者,即身份提供者,管理身份信息。

有关 SAML、IdP 和 SP 的更多信息,您还可以参考以下链接:

xml.coverpages.org/saml.html

kb.mit.edu/confluence/display/glossary/IdP+(Identity+Provider)

searchsecurity.techtarget.com/definition/SAML

Spring Security 有一个名为 Spring Security SAML 的顶级项目。 它被认为是一个扩展,为 Spring 应用程序提供了与支持 SAML 2.0 的各种认证和联合机制集成。 该扩展还支持多个 SAML 2.0 配置文件以及 IdP 和 SP 启动的 SSO。

有许多符合 SAML 2.0 标准的产品(IdP 模式),例如OktaPing FederateADFS,可以使用 Spring Security 扩展轻松集成到您的应用程序中。

深入讨论 SAML 的细节超出了本书的范围。但是,我们将尝试集成我们之前在第二章中构建的 Spring Boot 应用程序,深入了解 Spring Security,对其进行调整并转换为使用 SAML 2.0 产品 Okta 进行身份验证。在 SSO 的世界中,Okta 是一个知名的产品,允许应用程序轻松实现 SSO。在以下示例中,我们还将使用spring-security-saml-dsl项目,这是一个包含 Okta DSL 的 Spring Security 扩展项目。使用此项目可以显著简化 Spring Security 和 Okta 的集成。我们还将为您介绍在 Okta 平台上必须使用的配置,以确保示例是自包含和完整的。这并不意味着您必须将 Okta 作为应用程序的 SSO 平台;相反,它展示了 Spring Security SAML 模块,以 Okta 作为示例。

如前所述,我们将复制我们在第二章中创建的 Spring Boot 项目,作为此示例的起点。现在,让我们先来看看如何设置 SSO 提供程序(Okta);在随后的部分中,我们将看看如何调整我们复制的 Spring Boot 应用程序以实现 SAML 2.0 身份验证。

设置 SSO 提供程序

如详细说明,我们将使用 Okta 作为 SSO 提供程序来构建我们的示例应用程序,该应用程序使用 SAML 2.0 作为身份验证机制的 Spring Security。

要设置 Okta 用户,请执行以下步骤:

  1. 转到developer.okta.com,然后点击注册。

  2. 输入相关细节,然后点击开始。

  3. Okta 将向您发送包含组织子域和临时密码的电子邮件。

  4. 点击邮件中的登录按钮,输入您的用户名(电子邮件)和临时密码,然后登录。

  5. 您将看到一些与帐户相关的信息。填写详细信息并完成帐户设置。

  6. 您现在已经设置了一个 Okta 帐户,其中有一个用户(您),并且没有配置 SSO 的应用程序。

要设置 Okta 应用程序,请执行以下步骤:

  1. 登录到您的帐户,然后点击管理按钮。

  2. 在屏幕上,点击添加应用程序的快捷链接。

  3. 点击创建新应用程序按钮。选择 Web 作为平台,选择 SAML 2.0 单选按钮,然后点击创建按钮。

  4. 在应用程序名称字段中,输入您的应用程序名称,保持其余字段不变,然后点击下一步按钮。

  5. 在单点登录 URL 字段中,输入 URL 为https://localhost:8443/saml/SSO。在受众 URI 字段中,输入 URI 为https://localhost:8443/saml/metadata。保持其余字段不变,然后点击下一步按钮。

  6. 点击标有“我是 Okta 客户,正在添加内部应用程序”的单选按钮。

  7. 选择复选框,上面写着“这是我们创建的内部应用程序”,然后点击完成按钮。

要将 Okta 应用程序分配给用户,您需要按照以下步骤进行操作:

  1. 导航到仪表板,然后点击分配应用程序的快捷链接。

  2. 点击左侧的创建的应用程序(在应用程序部分),然后点击右侧的用户名(在人员部分),最后点击下一步按钮。

  3. 在下一页上,点击确认分配按钮,然后您就完成了将应用程序分配给用户。

您现在已经创建了 Okta 应用程序,并且您的用户分配已完成。现在,让我们尝试修改之前创建的应用程序,以便使用 SAML 2.0 对用户进行身份验证,针对我们创建的 Okta 应用程序。

设置项目

我们将更改两个文件:即SpringSecuirtyConfig(Spring 安全配置文件)和 Spring 应用程序属性文件(application.yml)。在之前的应用程序中,我们使用了属性文件(application.properties)而不是 YML(YAML)文件。在这个例子中,我们将放弃application.properties文件,并将使用application.yml文件进行所有设置。现在开始吧。

pom.xml 文件设置

复制您以前的项目。打开pom.xml文件并添加以下依赖项:

<!-- SAML2 -->
<dependency>
   <groupId>org.springframework.security.extensions</groupId>
   <artifactId>spring-security-saml2-core</artifactId>
   <version>1.0.3.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.springframework.security.extensions</groupId>
   <artifactId>spring-security-saml-dsl-core</artifactId>
   <version>1.0.5.RELEASE</version>
</dependency>

application.yml 文件设置

src/main/resources文件夹中创建一个新的application.yml文件,内容如下:

server:
 port: 8443
 ssl:
   enabled: true
   key-alias: spring
   key-store: src/main/resources/saml/keystore.jks
   key-store-password: secret

security:
 saml2:
   metadata-url: https://dev-858930.oktapreview.com/app/exkequgfgcSQUrK1N0h7/sso/saml/metadata

spring:
 mvc:
   view:
     prefix: /WEB-INF/views/
     suffix: .jsp

在第 13-17 行(在spring部分),我们将之前在application.properties文件中的配置数据迁移到了 YML 格式。除了metadata-url文件的配置之外,您可以保持所有之前的配置相同。对于这一点,您需要返回到您创建的 Okta 应用程序,并导航到“登录”选项卡。现在,点击“身份提供商元数据”链接并复制链接。它看起来类似于之前显示的链接,URL 末尾带有metadata

Spring 安全配置文件

现在,我们将改变(或者说配置)我们的 Spring Security 配置文件,如下所示:

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

   @Value("${security.saml2.metadata-url}")
   String metadataUrl;

   @Override
   protected void configure(HttpSecurity http) throws Exception {
      http
               .authorizeRequests()
               .antMatchers("/saml/**").permitAll()
               .anyRequest().authenticated()
               .and()
               .apply(saml())
               .serviceProvider()
               .keyStore()
               .storeFilePath("saml/keystore.jks")
               .password("secret")
               .keyname("spring")
               .keyPassword("secret")
               .and()
               .protocol("https")
               .hostname("localhost:8443")
               .basePath("/")
               .and()
               .identityProvider()
               .metadataFilePath(metadataUrl)
               .and();
   }
}

该文件无需进行任何修改。通过重要的configure方法,一切都进行得很顺利。在spring-security-saml-dsl-core中,引入saml()方法使编码变得非常简洁和容易。有了这个,您几乎完成了,最后一步是创建密钥库。

资源文件夹设置

导航到您的项目(在src/main/resources文件夹中)。创建一个名为saml的文件夹,并在该位置打开命令提示符。执行以下命令:

keytool -genkey -v -keystore keystore.jks -alias spring -keyalg RSA -keysize 2048 -validity 10000

在提示时,提供所需的详细信息,并在src/main/resources/saml文件夹中创建keystore.jks文件。

运行和测试应用程序

导航到您的项目文件夹并执行spring-boot命令,如下所示:

mvn spring-boot:run

打开浏览器,导航到https://localhost:8443。请注意https和端口8443(因为我们启用了 SSL)。如果在 URL 中不输入https,您将收到以下响应:

图 1:使用 HTTP 时浏览器的响应

浏览器将显示一个页面,指出您的连接不安全。消息可能会有所不同,这取决于您选择打开此 URL 的浏览器。只需确保您接受风险并继续前进。

您将被导航到 Okta URL,要求您使用用户名/密码登录,如下截图所示:

图 2:Okta 登录页面显示给用户

完成后,您将被导航回主页,显示您在home.jsp文件中放置的内容。下次打开 URL 时,您将直接进入主页,并且 Okta 将自动登录您。

使用 Spring Security 完成了 SAML 身份验证。您可以通过访问 GitHub 页面并导航到spring-boot-in-memory-saml2-authentication项目来查看完整的项目。

轻量级目录访问协议

轻量级目录访问协议LDAP)是一种目录服务协议,允许连接、搜索和修改 Internet 目录。不幸的是,LDAP 不支持反应式绑定;这意味着它不支持反应式编程(类似于 JDBC)。LDAP 身份验证的功能如下图所示:

图 3:LDAP 身份验证

与之前的示例类似,我们将克隆/复制之前的项目(任何 Spring Boot 项目都可以;我正在克隆spring-boot-in-memory-saml2-authentication项目)。与之前的项目类似,我们将修改一些文件并向项目中添加一些文件。我们将使用内置的基于 Java 的 LDAP 服务器来验证用户凭据。

在 pom.xml 文件中设置依赖项

打开pom.xml并添加以下依赖项:

<!-- LDAP -->
<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-tx</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.ldap</groupId>
   <artifactId>spring-ldap-core</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
   <groupId>com.unboundid</groupId>
   <artifactId>unboundid-ldapsdk</artifactId>
</dependency>

Spring 安全配置

修改SpringSecurityConfiguration.java文件,如下所示:

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
   private static final Logger LOG = 
                LoggerFactory.getLogger(SpringSecurityConfig.class);
   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()    .antMatchers("/admins").hasRole("ADMINS")
               .antMatchers("/users").hasRole("USERS")
               .anyRequest().fullyAuthenticated()
               .and()
               .httpBasic(); // Use Basic authentication
   }
   @Override
   public void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth
               .ldapAuthentication()
               .userDnPatterns("uid={0},ou=people")
               .userSearchBase("ou=people")
               .userSearchFilter("uid={0}")
               .groupSearchBase("ou=groups")
               .groupSearchFilter("uniqueMember={0}")
               .contextSource(contextSource())
               .passwordCompare()
               .passwordAttribute("userPassword");
   }
   @Bean
   public DefaultSpringSecurityContextSource contextSource() {
       LOG.info("Inside configuring embedded LDAP server");
       DefaultSpringSecurityContextSource contextSource = new 
               DefaultSpringSecurityContextSource(
               Arrays.asList("ldap://localhost:8389/"), "dc=packtpub,dc=com");
       contextSource.afterPropertiesSet();
       return contextSource;
   }
}

第一个configure方法与我们在之前的 SAML 示例中看到的非常相似。我们只是添加了某些匹配并分离了角色。通过这些更改,它仍将执行基本身份验证。

第二个configure方法是我们使用 LDAP 服务器设置身份验证的地方。LDAP 服务器以类似目录的格式存储用户信息。此方法详细说明了如何通过浏览目录结构来查找用户。

LDAP 服务器设置

我们将使用 Spring 的默认 LDAP 服务器来存储我们的用户,然后将其用作我们的应用程序中可以对用户进行身份验证的用户存储。LDAP 配置在我们的application.yml文件中完成,如下所示:

spring:
 ldap:
   # Embedded Spring LDAP
   embedded:
     base-dn: dc=packtpub,dc=com
     credential:
       username: uid=admin
       password: secret
     ldif: classpath:ldap/ldapschema.ldif
     port: 8389
     validation:
       enabled: false
 mvc:
   view:
     prefix: /WEB-INF/views/
     suffix: .jsp

ldap部分是不言自明的——我们正在使用各种参数设置嵌入式 LDAP 服务器。

在 LDAP 服务器中设置用户

我们将使用LDAP 数据交换格式LDIF)在我们的 LDAP 服务器上设置用户。LDIF 是 LDAP 数据的标准基于文本的表示形式,以及对该数据的更改(ldap.com/ldif-the-ldap-data-interchange-format/)。

在我们的application.yml文件中,我们已经告诉 Spring 在哪里查找我们的 LDIF 文件。LDIF 文件如下:

dn: dc=packtpub,dc=com
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: packtpub

dn: ou=groups,dc=packtpub,dc=com
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=packtpub,dc=com
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=john,ou=people,dc=packtpub,dc=com
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Tomcy John
uid: tjohn
userPassword: tjohn@password

dn: cn=admins,ou=groups,dc=packtpub,dc=com
objectclass: top
objectclass: groupOfUniqueNames
cn: admins
ou: admin
uniqueMember: uid=tjohn,ou=people,dc=packtpub,dc=com

dn: cn=users,ou=groups,dc=packtpub,dc=com
objectclass: top
objectclass: groupOfUniqueNames
cn: users
ou: user
uniqueMember: uid=tjohn,ou=people,dc=packtpub,dc=com

运行应用程序

在项目中的任何其他文件中都没有太多更改。就像运行任何其他spring-boot项目一样,转到项目文件夹并执行以下命令:

mvn spring-boot:run

在浏览器上查看应用程序的运行情况

打开浏览器,输入http://localhost:8080。输入用户名/密码为tjohn/tjohn@password(在 LDIF 文件中查找用户设置)。您将被带到home.jsp,在那里您将看到友好的欢迎消息,如下截图所示:

图 4:使用 LDAP 成功登录后在 home.jsp 页面显示的消息

OAuth2 和 OpenID Connect

OAuth是实现授权的开放标准/规范。它通过 HTTPS 工作,任何人都可以实现该规范。该规范通过验证访问令牌,然后授权设备、API、服务器等等。

存在两个版本,即 OAuth 1.0(tools.ietf.org/html/rfc5849)和 OAuth 2.0(tools.ietf.org/html/rfc6749)。这些版本彼此不兼容,不能一起工作。我们将使用版本 2.0,并且在本书中将其称为 OAuth 2.0。

SAML,于 2005 年发布,非常适合 Web 浏览器(至今仍然适用)。但是对于现代 Web 和原生应用程序(移动设备),SAML 需要进行严格的改进,这就是OAuth出现的原因。单页应用程序SPAs)和原生应用程序与传统的服务器端 Web 应用程序不同。SPAs 通过浏览器对服务器上暴露的 API 进行 AJAX/XHR 调用,并在客户端(浏览器)上执行许多其他操作。API 开发也发生了变化,从使用 XML 的重型 SOAP Web 服务到使用 JSON 的轻量级 REST over HTTP。

OAuth 还使您作为开发人员能够在不必透露用户密码的情况下访问最少的用户数据。它主要用于访问应用程序暴露的 API(REST),并通过委托授权功能来完成。

OAuth 支持各种应用程序类型,并将身份验证与授权解耦。

简而言之,这就是 OAuth 的工作原理:

  1. 希望访问资源的应用程序请求用户授予授权。

  2. 如果用户授权,应用程序将获得此协议的证明。

  3. 使用这个证明,应用程序去实际的服务器获取一个令牌。

  4. 使用此令牌,应用程序现在可以请求用户已授权的资源(API),同时提供证明。

上述步骤如下图所示:

图 5:OAuth 的功能

OAuth 通过使用访问令牌进行了微调,应用程序可以以 API 的形式获取用户信息。Facebook Connect(一个 SSO 应用程序,允许用户使用 Facebook 凭据与其他 Web 应用程序进行交互)使用这作为一种机制来公开一个端点(http(s)://<domain>/me),该端点将返回最少的用户信息。这在 OAuth 规范中从未清楚地存在过,这引发了Open ID ConnectOIDC),它结合了 OAuth2、Facebook Connect 和 SAML 2.0 的最佳部分。OIDC 引入了一个新的 ID 令牌(id_token),还有一个UserInfo端点,将提供最少的用户属性。OIDC 解决了 SAML 存在的许多复杂性,以及 OAuth2 的许多缺点。

深入研究 OAuth 和 OIDC 不在本书的范围之内。我相信我已经提供了足够的信息,您可以通过本节的其余部分进行导航。

设置项目

我们将在这里创建的示例代码与我们之前的示例有所不同。在这里,我们将使用Spring Initializrstart.spring.io/)创建基本项目,然后我们将注入适当的更改,使其能够使用提供程序(即 Google)进行登录。

使用 Spring Initializr 引导 Spring 项目

访问start.spring.io/并输入以下详细信息。确保选择正确的依赖项:

图 6:Spring Initializr 设置

单击“生成项目”按钮,将 ZIP 文件下载到您选择的文件夹中。执行以下unzip命令。我使用 Macintosh 运行所有示例应用程序,因此我将使用适用于此平台的命令(如果有的话):

unzip -a spring-boot-oauth-oidc-authentication.zip

在 pom.xml 中包含 OAuth 库

修改项目的pom.xml文件,添加以下依赖项:

<!-- Provided -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-tomcat</artifactId>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>org.apache.tomcat.embed</groupId>
  <artifactId>tomcat-embed-jasper</artifactId>
  <scope>provided</scope>
</dependency>
<!-- OAuth -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

在 application.properties 中设置提供程序详细信息

如果您运行应用程序(./mvnw spring-boot:run),然后在浏览器中导航到http://localhost:8080,您将看到一个默认的登录页面,如下所示。这个页面背后的所有魔术都是由 Spring Boot 和 Spring Security 为您完成的:

图 7:使用 Spring Initializr 创建的默认 Spring Boot + Spring Security 项目

打开application.properties文件(src/main/resources)并添加以下属性:

#Google app details
spring.security.oauth2.client.registration.google.client-id=1085570125650-l8j2r88b5i5gbe3vkhtlf8j7u3hvdu78.apps.googleusercontent.com
spring.security.oauth2.client.registration.google.client-secret=MdtcKp-ArG51FeqfAUw4K8Mp
#Facebook app details
spring.security.oauth2.client.registration.facebook.client-id=229630157771581
spring.security.oauth2.client.registration.facebook.client-secret=e37501e8adfc160d6c6c9e3c8cc5fc0b
#Github app details
spring.security.oauth2.client.registration.github.client-id=<your client id>
spring.security.oauth2.client.registration.github.client-secret=<your client secret>
#Spring MVC details
spring.mvc.view.prefix: /WEB-INF/views/
spring.mvc.view.suffix: .jsp

在这里,我们为每个提供程序声明了两个属性。我们将实现 Google 提供程序,但您可以添加任意数量的提供程序。只需添加这些属性,就会产生更多的魔法,您的登录页面将突然变成以下内容:

图 8:当修改 application.properties 文件时的 OAuth 默认登录页面

前面截图中显示的提供程序(链接)是根据application.properties文件中的配置而定的。它只查找两个属性,如下所示:

spring.security.oauth2.client.registration.<provider_name>.client-id=<client id>
spring.security.oauth2.client.registration.<provider_name>.client-secret=<client secret>

提供程序设置

在本示例中,我们将使用 Google 作为我们的提供程序。转到console.developers.google.com/并执行以下步骤:

  1. 创建项目。选择现有项目或创建新项目,如下图所示:

图 9:项目创建

  1. 创建凭据。选择新创建的项目(在下面的屏幕截图中,它显示在 Google APIs 徽标旁边),然后单击侧边菜单中的凭据链接,如下面的屏幕截图所示:

图 10:凭据创建 - 步骤 1

  1. 现在,单击“创建凭据”下拉菜单,如下面的屏幕截图所示:

图 11:凭据创建 - 步骤 2

  1. 从下拉菜单中,单击 OAuth 客户端 ID。这将导航您到下面屏幕截图中显示的页面。请注意,此时“应用程序类型”单选组将被禁用:

图 12:凭据创建 - 步骤 3

  1. 单击“配置同意屏幕”。您将被导航到以下页面:

图 13:凭据创建 - 步骤 4

  1. 输入相关详细信息(在填写表单时留出可选字段),如前图所示,然后单击“保存”按钮。您将被导航回到下图所示的页面。

这次,“应用程序类型”单选组将被启用:

图 14:凭据创建 - 步骤 5

  1. 将应用程序类型选择为 Web 应用程序,并输入相关详细信息,如前图所示。单击“创建”按钮,将显示以下弹出窗口:

图 15:凭据创建 - 步骤 6

现在您已经从 Google 那里获得了客户端 ID 和客户端密钥。将这些值复制并粘贴到application.properties文件的正确位置。

默认应用程序更改

为了与上一个示例保持一致,我们将对生成的默认应用程序进行更改,引入与上一个应用程序中看到的相同组件。这将帮助您详细了解应用程序。

HomeController 类

复制我们在上一个示例中创建的HomeController.java文件到一个新的包中。将欢迎消息更改为您想要的内容。

home.jsp 文件

将整个webapp文件夹从上一个示例中原样复制到此项目中。将页面标题更改为不同的内容,以便在运行应用程序时清楚地表明这确实是示例应用程序。

Spring Boot 主应用程序类更改

使您的应用程序类扩展SpringBootServletInitializer类。添加一个新的注释,如下所示,让您的 Spring Boot 应用程序知道一个新的控制器HomeController是一个必须扫描的组件:

@ComponentScan(basePackageClasses=HomeController.class)

运行应用程序

通过执行以下默认命令来运行您的应用程序:

./mvnw spring-boot:run

如果一切顺利,您应该能够单击 Google 链接,它应该将您导航到 Google 的登录页面。成功登录后,您将被重定向到home.jsp文件,如下面的屏幕截图所示:

图 16:使用 Google 作为 OAuth 提供程序登录

对 OAuth 的支持并不止于此,但我们必须停止,因为本书无法深入探讨框架提供的许多方面。

摘要

在本章中,我们看到了企业中常用的身份验证机制,即 SAML、LDAP 和 Spring Security 支持的 OAuth/OIDC,通过实际编码示例进行了支持。我们使用作为第二章的一部分构建的示例应用程序作为解释其他身份验证机制的功能和实现的基础。

然而,在我们的编码示例中,我们有意没有使用响应式编程。本章旨在通过使用熟悉的 Spring Web MVC 应用程序框架,让您了解每种身份验证机制的核心概念。我们将在《第五章》与 Spring WebFlux 集成中更详细地介绍响应式编程。

第四章:使用 CAS 和 JAAS 进行身份验证

本章将从上一章结束的地方继续,探讨 Spring Security 支持的其他身份验证机制,即 CAS 和 JAAS。同样,这也是一个完全动手编码的章节,我们将构建小型应用程序,其中大部分是从我们在第二章中构建的基础应用程序开始的,深入 Spring Security。这些身份验证机制在行业中广为人知,许多企业都将它们作为已建立的机制,用于对用户进行身份验证并允许访问他们的员工和消费者面向的许多应用程序。

每种身份验证机制都有一个项目,您可以在本书的 GitHub 页面上看到。但是,在本书中,我们只会涵盖样本代码的重要方面,以减少章节内的混乱。

在本章中,我们将涵盖以下主题:

  • CAS

  • Java 身份验证和授权服务

  • 凯尔伯斯

  • 自定义 AuthenticationEntryPoint

  • 密码编码器

  • 自定义过滤器

CAS

中央认证服务(CAS)是 Web 的单点登录/单点注销协议。它允许用户访问多个应用程序,同时只需向中央 CAS 服务器应用程序提供其凭据(如用户 ID 和密码)一次。

– CAS 协议规范

CAS 是一个开源的、平台无关的、支持各种知名协议的中央单点登录SSO)服务。Spring Security 对 CAS 有一流的支持,对于拥有中央 CAS 服务器的企业来说,实现非常简单。CAS 基于 Spring Framework,其架构非常简单,如下图所示:

图 1:CAS 架构(图表改编自 https://apereo.github.io

CAS 服务器是一个基于 Java Servlet 的应用程序,构建在 Spring Framework(Spring MVC 和 Spring Web Flow)上。它对 CAS 启用的服务进行身份验证并授予访问权限。

用户成功登录后,将创建一个 SSO 会话,并且服务器将发出票证授予票证TGT),并且该令牌将针对来自客户端的后续调用进行验证。

CAS 客户端是一个使用支持的协议(CAS、SAML、OAuth 等)与 CAS 通信的 CAS 启用应用程序。已经有许多语言支持 CAS,并且许多应用程序已经实现了这种方法。一些知名的应用程序是 Atlassian 产品(JIRA 和 Confluence)、Drupal 等。

以下图表显示了涉及 CAS 服务器和客户端的身份验证流程(序列图):

图 2:CAS 身份验证流程

现在让我们看一个实际的动手示例。我们将创建一个 CAS 服务器,然后创建一个客户端,该客户端使用 CAS 服务器进行连接并进行身份验证。

CAS 服务器设置

CAS 项目源代码可以在 GitHub 上找到,网址为github.com/apereo/cas。实际上并不需要检出源代码,构建 CAS 服务器,然后部署它。WAR 覆盖是一种方法,我们不是下载源代码并构建,而是获取一个预构建的 CAS Web 应用程序,然后根据需要自定义某些行为以实现我们的用例。我们将使用这种方法来设置我们的 CAS 服务器。此外,我们将使用基于 Maven 的 WAR 覆盖,可以在 GitHub 上找到,网址为github.com/apereo/cas-overlay-template

Git 克隆

启动您喜欢的命令提示符,并将 CAS 覆盖项目克隆到您想要的项目中。我将创建一个名为cas-sample的文件夹,在其中我将通过从cas-sample文件夹执行以下命令来在server文件夹中克隆服务器:

git clone https://github.com/apereo/cas-overlay-template.git server

添加额外的依赖项

CAS 服务器不允许任何客户端连接。每个客户端都必须在所需的 CAS 服务器上注册。我们可以使用多种机制将客户端注册到服务器。我们将使用 JSON/YML 配置将客户端注册到服务器。继续并将以下依赖项添加到您刚刚克隆的服务器项目的pom.xml文件中:

<dependency>
   <groupId>org.apereo.cas</groupId>
   <artifactId>cas-server-support-json-service-registry</artifactId>
   <version>${cas.version}</version>
</dependency>
<dependency>
   <groupId>org.apereo.cas</groupId>
   <artifactId>cas-server-support-yaml-service-registry</artifactId>
   <version>${cas.version}</version>
</dependency>

pom.xml文件中的大多数版本由父 POM 管理。

在项目中设置资源文件夹

server项目中,创建一个名为src/main/resources的文件夹。将server文件夹中的etc文件夹复制到src/main/resources中:

mkdir -p src/main/resources
cp -R etc src/main/resources

创建 application.properties 文件

创建一个名为application.properties的文件:

touch src/main/resources/application.properties

现在在application.properties文件中填写以下细节:

server.context-path=/cas
server.port=6443

server.ssl.key-store=classpath:/etc/cas/thekeystore
server.ssl.key-store-password=changeit
server.ssl.key-password=changeit

cas.server.name: https://localhost:6443
cas.server.prefix: https://localhost:6443/cas

cas.adminPagesSecurity.ip=127\.0\.0\.1

cas.authn.accept.users=casuser::password

上述文件设置了端口和 SSL 密钥库的值(在设置 CAS 服务器时非常重要),还设置了 CAS 服务器的config文件夹。显然,我们需要按照此文件中指示的方式创建一个密钥库。

请注意,覆盖项目中有一个文件,即build.sh文件,其中包含大部分这些细节。我们手动执行这些操作是为了更清楚地理解。

application.properties中的最后一行设置了一个测试用户,凭据为casuser/password,可用于登录 CAS 服务器进行各种演示目的。这种方法不建议在生产环境中使用。

创建本地 SSL 密钥库

在 shell 中导航到cas-sample/server/src/main/resources/etc/cas文件夹,并执行以下命令:

keytool -genkey -keyalg RSA -alias thekeystore -keystore thekeystore -storepass password -validity 360 -keysize 2048

以下图显示了在命令提示符窗口中成功执行上述命令:

图 3:SSL 密钥库的创建

重要的是要注意,为了使 SSL 握手正常工作,生成密钥库时大多数值都设置为 localhost。这是一个重要的步骤,需要严格遵循。

创建供客户端使用的.crt 文件

为了使客户端连接到 CAS 服务器,我们需要从生成的密钥库中创建一个.crt文件。在相同的文件夹(cas-sample/server/src/main/resources/etc/cas)中,运行以下命令:

keytool -export -alias thekeystore -file thekeystore.crt -keystore thekeystore

当要求输入密码时,请提供相同的密码(我们已将密码设置为password)。执行上述命令将创建thekeystore.crt文件。

将.crt 文件导出到 Java 和 JRE cacert 密钥库

执行以下命令以查找您的 Java 安装目录:

/usr/libexec/java_home

或者,直接执行以下命令将.crt文件添加到 Java cacerts:

keytool -import -alias thekeystore -storepass password -file thekeystore.crt -keystore "$(/usr/libexec/java_home)\jre\lib\security\cacerts"

以下图显示了在命令提示符窗口中成功执行上述命令:

图 4:将.crt 文件导出到 Java 密钥库

在设置客户端时,请确保使用的 JDK 与我们已添加.crt文件的 JDK 相同。为了将证书添加到 Java 上,建议重新启动机器。

构建 CAS 服务器项目并运行它

cas-sample/cas-server文件夹中,执行以下两个命令:

./build.sh package
./build.sh run

如果一切顺利,如下图所示,您应该看到一条日志消息,其中显示 READY:

图 5:CAS 服务器准备就绪日志

现在打开浏览器,导航到 URL https://localhost:6443/cas。这将导航您到 CAS 服务器的默认登录表单。输入默认凭据(casuser/Mellon)即可登录。大多数浏览器会显示连接不安全。将域名添加为异常情况,之后应用程序将正常工作:

图 6:默认 CAS 服务器登录表单

使用演示测试用户(testcasuser/password)登录,您应该已登录并导航到用户主页。

将客户端注册到 CAS 服务器

如前所述,每个客户端都必须在 CAS 服务器上注册,以允许参与 SSO。本节显示了如何将客户端注册到 CAS 服务器。

JSON 服务配置

客户/服务可以通过多种方式注册到 CAS 服务器。我们将在这里使用 JSON 配置,并已在之前的步骤中将依赖项包含到我们的pom.xml文件中。除了 JSON 之外,还存在其他格式,如 YAML、Mongo、LDAP 等。

src/main/resources文件夹中创建一个名为clients的新文件夹。在新创建的文件夹中创建一个新文件,内容如下:

--- !<org.apereo.cas.services.RegexRegisteredService>
serviceId: "^(http?|https?)://.*"
name: "YAML"
id: 5000
description: "description"
attributeReleasePolicy: !<org.apereo.cas.services.ReturnAllAttributeReleasePolicy> {}
accessStrategy: !<org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy>
 enabled: true
 ssoEnabled: true

将文件保存为newYmlFile-5000.yml。让我们详细了解一些重要属性:

  • serviceId:客户端想要连接到 CAS 服务器的 URL,以正则表达式模式表示。在我们的示例中,我们指的是运行在端口9090上的客户端 Spring Boot 应用程序,它连接到 CAS 服务器。

  • id:此配置的唯一标识符。

其他可配置属性在官方网站goo.gl/CGsDp1上有文档记录。

附加的 application.properties 文件更改

在此步骤中,我们让 CAS 服务器了解 YML 配置的使用以及在服务器中查找这些 YML 的位置。将以下属性添加到application.properties文件中:

cas.serviceRegistry.yaml.location=classpath:/clients

将 CAS 相关的配置属性分离到不同的属性文件中是一个好习惯。因此,继续创建一个cas.properties文件,并在其中包含 CAS 相关属性。

CAS 客户端设置

我们将使用 Spring Initializr 来创建 CAS 客户端项目设置。我们之前使用了类似的方法。让我们再次看一下。

使用 Spring Initializr 引导 Spring 项目

访问start.spring.io/,并输入如下图所示的详细信息。确保选择正确的依赖项:

图 7:用于创建 secured-cas-client 项目的 Spring Initializr

单击“生成项目”按钮,将 ZIP 文件下载到您选择的文件夹中(我将把它保存在cas-sample文件夹中)。执行以下unzip命令。我在 macOS 上运行所有示例应用程序,因此我将使用适用于此平台的命令(如果有的话):

unzip -a spring-boot-cas-client.zip

在 pom.xml 中包含 CAS 库

通过添加以下依赖项修改项目的pom.xml

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-cas</artifactId>
</dependency>

更改 application.properties 文件

为了确保我们不使用任何其他常用端口,我们将设置客户端监听端口9090。在 CAS 服务器中,我们还配置了客户端将监听端口9090。将以下属性添加到application.properties文件中:

server.port=9090

附加的 bean 配置

我们现在将设置各种 bean,CAS Spring Security 模块需要。

ServiceProperties bean

通过设置此 bean 来告诉 CAS 这是您的 CAS 客户端/服务。打开SpringBootCasClientApplication.java并添加以下 bean 定义:

@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService("http://localhost:9090/login/cas");
    serviceProperties.setSendRenew(false);
    return serviceProperties;
}

配置的 URLhttp://localhost:9090/login/cas将在内部映射到CasAuthenticationFilter。参数sendRenew设置为false。设置为false时,这告诉登录服务每次都需要用户名/密码才能访问服务。它还允许用户在不必再次输入用户名/密码的情况下访问所有服务/客户端。注销时,用户将自动注销所有服务。

AuthenticationEntryPoint bean

看一下以下代码。相当简单直接,不是吗?这是我们告诉的 CAS 服务器运行的位置。当用户尝试登录时,应用程序将被重定向到此 URL:

@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
    CasAuthenticationEntryPoint casAuthEntryPoint = new CasAuthenticationEntryPoint();
    casAuthEntryPoint.setLoginUrl("https://localhost:6443/cas/login");
    casAuthEntryPoint.setServiceProperties(serviceProperties());
    return casAuthEntryPoint;
}

TicketValidator bean

当客户端应用程序获得已经分配给特定用户的票证时,将使用此 bean 来验证其真实性:

@Bean
public TicketValidator ticketValidator() {
    return new Cas30ServiceTicketValidator("https://localhost:6443/cas");
}

CasAuthenticationProvider bean

将之前声明的所有 bean 绑定到认证提供者 bean。我们将从UserDetailsService中提供的静态列表中加载用户。在生产环境中,这将指向数据库:

@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
  CasAuthenticationProvider provider = new CasAuthenticationProvider();
  provider.setServiceProperties(serviceProperties());
  provider.setTicketValidator(ticketValidator());
  provider.setUserDetailsService((s) -> new User("casuser", "password",
        true, true, true, true,
        AuthorityUtils.createAuthorityList("ROLE_ADMIN")));
  provider.setKey("CAS_PROVIDER_PORT_9090");
  return provider;
}

现在我们准备设置非常重要的 Spring Security 配置。

设置 Spring Security

让我们将在上一步中完成的 bean 引用带入 Spring Security 配置文件中。创建一个名为SpringSecurityConfig的新的 Java 文件并添加成员变量。之后,创建一个带有@Autowired注解的构造函数如下:

private AuthenticationProvider authenticationProvider;
private AuthenticationEntryPoint authenticationEntryPoint;

@Autowired
public SpringSecurityConfig(CasAuthenticationProvider casAuthenticationProvider,
                     AuthenticationEntryPoint authenticationEntryPoint) {
   this.authenticationProvider = casAuthenticationProvider;
   this.authenticationEntryPoint = authenticationEntryPoint;
}

当用户访问由 CAS 服务器保护的客户端应用程序时,配置的 beanAuthenticationEntryPoint将被触发,并且用户将被带到在此 bean 中配置的 CAS 服务器 URL。一旦用户输入凭证并提交页面,CAS 服务器将对用户进行身份验证并创建服务票证。现在,该票证被附加到 URL,并且用户将被带到请求的客户端应用程序。客户端应用程序使用TicketValidator bean 来验证 CAS 服务器的票证,并且如果有效,则允许用户访问请求的页面。

在配置 HTTP 安全性之前,我们需要重写一些重要的方法。第一个方法使用AuthenticationManagerBuilder,我们告诉它使用我们的AuthenticationProvider。请按照以下方式创建该方法:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(authenticationProvider);
}

我们现在重写另一个方法,指示AuthenticationManager将我们创建的AuthenticationProvider放入其中:

@Override
protected AuthenticationManager authenticationManager() throws Exception {
    return new ProviderManager(Arrays.asList(authenticationProvider));
}

我们现在准备创建一个名为CasAuthenticationFilter的过滤器(作为一个 bean),它实际上拦截请求并进行 CAS 票证验证。

创建 CasAuthenticationFilter bean

创建CasAuthenticationFilter bean 非常简单,因为我们只需将我们创建的serviceProperties分配给CasAuthenticationFilter

@Bean
public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties serviceProperties) throws Exception {
    CasAuthenticationFilter filter = new CasAuthenticationFilter();
    filter.setServiceProperties(serviceProperties);
    filter.setAuthenticationManager(authenticationManager());
    return filter;
}

设置控制器

这是我们 CAS 客户端项目设置的最终设置。我们将有一个包含指向受保护页面链接的未受保护页面。当访问受保护页面时,CAS SSO 启动,用户被导航到 CAS 认证页面。一旦使用凭证(casuser/password)登录,用户将被带到受保护页面,我们将显示经过身份验证的用户名。

我们将创建一个ndexController,它具有根文件夹路由(/)。这将把用户导航到index.html页面。

在一个新的包中创建IndexController.java(最好在 controllers 包中):

@Controller
public class IndexController {
    @GetMapping("/")
    public String index() {
        return "index";
    }
}

src/resources/templates文件夹中创建index.html文件,内容如下:

<!DOCTYPE html>
<html >
<head>
   <meta charset="UTF-8" />
   <title>Spring Security CAS Sample - Unsecured page</title>
</head>
<body>
<h1>Spring Security CAS Sample - Unsecured page</h1>
<br>
<a href="/secured">Go to Secured Page</a>
</body>
</html>

现在在相同的 controllers 包中创建一个名为CasController.java的新控制器。我们将映射所有受保护的页面,并在此控制器中设置各种请求映射。在控制器类中,复制以下代码片段:

@Controller
@RequestMapping(value = "/secured")
public class CasController {

   @GetMapping
   public String secured(ModelMap modelMap) {
     Authentication auth = SecurityContextHolder.getContext().getAuthentication();
     if( auth != null && auth.getPrincipal() != null
         && auth.getPrincipal() instanceof UserDetails) {
       modelMap.put("authusername", ((UserDetails) auth.getPrincipal()).getUsername());
     }
     return "secured";
   }
}

创建一个名为secured.html的新 HTML 文件,内容如下。这是我们的受保护页面,将显示经过身份验证的用户名:

<!DOCTYPE html>
<html >
<head>
   <meta charset="UTF-8" />
   <title>Spring Security CAS Sample - Secured page</title>
</head>
<body>
<h1>Spring Security CAS Sample - Secured page</h1>
<br>
<h3 th:text="${authusername} ? 'Hello authenticated user, ' + ${authusername} + '!' : 'Hello non-logged in user!'">Hello non-logged in user!</h3>
</body>
</html>

运行应用程序

启动 CAS 服务器(在cas-server中运行./build.sh run)。之后,通过执行./mvnw spring-boot:run启动 spring boot 项目(secured-cas-client)。将浏览器导航到http://localhost:9090。这将带用户到index.html,当他们点击链接(导航到secured.html页面)时,用户将被带到 CAS 认证页面。要进行认证,请输入 CAS 凭证,然后将票证设置为查询字符串,然后您将被带到受保护的页面。受保护的页面将使用 CAS 服务器验证票证,然后显示用户名。

通过这样,我们完成了使用 Spring Security 的 CAS 示例。在下一节中,类似于 CAS,我们将详细介绍如何使用 JAAS 认证来使用 Spring Security。

Java 身份验证和授权服务

Java 身份验证和授权服务JAAS)(docs.oracle.com/javase/6/docs/technotes/guides/security/jaas/JAASRefGuide.html)实现了标准可插拔身份验证模块PAM)框架的 Java 版本。它作为 J2SDK(1.3)的可选包(扩展)引入,然后集成到 J2SDK 1.4 中。

JAAS 是一个标准库,为您的应用程序提供以下功能:

  • 通过提供凭证(用户名/密码-主体)来表示身份(主体)。

  • 一个登录服务,将回调您的应用程序以从用户那里收集凭证,然后在成功身份验证后返回一个主体。

  • 在成功身份验证后,向用户授予必要的授权的机制:

图 8:JAAS 的工作原理

如前图所示,JAAS 具有大多数内置登录机制的预定义登录模块。可以根据应用程序要求导入或构建自定义登录模块。JAAS 允许应用程序独立于实际的身份验证机制。它是真正可插拔的,因为可以集成新的登录模块而无需更改应用程序代码。

JAAS 很简单,流程如下:

  • 该应用程序实例化一个LoginContext对象,并调用适当的(由配置控制的)LoginModule,执行身份验证。

  • 一旦身份验证成功,主体(运行代码的人)将通过LoginModule更新为主体凭证

  • 在那之后,JAAS 启动授权过程(使用标准 Java SE 访问控制模型)。访问是基于以下内容授予的:

  • 代码源:代码的来源地和签署代码的人

  • 用户:运行代码的人(也称为主体

现在我们对 JAAS 及其工作原理有了大致的了解,接下来我们将通过以下部分中的示例来查看使用 Spring Security 的 JAAS 的工作原理。

设置项目

我们要构建的示例应用程序与第三章开始时创建的应用程序非常相似,即使用 SAML、LDAP 和 OAuth/OIDC 进行身份验证。许多方面都是相似的,但在细微的方式上有所不同。每个步骤都将得到解释;但是,有时我们不会详细介绍,因为我们已经在之前的示例中看到了一些方面。

设置 Maven 项目

我们将使用 IntelliJ IDE 创建一个 Maven 项目。在您的pom.xml文件中添加以下依赖项和构建设置:

<groupId>com.packtpub.book.ch04.springsecurity</groupId>
<artifactId>jetty-jaas-authentication</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
   <maven.compiler.source>1.8</maven.compiler.source>
   <maven.compiler.target>1.8</maven.compiler.target>
   <failOnMissingWebXml>false</failOnMissingWebXml>
</properties>
<dependencies>
   <!--Spring Security Dependencies-->
   <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-web</artifactId>
       <version>5.0.4.RELEASE</version>
   </dependency>
   <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-config</artifactId>
       <version>5.0.4.RELEASE</version>
   </dependency>
   <!--Spring Framework Dependencies-->
   <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-context</artifactId>
       <version>5.0.4.RELEASE</version>
   </dependency>
   <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-webmvc</artifactId>
       <version>5.0.4.RELEASE</version>
   </dependency>
   <!-- JSP, JSTL and Tag Libraries-->
   <dependency>
       <groupId>javax.servlet</groupId>
       <artifactId>javax.servlet-api</artifactId>
       <version>3.1.0</version>
       <scope>provided</scope>
   </dependency>
   <dependency>
       <groupId>javax.servlet</groupId>
       <artifactId>jstl</artifactId>
       <version>1.2</version>
       <scope>provided</scope>
   </dependency>
   <dependency>
       <groupId>javax.servlet.jsp</groupId>
       <artifactId>javax.servlet.jsp-api</artifactId>
       <version>2.3.1</version>
       <scope>provided</scope>
   </dependency>
   <dependency>
       <groupId>javax.servlet.jsp.jstl</groupId>
       <artifactId>javax.servlet.jsp.jstl-api</artifactId>
       <version>1.2.1</version>
   </dependency>
   <dependency>
       <groupId>taglibs</groupId>
       <artifactId>standard</artifactId>
       <version>1.1.2</version>
   </dependency>
   <!--SLF4J and logback-->
   <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
       <version>1.7.25</version>
   </dependency>
   <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>jcl-over-slf4j</artifactId>
       <version>1.7.25</version>
   </dependency>
   <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-core</artifactId>
       <version>1.2.3</version>
   </dependency>
   <dependency>
       <groupId>ch.qos.logback</groupId>
       <artifactId>logback-classic</artifactId>
       <version>1.2.3</version>
   </dependency>
</dependencies>

<build>
   <plugins>
       <plugin>
           <groupId>org.eclipse.jetty</groupId>
           <artifactId>jetty-maven-plugin</artifactId>
           <version>9.4.10.v20180503</version>
       </plugin>
   </plugins>
</build>

我们添加 Spring 框架、Spring 安全、JSP/JSTL 和日志框架(SLF4J 和 Logback)的依赖项。我们将使用嵌入式 jetty 服务器(查看构建部分)来运行我们的应用程序。

设置 LoginModule

LoginModule负责对用户进行身份验证。我们将创建自己的名为JaasLoginModuleLoginModule,然后实现login方法。作为示例应用程序,我们的登录逻辑非常简单。必须实现LoginModule接口,才能编写自定义的登录模块。

创建一个类JaasLoginModule.java(实现LoginModule),并实现所有方法。在这个类中,我们将专注于两个重要的方法。在initialize方法中,我们获取所有必要的信息,如用户名/密码/主体,这些信息存储为字段变量,以便在我们的主要login方法中使用:

// Gather information and then use this in the login method
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, 
            ?> sharedState, Map<String, ?> options) {
    this.subject = subject;

    NameCallback nameCallback = new NameCallback("Username:");
    PasswordCallback passwordCallback = new PasswordCallback("Password:", false);
    try {
        callbackHandler.handle(new Callback[] { nameCallback, passwordCallback });
    } catch (IOException e) {
        e.printStackTrace();
    } catch (UnsupportedCallbackException e) {
        e.printStackTrace();
    }
    username = nameCallback.getName();
    password = new String(passwordCallback.getPassword());
}

login方法中,我们将使用initialize方法中存储的值进行登录。在我们的情况下,如果硬编码的用户名/密码有效,则在主体中设置主体:

// Code where actual login happens. Implement any logic as required by your application
// In our sample we are just doing a hard-coded comparison of username and password
@Override
public boolean login() throws LoginException {
    if (username == null || (username.equalsIgnoreCase("")) ||
        password == null || (password.equalsIgnoreCase(""))) {
        throw new LoginException("Username and password is mandatory.");
    } else if (username.equalsIgnoreCase("admin") &&        
        password.equalsIgnoreCase("password")) {
        subject.getPrincipals().add(new JaasPrincipal(username));
        return true;
    } else if (username.equalsIgnoreCase("user") && 
        password.equalsIgnoreCase("password")) {
        subject.getPrincipals().add(new JaasPrincipal(username));
        return true;
    }
    return false;
}

设置自定义主体

我们通过实现java.security.Principal接口创建了我们自己的自定义主体类。这是一个非常简单的类,我们通过构造函数接收用户名,然后在getName方法中使用它返回:

public class JaasPrincipal implements Principal, Serializable {
    private String username;
    public JaasPrincipal(String username) {
        this.username = username;
    }
    @Override
    public String getName() {
        return "Authenticated_"+this.username;
    }
}

设置自定义 AuthorityGranter

AuthorityGranter被委托为经过身份验证的用户提供相关角色。我们将通过实现org.springframework.security.authentication.jaas.AuthorityGranter来创建我们自己的自定义类:

public class JaasAuthorityGranter implements AuthorityGranter {
    @Override
    public Set<String> grant(Principal principal) {
        if (principal.getName().equalsIgnoreCase("Authenticated_admin")) {
            return Collections.singleton("ROLE_ADMIN");
        } else if (principal.getName().equalsIgnoreCase("Authenticated_user")) {
            return Collections.singleton("ROLE_USER");
        }
        return Collections.singleton("ROLE_USER");
    }
}

作为一个示例实现,在这个类中,我们查看已登录用户的用户名并为其授予硬编码角色。在实际应用程序中,我们将在这里做一些更严肃的事情,实际上查询数据库,然后为已登录用户授予适当的角色。

配置文件

我们需要在示例中有许多配置文件(Java 配置),其中大部分已经在前面涵盖过。对于剩下的文件(尚未涵盖),我们要么快速浏览它们,要么在涵盖它们时进行详细讨论。

应用程序配置

我们在这里没有任何特定于应用程序的配置,但在您的应用程序中拥有这样的文件总是很好的。我们有ApplicationConfig.java作为我们的应用程序级 Java 配置(它里面没有任何内容)。

Spring MVC 配置

如下所示,我们将创建 Spring MVC 特定的 Java 配置(SpringMVCConfig.java):

@Configuration
@EnableWebMvc
@ComponentScan( basePackages = "com.packtpub")
public class SpringMVCConfig implements WebMvcConfigurer {
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp().prefix("/WEB-INF/views/").suffix(".jsp");
    }
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login");
    }
}

在这个配置中,设置视图的前缀后缀。确保您的登录视图控制器被显式添加,因为我们的控制器中没有定义路由(我们稍后会看到控制器)。

Spring Security 配置

这是一个非常重要的配置示例。

我们将创建一个AuthenticationProviderbean。我们将使用我们自定义的LoginModule,然后使用org.springframework.security.authentication.jaas.DefaultJaasAuthenticationProvider来设置一些内容。然后将此身份验证提供程序设置为全局提供程序。任何请求都将通过此提供程序(SpringSecurityConfig.java):

@Bean
DefaultJaasAuthenticationProvider jaasAuthenticationProvider() {
   AppConfigurationEntry appConfig = new AppConfigurationEntry("com.packtpub.book.ch04.springsecurity.loginmodule.JaasLoginModule",
           AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, new HashMap());

   InMemoryConfiguration memoryConfig = new InMemoryConfiguration(new AppConfigurationEntry[] { appConfig });

   DefaultJaasAuthenticationProvider def = new DefaultJaasAuthenticationProvider();
   def.setConfiguration(memoryConfig);
   def.setAuthorityGranters(new AuthorityGranter[] {jaasAuthorityGranter});
   return def;
}

//We are configuring jaasAuthenticationProvider as our global AuthenticationProvider
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
   auth.authenticationProvider(jaasAuthenticationProvider());
}

下一个最重要的方法是configure方法,在其中我们将确保设置需要受保护的正确路径,并且我们还将设置一些重要的配置:

// Setting up our HTTP security
@Override
protected void configure(HttpSecurity http) throws Exception {

   // Setting up security
   http.authorizeRequests()
           .regexMatchers("/admin/.*").hasRole("ADMIN")
           .anyRequest().authenticated().and().httpBasic();

   // Setting our login page and to make it public
   http.formLogin().loginPage("/login").permitAll();
   // Logout configuration
   http.logout().logoutSuccessUrl("/");
   // Exception handling, for access denied
   http.exceptionHandling().accessDeniedPage("/noaccess");
}

控制器

我们只有一个控制器,我们将在其中配置所有路由(JaasController.java):

@Controller
public class JaasController {
    @RequestMapping(value="/", method = RequestMethod.GET)
    public ModelAndView userPage() {
        ModelAndView modelAndView = new ModelAndView("user");
        return modelAndView;
    }
    @RequestMapping(value = "/admin/moresecured", method = RequestMethod.GET)
    public ModelAndView adminPage(HttpServletRequest request) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("moresecured");
        return modelAndView;
    }
    @RequestMapping(value="/noaccess", method = RequestMethod.GET)
    public ModelAndView accessDenied() {
        ModelAndView modelAndView = new ModelAndView("noaccess");
        return modelAndView;
    }
}

设置页面

我们有一些琐碎的页面。我不想在这里粘贴代码,因为它相当容易理解:

  • login.jsp:我们自定义的登录页面,用于从最终用户那里收集用户名和密码。

  • user.jsp:在示例中设置为根的页面。登录后,用户将被导航到此页面。我们只是打印会话 ID 和用户名,以展示登录。

  • moresecured.jsp:这只是为了展示用户角色的重要性。只有具有ADMIN角色的用户才能访问此页面。

  • noaccess.jsp:当用户无法访问任何页面时,这个虚拟页面就会出现。

可以在书的 GitHub 页面的jetty-jaas-authentication项目中找到完整的示例项目。

运行应用程序

从项目的根目录执行以下命令:

mvn jetty:run

打开浏览器,导航到http://localhost:8080。您将看到一个看起来很简陋的登录页面。输入用户名/密码(admin/password 或 user/password),然后您将被导航到根页面(user.jsp)。

这完成了我们使用 Spring Security 的 JAAS 示例。如上图所示,JAAS 可以用于使用其他协议进行身份验证。其中一个众所周知的机制是使用 Kerberos 协议进行身份验证。下一节简要介绍了 JAAS 如何用于实现基于 Kerberos 的身份验证的大致想法。

Kerberos

JAAS 提供了许多内置类型的LoginModule,其中之一是rb5LoginModule,用于使用 Kerberos 协议对用户进行身份验证。因此,确实可以使用 JAAS 方法来轻松实现基于 Spring 的应用程序中的 Kerberos 身份验证。

让我们深入了解一些关于身份验证的重要细节。

自定义身份验证入口点

在将响应发送回客户端之前,可以使用自定义AuthenticationEntryPoint来设置必要的响应头、内容类型等。

org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint类是一个内置的AuthenticationEntryPoint实现,用于启动基本身份验证。可以通过实现org.springframework.security.web.AuthenticationEntryPoint接口来创建自定义入口点。以下是一个示例实现:

@Component
public final class CustomAuthenticationEntryPoint implements 
        AuthenticationEntryPoint {
    @Override
    public void commence(final HttpServletRequest request, final 
            HttpServletResponse response, final AuthenticationException 
        authException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

当客户端在没有身份验证的情况下访问资源时,此入口点会启动并抛出 401 状态码(未经授权)。

在 Spring Security Java 配置文件中,确保configure方法定义了这个自定义AuthenticationEntryPoint,如下面的代码片段所示:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/public").permitAll()
        .anyRequest().authenticated()
        .and()
        .httpBasic()
        .authenticationEntryPoint(customAuthenticationEntryPoint);
}

多个 AuthenticationEntryPoint

Spring Security 确实允许您为应用程序配置多个AuthenticationEntryPoint,如果需要的话。

自 Spring Security 3.0.2 以来,org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint查看配置中声明的所有AuthenticationEntryPoint并执行它们。

自 Spring Security 5.x 以来,我们有org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint,它使用反应性数据类型,并为其执行带来了异步性质。

Spring Security 配置中的defaultAuthenticationEntryPointFor()方法也可以用于设置查看不同 URL 匹配的多个入口点(请参见以下代码片段):

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
    .authorizeRequests()
        .antMatchers("/public").permitAll()
        .anyRequest().authenticated()
        .and()
        .httpBasic()
    .defaultAuthenticationEntryPointFor(
        loginUrlAuthenticationEntryPointUser(),
        new AntPathRequestMatcher("/secured/user/**"))
    .defaultAuthenticationEntryPointFor(
        loginUrlAuthenticationEntryPointAdmin(),
        new AntPathRequestMatcher("/secured/admin/**"));
}
@Bean
public AuthenticationEntryPoint loginUrlAuthenticationEntryPointUser(){
    return new LoginUrlAuthenticationEntryPoint("/userAuth");
}      
@Bean
public AuthenticationEntryPoint loginUrlAuthenticationEntryPointAdmin(){
    return new LoginUrlAuthenticationEntryPoint("/adminAuth");
}

PasswordEncoder

在 Spring Security 5 之前,该框架只允许应用程序中有一个PasswordEncoder,并且还有弱密码编码器,如 MD5 和 SHA。这些编码器也没有动态盐,而是更多的静态盐需要提供。通过 Spring Security 5,在这个领域发生了巨大的变化,新版本中的密码编码概念采用了委托,并允许在同一应用程序中进行多次密码编码。已编码的密码有一个前缀标识符,指示使用了什么算法(请参见以下示例):

{bcrypt}$2y$10$zsUaFDpkjg01.JVipZhtFeOHpC2/LCH3yx6aNJpTNDOA8zDqhzgR6

此方法允许根据需要在应用程序中使用多种编码。如果没有提到标识符,这意味着它使用默认编码器,即StandardPasswordEncoder

一旦您决定密码编码,这可以在AuthenticationManager中使用。一个示例是以下代码片段:

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth
        .inMemoryAuthentication()
        .passwordEncoder(new StandardPasswordEncoder())    
        .withUser("user")
        .password("025baf3868bc8f785267d4aec1f02fa50809b7f715576198eda6466")
        .roles("USER");
}

如前所述,Spring Security 5 通过引入DelegationPasswordEncoder引入了委托方法。DelegatingPasswordEncoder已取代PasswordEncoder,并可以通过以下两种方法创建:

  • 方法 1:
PasswordEncoder passwordEncoder = 
    PasswordEncoderFactories.createDelegatingPasswordEncoder();
passwordEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder());
  • 方法 2:
String defaultEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(defaultEncode, new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(defaultEncode, encoders);

DelegatingPasswordEncoder允许针对旧的编码方法验证密码,并在一段时间内升级密码,而无需任何麻烦。这种方法可以用于在用户进行身份验证时自动升级密码(从旧编码到新编码)。

为了使暴力攻击更加困难,我们在编码时还可以提供一个随机字符串。这个随机字符串称为。盐文本包含在PasswordEncoder中,如下面的代码片段所示:

auth
    .inMemoryAuthentication()
    .passwordEncoder(new StandardPasswordEncoder(“random-text-salt”));

自定义过滤器

正如前面所解释的,Spring Security 是基于 servlet 过滤器工作的。有许多内置的 servlet 过滤器几乎可以完成所有必要的功能。如果需要,Spring Security 确实提供了一种机制来编写自定义过滤器,并可以在过滤器链执行的正确位置插入。通过扩展org.springframework.web.filter.GenericFilterBean来创建自己的过滤器,如下面的代码片段所示:

public class NewLogicFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        // Custom logic
        chain.doFilter(request, response);
    }
}

一旦您创建了自己的过滤器,请将其插入到 Spring Security 配置文件中的过滤器链中,如下所示:

@Configuration
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(new NewLogicFilter(), 
                BasicAuthenticationFilter.class);
    }
}

您可以将新的过滤器放置在过滤器链中的任何位置,之前、之后或特定位置。如果您想要扩展现有的过滤器,也可以这样做。

摘要

在本章中,我们通过实际编码示例介绍了 Spring Security 支持的 CAS 和 JAAS 两种认证机制。同样,我们使用了作为第二章的一部分构建的示例应用程序作为基础,以解释其他认证机制的工作和实现。然后,我们介绍了 Spring Security 中的一些重要概念和可定制性。

在本章中,我们故意没有在编码示例中使用响应式编程。本章的目的是让您通过使用熟悉的 Spring Web MVC 应用程序框架来理解每个 CAS 和 JAAS 认证机制的核心概念。我们将在第五章中更详细地介绍响应式编程,即与 Spring WebFlux 集成。我们将在下一章中介绍 Spring WebFlux,并在适当的时候实现 Spring Security。在阅读第五章的主要内容时,您将清楚地了解,使本章中的代码示例符合响应式是非常容易的。

第五章:与 Spring WebFlux 集成

Spring Framework 5 引入的新功能之一是引入了一个新的响应式 Web 应用程序框架,Spring WebFlux。WebFlux 与成熟的 Web 应用程序框架 Spring MVC 并存。该书旨在介绍 Spring Security 的响应式部分,其中 Spring WebFlux 是核心组件之一。

使您的应用程序具有响应式特性会为您的应用程序带来异步性。传统的 Java 应用程序使用线程来实现应用程序的并行和异步特性,但是对于 Web 应用程序来说,使用线程是不可伸缩和高效的。

本章首先介绍了 Spring MVC 和 Spring WebFlux 之间的核心区别,然后深入探讨了 Spring Security 模块以及如何将响应式方面引入其中。

在本章中,我们将涵盖以下主题:

  • Spring MVC 与 WebFlux

  • Spring 5 中的响应式支持

  • Spring WebFlux

  • Spring WebFlux 身份验证架构

  • Spring WebFlux 授权

  • 示例项目

  • 自定义

Spring MVC 与 WebFlux

Spring WebFlux 作为 Spring 5 的一部分引入,为现有的 Spring MVC 带来了一个新的替代方案。Spring WebFlux 引入了非阻塞的事件循环式编程,以提供异步性。

事件循环是由 Node.js 引入并因此而出名。Node.js 能够使用单线程的 JavaScript 执行非阻塞操作,通过在可能的情况下将操作卸载到系统内核。内核是多线程的,能够执行这些卸载的操作,并在成功执行后通过回调通知 Node.js。有一个不断运行的进程来检查调用堆栈(其中堆叠了需要执行的操作),并以先进先出FIFO)的方式继续执行进程。如果调用堆栈为空,它会查看事件队列中的操作。它会将它们拾起,然后将它们移动到调用堆栈中以供进一步执行。

以下图显示了两个 Web 应用程序框架中的内容:

图 1:Spring MVC 和 Spring WebFlux

如前图所示,Spring MVC 基于 Servlet API(在线程池上工作),而 Spring WebFlux 基于响应式流(它基于事件循环机制)。然而,这两个框架都支持常用的注解,如@Controller,并且也支持一些知名的服务器。

让我们在下图中并排看一下 Spring MVC 和 Spring WebFlux 的工作方式:

图 2:Spring MVC 和 Spring WebFlux 的工作方式

正如您所看到的,这两个框架的工作方式的根本区别在于 Spring MVC 是阻塞的,而 Spring WebFlux 是非阻塞的。

在 Spring WebFlux 中,Servlet API 充当适配器层,使其能够支持诸如TomcatJetty等 Servlet 容器以及UndertowNetty等非 Servlet 运行时。

Spring MVC 包括同步 API(过滤器、Servlet 等)和阻塞 I/O(InputStreamOutputStream等),而 Spring WebFlux 包括异步 API(WebFilterWebHandler等)和非阻塞 I/O(Reactor Mono 用于0..1元素和 Reactor Flux 用于0..N元素)。

Spring WebFlux 支持各种异步和响应式 API,即 Java 9 Flow API、RxJava、Reactor 和 Akka Streams。默认情况下,它使用 Spring 自己的响应式框架 Reactor,并且它的工作相当出色:

图 3:Spring WebFlux 响应式 API 支持

如前所述,Spring WebFlux 是作为 Spring MVC 的一种替代方案引入的。这并不意味着 Spring MVC 已经被弃用。在 Spring MVC 中编写的应用程序可以继续在相同的堆栈上运行,无需迁移到 Spring WebFlux。如果需要,我们可以通过运行一个响应式客户端来调用远程服务,将响应式编码实践引入现有的 Spring MVC 应用程序中。

现在我们已经了解了 Spring 中两种 Web 应用程序框架的特点,下一节将介绍在构建应用程序时何时选择哪种框架。

何时选择何种方式?

响应式编程非常好,但这并不意味着我们必须为每个应用程序都采用响应式。同样,不是所有应用程序都适合 Spring WebFlux。通过查看需求以及这些框架如何解决需求来选择框架。如果应用程序在 Spring MVC 框架下运行良好,那么没有必要将其迁移到 Spring WebFlux。事实上,如前所述,如果需要,可以将响应式的优点带入 Spring MVC 中,而不会有太多麻烦。

此外,如果应用程序已经具有阻塞依赖项(JDBC、LDAP 等),那么最好坚持使用 Spring MVC,因为引入响应式概念会带来复杂性。即使引入了响应式概念,应用程序的许多部分仍处于阻塞模式,这将阻止充分利用这种编程范式。

如果应用程序涉及数据流(输入和输出),则采用 Spring WebFlux。如果可伸缩性和性能至关重要,也可以考虑这作为 Web 应用程序选择。由于其异步和非阻塞的本质,这些应用程序在性能上会比同步和阻塞的应用程序更高。由于是异步的,它们可以处理延迟,并且更具可伸缩性。

Spring 5 中的响应式支持

Spring Framework 5 对响应式编程范式有着广泛的支持。许多模块都全力拥抱这一概念,并将其视为一流公民。以下图表总结了 Spring 5 对响应式的支持:

图 4:Spring 5 和响应式支持

Spring WebFlux 模块是建立在响应式编程范式之上的一个完整的 Web 应用程序框架(它使用 Reactor 和 RxJava)。在 Spring/Java 生态系统中,响应式编程的早期采用者包括 Spring Data、Spring Security 和 Thymeleaf。Spring Security 具有支持响应式编程的许多功能。

Spring Data 对 Redis、MongoDB、Couchbase 和 Cassandra 提供了响应式支持。它还支持从数据库中以@Tailable的形式发出的无限流(以流的形式逐个发出的记录)。JDBC 本质上是阻塞的,因此 Spring Data JPA 是阻塞的,无法变为响应式。

Spring MVC 中的响应式

尽管 Spring MVC 在本质上是阻塞的,但是通过使用 Spring 5 提供的响应式编程能力,一些方面可以变得响应式。

在 Spring MVC 控制器中,可以使用响应式类型FluxMono,如下图所示。唯一的规则是只能将这些响应式类型用作控制器的返回值:

图 5:Spring MVC 使用响应式类型变为非阻塞

Spring MVC 的注解,如@Controller@RequestMapping等,在 Spring WebFlux 中也得到支持。因此,可以在一段时间内以缓慢的方式将 Spring MVC Web 应用程序转换为 Spring WebFlux。

Spring WebFlux

在本节中,我们将更详细地介绍 Spring WebFlux。Spring WebFlux 有两种(编程模型)使用方式。它们如下:

  • 使用注解:通过使用注解,如在 Spring MVC 中所做的那样

  • 使用函数式风格:使用 Java Lambdas 进行路由和处理

以下代码展示了使用 Spring WebFlux 的基于注解的风格。我们将在本章的后续部分中逐步介绍整个代码示例。然而,本节旨在在深入探讨之前进行介绍:

@RestController
@RequestMapping(value=”/api/movie”)
public class MovieAPI {
    @GetMapping(“/”)
    public Flux(Movie) getMovies() {
        //Logic of getting all movies
    }
    @GetMapping(“/{id}”)
    public Mono<Movie> getMovie(@PathVariable Long id) {
        //Logic for getting a specific movie
    }
    @PostMapping(“/post”)
    public Mono<ResponseEntity<String>> createMovie(@RequestBody Movie movie) {
        // Logic for creating movie
    }
}

Spring WebFlux 的函数式编程模型使用了两个基本组件:

  • HandlerFunction:负责处理 HTTP 请求。相当于我们在之前的代码片段中看到的@Controller处理方法。

  • RouterFunction:负责路由 HTTP 请求。相当于基于注解的@RequestMapping

HandlerFunction

HandlerFunction接受一个ServerRequest对象,并返回Mono<ServerResponse>ServerRequestServerResponse对象都是不可变的,并且完全是响应式的,建立在 Reactor 之上。

ServerRequest将 body 公开为MonoFlux。传统上,使用BodyExtractor来实现这一点。但是,它还具有实用方法,可以将这些对象公开为下面代码中所示的对象。ServerRequest还可以访问所有 HTTP 请求元素,如方法、URI 和查询字符串参数:

Mono<String> helloWorld = request.body(BodyExtractors.toMono(String.class);
Mono<String> helloWorldUtil = request.bodyToMono(String.class);

Flux<Person> movie = request.body(BodyExtractors.toFlux(Movie.class);
Flux<Person> movieUtil = request.bodyToFlux(Movie.class);

ServerResponse对象让您访问各种 HTTP 响应。ServerResponse对象可以通过使用构建器创建,允许设置响应状态和响应头。它还允许您设置响应体:

Mono<Movie> movie = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(movie);

HandlerFunction可以使用 Lambda 函数创建,如下面的代码,并返回状态为 200 OK 的ServerResponse,并且基于String的 body。

HandlerFunction<ServerResponse> handlerFunction =
  request -> ServerResponse.ok().body(fromObject("Sample HandlerFunction"));

建议将所有的HandlerFunction对象分组到一个单独的类中,每个方法处理一个特定的功能,如下面的代码片段所示:

public class MovieHandler {
    public Mono<ServerResponse> listMovies(ServerRequest request) {
        // Logic that returns all Movies objects
    }
    public Mono<ServerResponse> createMovie(ServerRequest request) {
        // Logic that returns creates Movie object in the request object
    }
    public Mono<ServerResponse> getMovie(ServerRequest request) {
        // Logic that returns one Movie object
    }
    //.. More methods as needed
}

RouterFunction

传入的请求被RouterFunction拦截,并根据配置的路由导航到正确的HandlerFunction。如果匹配路由,则RouterFunction接受ServerRequest并返回Mono<HandlerFunction>。如果不匹配,则返回空的Mono

RouterFunction如下面的代码片段所示创建:

RouterFunctions.route(RequestPredicate, HandlerFunction)

RequestPredicate是一个实用类,具有大多数常见用例的预定义匹配模式,例如基于路径、内容类型、HTTP 方法等的匹配。RouterFunction的示例代码片段如下:

RouterFunction<ServerResponse> routeFunctionSample =
    RouterFunctions.route(RequestPredicates.path("/sample-route"),
    request -> Response.ok().body(fromObject("Sample Route")));

可以通过调用以下方法组合多个RouterFunction对象:

RouterFunction.and(RouterFunction)

还有一个方便的方法,如下所示,它是RouterFunction.and()RouterFunctions.route()方法的组合:

RouterFunction.andRoute(RequestPredicate, HandlerFunction)

前面HandlerFunctionRouterFunction如下:

RouterFunction<ServerResponse> movieRoutes =
    route(GET("/movie/{id}").and(accept(APPLICATION_JSON)), handler::getMovie)
    .andRoute(GET("/movie").and(accept(APPLICATION_JSON)), handler::listMovies)
    .andRoute(POST("/movie").and(contentType(APPLICATION_JSON)), handler::createMovie);

Spring WebFlux 服务器支持

Spring Webflux 支持多个服务器,如下所示:

  • Netty

  • Jetty

  • Tomcat

  • Undertow

  • Servlet 3.1+容器

Spring Boot 2+在选择 Spring WebFlux 作为 Web 应用程序框架时,默认使用 Netty。

创建的RouterFunction可以在之前列出的任何服务器上运行。为了做到这一点,需要将RouterFunction转换为HttpHandler,使用以下方法:

RouterFunctions.toHttpHandler(RouterFunction)

如果要在 Netty 中运行先前创建的RouterFunction,可以使用以下代码片段:

HttpHandler httpHandler = RouterFunctions.toHttpHandler(movieRoutes);
ReactorHttpHandlerAdapter reactorAdapter = new ReactorHttpHandlerAdapter(httpHandler);
HttpServer server = HttpServer.create(HOST, PORT);
server.newHandler(reactorAdapter).block();

当我们在本章的后续部分查看示例应用程序时,我们将查看其他 Spring WebFlux 支持的服务器的代码。

响应式 WebClient

Spring WebFlux 包括一个名为WebClient的响应式客户端,使我们能够以非阻塞的方式执行 HTTP 请求并使用响应式流。WebClient可以作为传统上更常用的RestTemplate的替代品。WebClient公开了响应式ClientHttpRequestClientHttpResponse对象。这些对象的 body 由响应式Flux<DataBuffer>组成,而不是传统的阻塞流实现(InputStreamOutputStream)。

创建WebClient的实例,执行请求,然后处理响应。以下是显示WebClient用法的代码片段:

WebClient client = WebClient.create("http://any-domain.com");
Mono<Movie> movie = client.get()
        .url("/movie/{id}", 1L)
        .accept(APPLICATION_JSON)
        .exchange(request)
        .then(response -> response.bodyToMono(Movie.class));

WebClient可以在 Spring MVC 和 Spring WebFlux Web 应用程序中使用。RestTemplate的使用可以很容易地替换为WebClient,利用其提供的响应式优势。

在我们的示例项目中,我们将使用一个示例来介绍WebClient的概念和功能。

响应式 WebTestClient

WebClient类似,Spring WebFlux 为您提供了一个非阻塞的响应式客户端WebTestClient,用于测试服务器上的响应式 API。它具有使在测试环境设置中轻松测试这些 API 的实用程序。WebTestClient可以连接到任何服务器,如前面详细介绍的那样,通过 HTTP 连接执行必要的测试。但是,该客户端具有在运行服务器时运行测试和在没有运行服务器时运行测试的能力。

WebTestClient还有许多实用工具,可以验证执行这些服务器端 API 产生的响应。它可以很容易地绑定到 WebFlux Web 应用程序,并模拟必要的请求和响应对象,以确定 API 的功能方面。WebTestClient可以根据需要修改标头,以模拟所需的测试环境。您可以通过使用WebTestClient.bindToApplicationContext方法获取整个应用程序的WebTestClient实例,或者可以将其限制为特定的控制器(使用WebTextClient.bindToController方法),RouterFunction(使用WebTestClient.bindToRouterFunction方法)等等。

我们将在随后的实践部分(示例项目部分,测试(WebTestClient)子部分下)看到WebTestClient的工作示例。

响应式 WebSocket

Spring WebFlux 包括基于 Java WebSocket API 的响应式WebSocket客户端和服务器支持。

在服务器上,创建WebSocketHandlerAdapter,然后将每个处理程序映射到 URL。由于我们的示例应用程序中不涉及WebSocket,让我们更详细地了解一下:

public class MovieWebSocketHandler implements WebSocketHandler {
    @Override
    public Mono<Void> handle(WebSocketSession session) {
        // ...
    }
}

handle()方法接受WebSocketSession对象,并在会话处理完成时返回Mono<Void>WebSocketSession使用Flux<WebSocketMessage> receive()Mono<Void> send(Publisher<WebSocketMessage>)方法处理入站和出站消息。

在 Web 应用程序 Java 配置中,声明WebSocketHandlerAdpater的 bean,并创建另一个 bean 将 URL 映射到适当的WebSocketHandler,如下面的代码片段所示:

@Configuration
static class WebApplicationConfig {
    @Bean
    public HandlerMapping webSockerHandlerMapping() {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/movie", new MovieWebSocketHandler());

        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setUrlMap(map);
        return mapping;
    }
    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}

Spring WebFlux 还提供了WebSocketClient,并为之前讨论的所有 Web 服务器提供了抽象,如 Netty、Jetty 等。使用适当的服务器抽象并创建客户端,如下面的代码片段所示:

WebSocketClient client = new ReactorNettyWebSocketClient();
URI url = new URI("ws://localhost:8080/movie");
client.execute(url, session ->
        session.receive()
            .doOnNext(System.out::println)
            .then());

在客户端代码中,我们现在可以订阅WebSocket端点并监听消息并执行必要的操作(基本的WebSocket实现)。前端的这样一个客户端的代码片段如下:

<script>
   var clientWebSocket = new WebSocket("ws://localhost:8080/movie");
   clientWebSocket.onopen = function() {
       // Logic as needed
   }
   clientWebSocket.onclose = function(error) {
       // Logic as needed
   }
   clientWebSocket.onerror = function(error) {
       // Logic as needed
   }
   clientWebSocket.onmessage = function(error) {
       // Logic as needed
   }
</script>

为了使本章专注而简洁,我们将不讨论 Spring Security 提供的WebSocket安全性。在本书的最后一章中,我们将快速介绍WebSocket安全性,使用一个示例。

Spring WebFlux 身份验证架构

在涵盖了核心 Spring WebFlux 概念之后,我们现在将进入本章的重点;为您介绍 Spring WebFlux 基于响应式 Web 应用程序的 Spring Security。

如前所述,Spring MVC Web 应用程序中的 Spring Security 基于 ServletFilter,而 Spring WebFlux 中的 Spring Security 基于 WebFilter:

图 6:Spring MVC 和 Spring WebFlux 身份验证方法

我们在之前的章节中详细了解了 Spring MVC web 应用中的 Spring Security。现在我们将看一下基于 Spring WebFlux 的 Web 应用的 Spring Security 认证的内部细节。下图显示了在 WebFlux 应用程序的认证过程中各种类的交互:

图 7:Spring WebFlux 认证架构

上述图表相当不言自明,并且与您之前在 Spring MVC 中看到的非常相似。核心区别在于ServletFilter现在被WebFilter取代,并且我们在 Spring MVC 中有基于阻塞类的反应式类。然而,Spring Security 的核心概念仍然保持完整,WebFilter处理初始认证过程中的许多方面;核心认证由ReactiveAuthenticationManager和相关类处理。

Spring WebFlux 授权

与认证类似,就授权而言,核心概念与我们之前在 Spring MVC 中看到的相似。但是,执行操作的类已经改变,并且是响应式和非阻塞的。下图显示了 Spring WebFlux 应用程序中与授权相关的主要类及其交互:

图 8:Spring WebFlux 应用程序中与授权相关的类

正如我们现在都知道的那样,Spring WebFlux 安全性基于WebFilter工作,AuthorizationWebFilter拦截请求并使用ReactiveAuthorizationManager检查Authentication对象是否有权访问受保护的资源。ReactiveAuthorizationManager有两种方法,即check(检查Authentication对象是否被授予访问权限)和verify(检查Authentication对象是否被授予访问权限)。在任何异常情况下,ExceptionTranslationWebFilter负责通过遵循适当的路径来处理异常。

示例项目

足够的解释;现在是时候动手写实际的代码了。在本节中,我们将创建一个集成了 Spring Security 的电影目录网站。我们将贯穿始终地使用响应式概念,并使用基于表单的登录。我们将从硬编码的用户开始,然后看看如何查看持久用户存储来对用户进行认证。然后我们将更详细地进行测试,最后看看我们可以为 Spring Security 页面带来的一些自定义。最后,我们将涉及授权方面,并关闭示例应用程序。

WebFlux 项目设置

我们将首先创建一个基本的基于 WebFlux 的 Web 应用程序,然后慢慢添加其他功能,包括安全性。整个代码都可以在我们书的 GitHub 页面上找到,在章节的文件夹下,即spring-boot-webflux

我正在使用 IntelliJ 作为我的 IDE,由于我们使用了Lombok 库(注解preprocessor),请确保启用 Lombok 插件,以便为您的模型生成适当的样板代码。我们的项目非常简单,只执行电影管理的功能(电影 CRUD 操作)。

Maven 设置

使用 Spring Initializr 生成 Spring WebFlux 项目非常容易。但是为了让我们掌握 WebFlux 应用程序的各个方面,我们将逐步构建。但是,我们将使用 Spring Boot 来运行我们的应用程序。

我们将创建一个 maven 项目,然后将添加以下主要依赖项(为了使代码更简洁,以下代码只显示了重要的依赖项)到我们的pom.xml中:

<!--Spring Framework and Spring Boot-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!--JSON-->
<dependency>
…
</dependency>
<!--Logging-->
<dependency>
…
</dependency>
<!--Testing-->
<dependency>
…
</dependency>

我们将为库和插件依赖项包括快照存储库。最后,我们将为我们的 Spring Boot 添加非常重要的 maven 插件,如下所示:

<build>
  <plugins>
      <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
  </plugins>
</build>

配置类

尽管我们将尽可能使用默认配置,但我们仍将为各种组件编写单独的配置类。在我们的项目中,我们正在构建一个基本的 WebFlux 应用程序,因此我们只有一个配置类。

SpringWebFluxConfig 类

Spring WebFlux Web 应用程序的主要配置类是通过这个类实现的:

@Configuration
@EnableWebFlux
@ComponentScan
public class SpringWebFluxConfig {
  // ...
}

我们有一个空的类,只有一些非常重要的注释,如前面的代码所示。@EnableWebFlux使应用程序具有反应性,并使其成为 WebFlux。

存储库

我们将使用硬编码的电影作为我们的样本数据结构,并以一种反应性的方式编写方法,以公开我们存储库类中的方法。这些方法可以用于操作电影的数据结构。我们的存储库类是一个传统的类,但正确的数据结构,以MonoFlux的形式,有助于为应用程序带来反应性的特性:

@Repository
public class MovieRepositoryImpl implements MovieRepository {
    private Map<Long, Movie> movies = new HashMap<Long, Movie>();

    @PostConstruct
    public void initIt() throws Exception {
      movies.put(Long.valueOf(1), new Movie(Long.valueOf(1), "Moonlight",     
        "Drama"));
      movies.put(Long.valueOf(2), new Movie(Long.valueOf(2), "Dunkirk", 
        "Drama/Thriller"));
      movies.put(Long.valueOf(3), new Movie(Long.valueOf(3), "Get Out", 
        "Mystery/Thriller"));
      movies.put(Long.valueOf(4), new Movie(Long.valueOf(4), "The Shape of 
        Water", "Drama/Thriller"));
    }
    @Override
    public Mono<Movie> getMovieById(Long id) {
        return Mono.just(movies.get(id));
    }
    //...Other methods
}

该类只是从类中提取的片段,仅显示一个方法(getMovieById)。与往常一样,我们的类实现了一个接口(MovieRepository),并且这个引用将在应用程序的其他部分中使用(使用 Spring 的依赖注入功能)。

处理程序和路由器

如前所述,我们有两种方法,即基于功能的基于注释的,用于实现 WebFlux 应用程序。基于注释的方法类似于 Spring MVC,因此我们将在我们的样本应用程序中使用基于功能的方法:

@Component
public class MovieHandler {
    private final MovieRepository movieRepository;

    public MovieHandler(MovieRepository movieRepository) {
        this.movieRepository = movieRepository;
    }
    public Mono<ServerResponse> listMovies(ServerRequest request) {
        // fetch all Movies from repository
        Flux<Movie> movies = movieRepository.listMovies();
        // build response
        return 
            ServerResponse.ok().contentType(MediaType.APPLICATION_JSON)
            .body(movies, Movie.class);
    }
    //...Other methods
}

该类非常简单直接,使用存储库类进行数据结构查询和操作。每个方法都完成了功能,并最终返回Mono<ServerResponse>。基于功能的编程中 WebFlux 的另一个重要方面是路由配置类,如下所示:

@Configuration
public class RouterConfig {

    @Bean
    public RouterFunction<ServerResponse> routerFunction1(MovieHandler 
        movieHandler) {
      return 
        route(GET("/").and(accept(MediaType.APPLICATION_JSON)), 
            movieHandler::listMovies)
        .andRoute(GET("/api/movie").and(accept(MediaType.APPLICATION_JSON)), 
            movieHandler::listMovies)
        .andRoute(GET("/api/movie/{id}").and(accept(MediaType.APPLICATION_JSON)), 
            movieHandler::getMovieById)
        .andRoute(POST("/api/movie").and(accept(MediaType.APPLICATION_JSON)), 
            movieHandler::saveMovie)
        .andRoute(PUT("/api/movie/{id}").and(accept(MediaType.APPLICATION_JSON)), 
            movieHandler::putMovie)
        .andRoute(DELETE("/api/movie/{id}")
            .and(accept(MediaType.APPLICATION_JSON)), movieHandler::deleteMovie);
    }
}

这是一个查看请求并将其路由到适当处理程序方法的类。在您的应用程序中,您可以拥有任意数量的路由器配置文件。

引导应用程序

我们的样本应用程序使用 Spring Boot。Spring WebFlux 默认在 Spring Boot 中运行 Reactor Netty 服务器。我们的 Spring Boot 类非常基本,如下所示:

@SpringBootApplication
public class Run {
  public static void main(String[] args) {
      SpringApplication.run(Run.class, args);
  }
}

您可以在除 Spring Boot 之外的任何其他服务器上运行应用程序,这是非常容易实现的。我们有一个名为spring-boot-tomcat-webflux的单独项目,它在 Spring Boot 上运行,但不是在 Reactor Netty 上运行,而是在 Tomcat 服务器上运行。

除了pom.xml之外,代码的任何部分都不需要更改:

<!--Spring Framework and Spring Boot-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
  <exclusions>
      <exclusion>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-reactor-netty</artifactId>
      </exclusion>
  </exclusions>
</dependency>
<!--Explicit Tomcat dependency-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>

spring-boot-starter-webflux工件中排除 Reactor Netty。然后,显式添加 Tomcat 依赖项,spring-boot-starter-tomcat。其余的pom.xml保持不变。对于其他服务器运行时,如 Undertow、Jetty 等,方法与此处详细介绍的方法类似。

运行应用程序

现在,对于我们构建的最重要的部分:运行应用程序。由于它是一个 Spring Boot 应用程序,执行默认命令如下:

mvn spring-boot:run

一旦服务器启动(默认为 Rector Netty 或 Tomcat),打开浏览器并导航到localhost:8080/movies。我们已经创建了默认路由指向“列出所有电影”终点,如果一切顺利,您应该看到显示我们存储库类中所有硬编码电影的 JSON。

在本节中,我们创建了一个样本 Spring WebFlux 电影应用程序。在下一节中,我们将为这个应用程序添加所有重要的安全性。

添加安全性

与我们迄今为止所取得的成就分开,我们将有一个单独的项目,spring-boot-security-webflux(与spring-boot-webflux相同)。在其中,我们将构建所有安全方面。

配置类

我们将为 Spring Security 创建一个新的配置类:SpringSecurityWebFluxConfig。首先,我们将使用最重要的注解对类进行注释:@EnableWebFluxSecurity。这指示它为 WebFlux Web 应用程序启用 Spring Security。在配置类中,我们将查看两个重要的 bean,如下所示。

UserDetailsService bean

我们将使用硬编码的用户详细信息进行身份验证。这不是生产就绪应用程序的操作方式,但为了简单起见并解释概念,让我们采取这种捷径:

@Bean
public MapReactiveUserDetailsService userDetailsRepository() {
    UserDetails user = User.withUsername("user")
        .password("{noop}password").roles("USER").build();
    UserDetails admin = User.withUsername("admin")
        .password("{noop}password").roles("USER","ADMIN").build();
    return new MapReactiveUserDetailsService(user, admin);
}

该 bean 返回了包含两个用户的硬编码凭据的响应式用户详细信息服务;一个是普通用户,另一个是管理员。

SpringSecurityFilterChain bean

这是我们实际指定 Spring Security 配置的 bean:

@Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) 
    throws Exception {
    return http
      .authorizeExchange()
      .pathMatchers(HttpMethod.GET, "/api/movie/**").hasRole("USER")
      .pathMatchers(HttpMethod.POST, "/api/movie/**").hasRole("ADMIN")
      .anyExchange().authenticated()
      .and().formLogin()
      .and().build();
}

与我们之前在 Spring MVC 应用程序中看到的类似,我们匹配 URL 模式并指定访问所需的角色。我们正在将登录方法配置为一个表单,用户将通过 Spring Security 显示默认登录表单。

运行应用程序

执行以下命令:

mvn spring-boot:run

服务器启动时,您有两种方式可以测试应用程序,如下所示。

CURL

打开您喜欢的命令提示符并执行以下命令:

curl http://localhost:8080/ -v

您将被重定向到http://localhost:8080/login页面。您的整个应用程序都是安全的,如果不登录,您将无法访问任何内容。使用表单登录作为方法,您将无法使用curl进行测试。让我们将登录方法从表单(formLogin)更改为基本(httpBasic)在 Spring Security 配置(springWebFilterChain bean)中。现在,执行以下命令:

curl http://localhost:8080/api/movie -v -u admin:password

现在,您应该看到显示所有硬编码电影的原始 JSON。使用其他常见的 CURL 命令,如下所示,测试其他端点:

curl http://localhost:8080/api/movie/1 -v -u admin:password

浏览器

让我们将登录方法改回表单,然后打开浏览器并导航到http://localhost:8080。您将被导航到默认的 Spring Security 登录页面。输入用户名为admin,密码为password,然后单击登录:

图 9:默认的 Spring Security 登录表单

成功登录后,您将被导航到列出所有电影的端点,如下所示:

图 10:登录后默认主页上列出所有电影

WebClient

在该书的 GitHub 页面上,我们有一个单独的项目(spring-boot-security-webclient-webflux),您可以在其中看到本节中将详细介绍的整个代码。

Maven 设置

创建一个基本的 maven 项目,并将以下主要依赖项添加到您的pom.xml文件中:

<!--Spring Framework and Spring Boot-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

现在,添加其他依赖项,以及默认的 Spring Boot 构建部分。

创建一个 WebClient 实例

WebClient实例可以通过使用create()方法或使用builder()方法来创建。在我们的示例中,我们使用了builder()方法,如下所示:

@Service
public class WebClientTestImpl implements WebClientTestInterface {
    private final WebClient webClient;
    public WebClientTestImpl(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.defaultHeader(HttpHeaders.ACCEPT,     
        MediaType.APPLICATION_JSON_VALUE)
              .baseUrl("http://localhost:8080/api/movie").build();
    }
    //...Other methods
}

我们将使用我们在基本 Spring WebFlux 项目中创建的所有端点,并将使用WebClient访问它们。

使用create()方法创建WebClient的实例,如下所示:

WebClient webClient = WebClient.create();

如果您有基本 URL,则可以创建WebClient如下:

WebClient webClient = WebClient.create("http://localhost:8080/api/movie");

builder()方法提供了一堆实用方法,如过滤器、设置标头、设置 cookie 等。在我们的示例中,我们设置了一些默认标头,并设置了基本 URL。

处理错误

WebClient实例允许您处理错误(WebClientTestImpl类)在listMovies()方法中,如下所示:

@Override
public Flux<Movie> listMovies() {
    return webClient.get().uri("/")
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError, clientResponse ->
            Mono.error(new SampleException())
        )
        .onStatus(HttpStatus::is5xxServerError, clientResponse ->
            Mono.error(new SampleException())
        )
        .bodyToFlux(Movie.class);
}

SampleException是我们通过扩展Exception类创建的自定义异常类。我们正在处理 4xx 和 5xx 错误,并且在遇到时,它会将自定义异常作为响应发送。

发送请求和检索响应

retrieve()方法是一个简单的方法,可以用来检索响应主体。如果您想对返回的响应有更多控制,可以使用exchange()方法来检索响应。我们在示例应用程序中使用了这两种方法;WebClientTestImpl类中这两种方法的代码片段如下:

@Override
public Mono<Movie> getMovieById(Long id) 
  return this.webClient.get().uri("/{id}", id)
          .retrieve().bodyToMono(Movie.class);
}
@Override
public Mono<Movie> saveMovie(Movie movie) {
  return webClient.post().uri("/")
          .body(BodyInserters.fromObject(movie))
          .exchange().flatMap( clientResponse ->     
            clientResponse.bodyToMono(Movie.class) );
}

在第一种方法中,我们在 URI http://localhost:8080/api/movie/{id} 上执行 GET 方法,使用retrieve()方法,然后转换为Mono

在第二种方法中,我们在 URL http://localhost:8080/api/movie 上执行 POST 方法,使用exchange()方法,并使用flatMap()方法创建响应。

运行和测试应用程序

在这个示例项目中,我们将使用相同的电影模型。由于这是我们从之前的示例应用程序中需要的唯一类,我们将在这里复制该类。在理想情况下,我们将有一个包含所有公共类的 JAR 文件,并且可以将其包含在我们的pom.xml文件中。

创建Run类(如前所示)并调用WebClient方法。其中一个方法的代码片段如下:

@SpringBootApplication
public class Run implements CommandLineRunner {
  @Autowired
  WebClientTestInterface webClient;
  public static void main(String[] args) {
      SpringApplication.run(Run.class, args);
  }
  @Override
  public void run(String... args) throws Exception {
      // get all movies
      System.out.println("Get All Movies");
      webClient.listMovies().subscribe(System.out::println);
      Thread.sleep(3000);
      … Other methods
  }
  //… Other WebClient methods getting called
}

在执行每个WebClient调用后,我们将休眠三秒。由于WebClient方法发出反应类型(MonoFlux),您必须订阅,如前面的代码所示。

启动spring-boot-webflux项目,暴露端点,我们将使用此项目中的WebClient进行测试。

确保在您的application.properties文件中更改应用程序的默认端口,包括以下条目:

server.port=8081

通过执行 Spring Boot 命令启动应用程序,如下所示:

mvn spring-boot:run

如果一切顺利,您应该在服务器控制台中看到输出,如下所示:

图 11:WebClient 测试执行

单元测试(WebTestClient)

在我们的基本spring-boot-webflux项目中,我们使用WebTestClient编写了测试用例。我们有两个测试用例:一个是获取所有电影,另一个是保存电影。

Maven 依赖

确保在您的pom.xml文件中有以下依赖项:

<!--Testing-->
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.skyscreamer</groupId>
  <artifactId>jsonassert</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>io.projectreactor</groupId>
  <artifactId>reactor-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

如您所见,在前面的代码中,所有依赖项都可以用于测试目的。

测试类

创建一个普通的测试类,如下所示。在测试类中使用@Autowired注解来注入WebTestClient实例:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class WebclientDemoApplicationTests {
  @Autowired
  private WebTestClient webTestClient;
  @Test
  public void getAllMovies() {
      System.out.println("Test 1 executing getAllMovies");
      webTestClient.get().uri("/api/movie")
              .accept(MediaType.APPLICATION_JSON)
              .exchange()
              .expectStatus().isOk()
              .expectHeader().contentType(MediaType.APPLICATION_JSON)
              .expectBodyList(Movie.class);
  }
  @Test
  public void saveMovie() {
      System.out.println("Test 2 executing saveMovie");
      Movie movie = new Movie(Long.valueOf(10), "Test Title", "Test Genre");
      webTestClient.post().uri("/api/movie")
              .body(Mono.just(movie), Movie.class)
              .exchange()
              .expectStatus().isOk()
              .expectBody();
  }
}

WebTestClient对象的功能与之前看到的WebClient类似。我们可以检查响应中的各种属性,以确定我们要测试的内容。在前面的示例中,对于第一个测试,我们正在发送 GET 请求并检查 OK 状态,应用程序/JSON 内容类型标头,最后,一个包含Movie对象列表的主体。在第二个测试中,我们正在发送一个带有Movie对象的 POST 请求作为主体,并期望一个 OK 状态和一个空主体。

Spring Data

尽管本书侧重于响应式概念上的 Spring Security,但我真的希望您也对其他领域的响应式概念有一些了解。因此,有一个单独的项目spring-boot-security-mongo-webflux,它通过将之前的项目与响应式 MongoDB 集成,使用 Spring Data 来实现响应式概念。我们不会涵盖与此相关的每个方面。但是,基于之前的项目,我们将在本节中涵盖一些重要方面。

Maven 依赖

在您的应用程序pom.xml中,添加以下依赖项,都涉及将 MongoDB 包含到项目中:

<!--Mongo-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
  <groupId>de.flapdoodle.embed</groupId>
  <artifactId>de.flapdoodle.embed.mongo</artifactId>
  <scope>test</scope>
</dependency>

我已在我的机器上安装了 MongoDB。我已在默认端口(27017)上本地启动数据库。

MongoDB 配置

将以下内容添加到您的 application.properties 文件中:

spring.data.mongodb.uri=mongodb://localhost:27017/movie

我们将把我们的数据库指向本地运行的默认端口上的数据库,利用电影数据库。

设置模型

在我们已经存在的Movie模型中,我们只添加了一个注解:@Document(collection = "movies")。此注解将告知 MongoDB 该模型将存储在 DB 中的集合的名称。

实现存储库

我们将创建一个新的存储库ReactiveMovieRepository,其中包含我们的两个精心策划的方法和我们扩展类提供的所有默认方法:

@Repository
public interface ReactiveMovieRepository extends 
    ReactiveMongoRepository<Movie, Long> {
      @Query("{ 'title': ?0, 'genre': ?1}")
      Flux<Movie> findByTitleAndGenre(String title, String genre);
      @Query("{ 'genre': ?0}")
      Flux<Movie> findByGenre(String genre);
}

我们将从ReactiveMongoRepository扩展我们的存储库。ReactiveMongoRepository有很多通用方法,可以立即使用,毫不费力。我们实现的方法使用普通查询来对 MongoDB 进行操作并返回列表。

实现控制器

为了使其与我们现有的基于功能的编程分离,我们创建了一个新的控制器,它将以 RESTful 方式暴露一些方法,使用新创建的ReactiveMovieRepository

@RestController
public class MovieController {
  @Autowired
  private ReactiveMovieRepository reactiveMovieRepository;
  @GetMapping("/movies")
  public Flux<Movie> getAllMovies() {
      return reactiveMovieRepository.findAll();
  }
  @GetMapping("/movies/{genre}")
  public Flux<Movie> getAllMoviesByGenre(@PathVariable String genre) {
      return reactiveMovieRepository.findByGenre(genre);
  }
  @GetMapping("/movies/{title}/{genre}")
  public Flux<Movie> getAllMoviesByTitleAndGenre
    (@PathVariable String title, @PathVariable String genre) {
      return reactiveMovieRepository.findByTitleAndGenre(title, genre);
  }
  @PostMapping("/movies")
  public Mono<Movie> createMovies(@Valid @RequestBody Movie movie) {
      return reactiveMovieRepository.save(movie);
  }
}

这个类非常简单;每个方法都有适当的映射,并使用相应的存储库类来实际完成工作。

运行应用程序

使用mongod命令,我们将启动本地安装的 MongoDB,然后使用以下命令,我们将启动刚刚创建的项目:

mvn spring-boot:run

转到 postman 并调用 URL http://localhost:8080/movies(GET)。您将看到其中有零个元素的数组。现在,调用 URL http://localhost:8080/movies(POST),在请求体中使用以下 JSON:

{
   "id": 1,
   "title": "testtitle",
   "genre": "thriller"
}

您将获得一个 200 OK 状态,并应该看到新创建的 JSON 作为响应。现在,如果您在电影端点上运行 GET 请求,您应该会看到新创建的Movie作为响应。

在这里,我们通过使用 MongoDB 作为响应式编程范式中的持久存储库,实现了对我们的Movie模型的 CRUD。

授权

过去,我们已经看到使用@EnableWebFluxSecurity注解,我们可以获得 URL 安全性。Spring Security 还允许您以一种响应式的方式保护方法执行,通过使用另一个注解@EnableReactiveMethodSecurity。这个概念与我们之前基于 Spring MVC 的示例中看到的是相同的。我们将在本节中只涵盖方法安全性;其他方面完全相同,我们将避免在此重复。

方法安全性

要启用方法安全性,首先要用@EnableReactiveMethodSecurity注解 Spring Security 配置类:

@EnableReactiveMethodSecurity
public class SpringSecurityWebFluxConfig {
    …
}

之后,对于任何您希望具有一些安全功能的方法,使用前几章讨论的各种安全相关注解:

@GetMapping("/movies")
@PreAuthorize("hasRole('ADMIN')")
public Flux<Movie> getAllMovies() {
  return reactiveMovieRepository.findAll();
}

在上述方法中,我们指示 Spring Security,如果用户经过身份验证并被授予ADMIN角色,则应允许getAllMovies()的方法执行。

定制

Spring Security 允许进行许多定制。Spring Security 生成的默认页面,如登录表单、注销表单等,可以在所有方面完全定制,以适应您应用程序的品牌。如果您想要调整 Spring Security 的默认执行,实现自己的过滤器是合适的。由于 Spring Security 在很大程度上依赖过滤器来实现其功能,让我们看看在这方面的定制机会。

此外,几乎可以通过使用自己的类来定制 Spring Security 的几乎所有部分,并将其插入 Spring Security 默认流程中以管理自己的定制。

编写自定义过滤器

正如我们之前看到的,在 WebFlux Web 应用程序中,Spring Security 基于WebFilter(类似于 Spring MVC 中的 Servlet Filter)工作。如果您想要定制 Spring Security 中的某些方面,特别是在请求和响应操作中,实现自定义WebFilter是可以考虑的方法之一。

Spring WebFlux 提供了两种实现过滤器的方法:

  • 使用 WebFilter:适用于基于注解和基于功能的(routerhandler

  • 使用 HandlerFilterFunction:仅适用于基于功能的

使用 WebFilter

我们将在我们的项目spring-boot-webflux的基础上进行构建。为了使其与其他项目隔离,我们将创建一个新项目spring-boot-webflux-custom。如前所述,使用WebFilter适用于基于注解和基于功能的 WebFlux 方法。在我们的示例中,我们将有两个路径:filtertest1filtertest2。我们将使用WebFluxTestClient编写测试用例,并断言某些条件。作为与其他部分分离,我们将创建一个新的路由配置、一个处理程序和一个全新的 REST 控制器。我们不会详细介绍一些已经涵盖的方面。在本节中,我们只会介绍WebFilter代码,以及测试用例的一些重要方面:

@Component
public class SampleWebFilter implements WebFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange serverWebExchange, 
            WebFilterChain webFilterChain) {
        serverWebExchange.getResponse().getHeaders().add("filter-added-header", 
            "filter-added-header-value");
        return webFilterChain.filter(serverWebExchange);
    }
}

SampleWebFilter类实现了WebFilter,并实现了filter方法。在这个类中,我们将添加一个新的响应头,filter-added-header

@Test
public void filtertest1_with_pathVariable_equalTo_value1_apply_WebFilter() {
    EntityExchangeResult<String> result = 
        webTestClient.get().uri("/filtertest1/value1")
        .exchange()
        .expectStatus().isOk()
        .expectBody(String.class)
        .returnResult();
    Assert.assertEquals(result.getResponseBody(), "value1");
    Assert.assertEquals(result.getResponseHeaders()
        .getFirst("filter-added-header"), "filter-added-header-value");
}
@Test
public void filtertest2_with_pathVariable_equalTo_value1_apply_WebFilter() {
    EntityExchangeResult<String> result = 
        webTestClient.get().uri("/filtertest2/value1")
        .exchange()
        .expectStatus().isOk()
        .expectBody(String.class)
        .returnResult();
    Assert.assertEquals(result.getResponseBody(), "value1");
    Assert.assertEquals(result.getResponseHeaders()
        .getFirst("filter-added-header"), "filter-added-header-value");
}

在两个测试用例中,我们将检查新添加的头。当您运行测试用例(使用mvn test)时,它将确认这一发现。

使用 HandlerFilterFunction

我们将实现一个新的HandlerFilterFunctionSampleHandlerFilterFunction,在其中我们将查看一个路径变量(pathVariable)并检查其值。如果该值等于value2,我们将标记状态为BAD_REQUEST。需要注意的是,由于HandlerFilterFunction仅适用于基于功能的,即使路径变量值等于value2,状态也不会被标记为BAD_REQUEST,而接收到的响应是 OK:

public class SampleHandlerFilterFunction implements 
        HandlerFilterFunction<ServerResponse, ServerResponse> {
    @Override
    public Mono<ServerResponse> filter(ServerRequest serverRequest, 
        HandlerFunction<ServerResponse> handlerFunction) {
        if (serverRequest.pathVariable("pathVariable")
                .equalsIgnoreCase("value2")) {
            return ServerResponse.status(BAD_REQUEST).build();
        }
        return handlerFunction.handle(serverRequest);
    }
}

SampleHandlerFilterFunction实现了HandlerFilterFunction类,并实现了filter方法。在这个类中,如果满足条件,我们将明确将响应状态设置为bad request

@Test
public void filtertest1_with_pathVariable_equalTo_value2_apply_HandlerFilterFunction() {
    webTestClient.get().uri("/filtertest1/value2")
        .exchange()
        .expectStatus().isOk();
}
@Test
public void filtertest2_with_pathVariable_equalTo_value2_apply_HandlerFilterFunction() {
    webTestClient.get().uri("/filtertest2/value2")
        .exchange()
        .expectStatus().isBadRequest();
}

在前面的测试用例中,测试的路径是不同的,由于HandlerFilterFunction仅适用于基于功能的,因此当路径为filtertest1时,响应为 OK,当路径为filtertest2时,响应为BAD_REQUEST

总结

在本章中,我们首次详细介绍了响应式编程,使用了 Spring WebFlux 框架。我们首先从高层次上对框架本身进行了充分的介绍。我们介绍了一个非常基本的例子,然后介绍了 Spring Security 及其在 Spring WebFlux 中的功能。

最后,我们进行了一个实际的编码会话,使用了一个示例应用程序。在这个例子中,我们涵盖了其他响应式方面,比如 Spring Data Mongo,以便让您更深入地了解响应式世界。

我们以 Spring WebFlux 与 Spring Security 中可能的一些自定义结束了本章。

阅读完本章后,您应该清楚了解了 Spring MVC 和 Spring WebFlux 框架之间的区别。您还应该对使用 Spring Security 模块的 Spring WebFlux 安全性有很好的理解。这些示例旨在简单易懂,因为在本书中我们正在介绍 Spring Security,所以在解释中给予了更多的价值。

第六章:REST API 安全

Spring Security 可以用于保护 REST API。本章首先介绍了有关 REST 和 JWT 的一些重要概念。

然后,本章介绍了 OAuth 概念,并通过实际编码示例,解释了在 Spring 框架的 Spring Security 和 Spring Boot 模块中利用简单和高级 REST API 安全。

我们将在示例中使用 OAuth 协议来保护暴露的 REST API,充分利用 Spring Security 功能。我们将使用 JWT 在服务器和客户端之间交换声明。

在本章中,我们将涵盖以下概念:

  • 现代应用程序架构

  • 响应式 REST API

  • 简单的 REST API 安全

  • 高级 REST API 安全

  • Spring Security OAuth 项目

  • OAuth2 和 Spring WebFlux

  • Spring Boot 和 OAuth2

重要概念

在进行编码之前,我们需要熟悉一些重要概念。本节旨在详细介绍一些这些概念。

REST

表述性状态转移REST)是 Roy Fielding 于 2000 年提出的一种用于开发 Web 服务的架构风格。它建立在著名的超文本传输协议HTTP)之上,可以以多种格式传输数据,最常见的是JavaScript 对象表示法JSON)和可扩展标记语言XML)。在 REST 中,请求的状态使用标准的 HTTP 状态码表示(200:OK,404:页面未找到!等)。基于 HTTP,安全性是通过已熟悉的安全套接字层SSL)和传输层安全性TLS)来处理的。

在编写此类 Web 服务时,您可以自由选择任何编程语言(Java,.NET 等),只要它能够基于 HTTP 进行 Web 请求(这是每种语言都支持的事实标准)。您可以使用许多知名的框架来开发服务器端的 RESTful API,这样做非常容易和简单。此外,在客户端,有许多框架可以使调用 RESTful API 和处理响应变得简单直接。

由于 REST 是基于互联网协议工作的,通过提供适当的 HTTP 头部(Cache-Control,Expires 等),可以很容易地实现对 Web 服务响应的缓存。PUTDELETE方法在任何情况下都不可缓存。以下表格总结了 HTTP 方法的使用:

HTTP 方法 描述
GET 检索资源
POST 创建新资源
PUT 更新现有资源
DELETE 删除现有资源
PATCH 对资源进行部分更新

表 1:HTTP 方法使用

REST API 请求/响应(通过网络发送的数据)可以通过指定适当的 HTTP 头部进行压缩,类似于缓存。客户端发送 Accept-Encoding 的 HTTP 头部,让服务器知道它可以理解哪些压缩算法。服务器成功压缩响应并输出另一个 HTTP 头部 Content-Encoding,让客户端知道应该使用哪种算法进行解压缩。

JSON Web Token(JWT)

"JSON Web Tokens 是一种开放的、行业标准的 RFC 7519 方法,用于在两个当事方之间安全地表示声明。"

- jwt.io/

在过去,HTTP 的无状态性质在 Web 应用程序中被规避(大多数 Web 应用程序的性质是有状态的),方法是将每个请求与在服务器上创建的会话 ID 相关联,然后由客户端使用 cookie 存储。每个请求都以 HTTP 头部的形式发送 cookie(会话 ID),服务器对其进行验证,并将状态(用户会话)与每个请求相关联。在现代应用程序中(我们将在下一节中更详细地介绍),服务器端的会话 ID 被 JWT 替代。以下图表显示了 JWT 的工作原理:

图 1:JWT 在现代应用程序中的工作原理

在这种情况下,Web 服务器不会创建用户会话,并且对于需要有状态应用程序的用户会话管理功能被卸载到其他机制。

在 Spring 框架的世界中,Spring Session 模块可以用于将会话从 Web 服务器外部化到中央持久性存储(Redis、Couchbase 等)。每个包含有效令牌(JWT)的请求都会针对这个外部的真实性和有效性存储进行验证。验证成功后,应用程序可以生成有效令牌并将其作为响应发送给客户端。然后客户端可以将此令牌存储在其使用的任何客户端存储机制中(sessionStorage、localStorage、cookies 等,在浏览器中)。使用 Spring Security,我们可以验证此令牌以确定用户的真实性和有效性,然后执行所需的操作。本章的后续部分(简单 REST API 安全性)中有一个专门的示例,该示例使用基本身份验证机制,并在成功时创建 JWT。随后的请求使用 HTTP 标头中的令牌,在服务器上进行验证以访问其他受保护的资源。

以下几点突出了使用 JWT 的一些优点:

  • 更好的性能:每个到达服务器的请求都必须检查发送的令牌的真实性。JWT 的真实性可以在本地检查,不需要外部调用(比如到数据库)。这种本地验证性能良好,减少了请求的整体响应时间。

  • 简单性:JWT 易于实现和简单。此外,它是行业中已经建立的令牌格式。有许多知名的库可以轻松使用 JWT。

令牌的结构

与常见的安全机制(如加密、混淆和隐藏)不同,JWT 不会加密或隐藏其中包含的数据。但是,它确实让目标系统检查令牌是否来自真实来源。JWT 的结构包括标头、有效载荷和签名。如前所述,与其加密,JWT 中包含的数据被编码,然后签名。编码的作用是以一种可被各方接受的方式转换数据,签名允许我们检查其真实性,实际上是其来源:

JWT = header.payload.signature

让我们更详细地了解构成令牌的每个组件。

标头

这是一个 JSON 对象,采用以下格式。它提供有关如何计算签名的信息:

{
 "alg": "HS256",
 "typ": "JWT"
}

typ的值指定对象的类型,在这种情况下是JWTalg的值指定用于创建签名的算法,在这种情况下是HMAC-SHA256

有效载荷

有效载荷形成 JWT 中存储的实际数据(也称为声明)。根据应用程序的要求,您可以将任意数量的声明放入 JWT 有效载荷组件中。有一些预定义的声明,例如iss(发行人)、sub(主题)、exp(过期时间)、iat(发布时间)等,可以使用,但所有这些都是可选的:

{
 "sub": "1234567890",
 "username": "Test User",
 "iat": 1516239022
}

签名

签名形成如下:

  1. 标头base64编码的:base64(标头)

  2. 有效载荷base64编码的:base64(有效载荷)

  3. 现在用中间的"."连接步骤 1步骤 2中的值:

base64UrlEncode(header) + "." +base64UrlEncode(payload)
  1. 现在,签名是通过使用标头中指定的算法对步骤 3中获得的值进行哈希,然后将其与您选择的秘密文本(例如packtpub)附加而获得的:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  packtpub
)

最终的 JWT 如下所示:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IlRlc3QgVXNlciIsImlhdCI6MTUxNjIzOTAyMn0.yzBMVScwv9Ln4vYafpTuaSGa6mUbpwCg84VOhVTQKBg

网站jwt.io/是我在任何 JWT 需求中经常访问的地方。此示例中使用的示例数据来自该网站:

图 2:来自 https://jwt.io/的屏幕截图

现代应用程序架构

大多数现代应用的前端不是使用服务器端 Web 应用程序框架构建的,比如 Spring MVC、Java Server Faces(JSF)等。事实上,许多应用是使用完整的客户端框架构建的,比如 React(要成为完整的框架,必须与其他库结合使用)、Angular 等。前面的陈述并不意味着这些服务器端 Web 应用程序框架没有任何用处。根据您正在构建的应用程序,每个框架都有特定的用途。

在使用客户端框架时,一般来说,客户端代码(HTML、JS、CSS 等)并不安全。然而,渲染这些动态页面所需的数据是安全的,位于 RESTful 端点之后。

为了保护 RESTful 后端,使用 JWT 在服务器和客户端之间交换声明。JWT 实现了两方之间令牌的无状态交换,并通过服务器消除了会话管理的负担(不再需要多个服务器节点之间的粘性会话或会话复制),从而使应用程序能够以成本效益的方式水平扩展:

图 3:基于 API 的现代应用架构

SOFEA

面向服务的前端架构SOFEA)是一种在过去获得了流行的架构风格,当时面向服务的架构SOA)在许多企业中变得普遍。在现代,SOA 更多地采用基于微服务的架构,后端被减少为一堆 RESTful 端点。另一方面,客户端变得更厚,并使用客户端 MVC 框架,比如 Angular 和 React,只是举几个例子。然而,SOFEA 的核心概念,即后端只是端点,前端(UI)变得更厚,是现代 Web 应用程序开发中每个人都考虑的事情。

SOFEA 的一些优点如下:

  • 这种客户端比我们过去看到的薄客户端 Web 应用程序更厚(类似于厚客户端应用程序)。在页面的初始视图/渲染之后,所有资产都从服务器下载并驻留/缓存在客户端(浏览器)上。此后,只有在客户端通过 XHR(Ajax)调用需要数据时,才会与服务器联系。

  • 客户端代码下载后,只有数据从服务器流向客户端,而不是呈现代码(HTML、JavaScript 等),更好地利用带宽。由于传输的数据量较少,响应时间更快,使应用程序性能更好。

  • 任意数量的客户端可以利用相同的 RESTful 服务器端点编写,充分重用 API。

  • 这些端点可以外部化会话(在 Spring 框架中,有一个称为Spring Session的模块,可以用来实现这种技术能力),从而轻松实现服务器的水平扩展。

  • 在项目中,通过由一个团队管理的 API 和由另一个团队管理的 UI 代码,更好地分离团队成员的角色。

响应式 REST API

在第四章中,使用 CAS 和 JAAS 进行身份验证,我们详细介绍了响应式 Spring WebFlux Web 应用程序框架。我们还深入研究了 Spring 框架和其他 Spring 模块提供的许多响应式编程支持。无论是有意还是无意,我们在上一章的示例部分创建了一个响应式 REST API。我们使用了处理程序和路由器机制来创建一个 RESTful 应用程序,并使用了BASIC身份验证机制进行了安全保护。

我们看到了WebClient(一种调用 REST API 的响应式方式,与使用阻塞的RestTemplate相对)和WebTestClient(一种编写测试用例的响应式方式)的工作原理。我们还以响应式方式使用 Spring Data,使用 MongoDB 作为持久存储。

我们不会在这里详细介绍这些方面;我们只会提到,如果你愿意,你可以通过阅读第四章中的部分来熟悉这个主题,使用 CAS 和 JAAS 进行身份验证。在本章中,我们将继续上一章的内容,介绍使用 JWT 进行 REST API 安全,然后介绍使用 OAuth 进行 REST API 安全(实现自定义提供者,而不是使用公共提供者,如 Google、Facebook 等)。

简单的 REST API 安全

我们将使用我们在第五章中创建的示例,与 Spring WebFlux 集成spring-boot-spring-webflux),并通过以下方式进行扩展:

  • 将 JWT 支持引入到已使用基本身份验证进行安全保护的现有 Spring WebFlux 应用程序中。

  • 创建一个新的控制器(路径/auth/**),将有新的端点,使用这些端点可以对用户进行身份验证。

  • 使用基本身份验证或 auth REST 端点,我们将在服务器上生成 JWT 并将其作为响应发送给客户端。客户端对受保护的 REST API 的后续调用可以通过使用作为 HTTP 头部(授权,令牌)提供的 JWT 来实现。

我们无法详细介绍这个项目的每一个细节(我们在规定的页数内需要涵盖一个更重要的主题)。然而,在浏览示例时,重要的代码片段将被列出并进行详细解释。

Spring Security 配置

在 Spring Security 配置中,我们调整了springSecurityFilterChain bean,如下面的代码片段所示:

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http){
    AuthenticationWebFilter authenticationJWT = new AuthenticationWebFilter(new     
    UserDetailsRepositoryReactiveAuthenticationManager(userDetailsRepository()));
    authenticationJWT.setAuthenticationSuccessHandler(new         
                                                    JWTAuthSuccessHandler());
    http.csrf().disable();
    http
      .authorizeExchange()
      .pathMatchers(WHITELISTED_AUTH_URLS)
      .permitAll()
      .and()
      .addFilterAt(authenticationJWT, SecurityWebFiltersOrder.FIRST)
      .authorizeExchange()
      .pathMatchers(HttpMethod.GET, "/api/movie/**").hasRole("USER")
      .pathMatchers(HttpMethod.POST, "/api/movie/**").hasRole("ADMIN")
      .anyExchange().authenticated()
      .and()
      .addFilterAt(new JWTAuthWebFilter(), SecurityWebFiltersOrder.HTTP_BASIC);
    return http.build();
}

正如你所看到的,我们配置了一个新的AuthenticationWebFilter和一个AuthenticationSuccessHandler。我们还有一个新的JWTAuthWebFilter类来处理基于 JWT 的身份验证。

我们将使用ReactiveUserDetailsService和硬编码的用户凭据进行测试,如下面的代码片段所示:

@Bean
public MapReactiveUserDetailsService userDetailsRepository() {
    UserDetails user = User.withUsername("user").password("    
        {noop}password").roles("USER").build();
    UserDetails admin = User.withUsername("admin").password("
        {noop}password").roles("USER","ADMIN").build();
    return new MapReactiveUserDetailsService(user, admin);
}

身份验证成功处理程序

我们在 Spring Security 配置类中设置了自定义的AuthenticationSuccessHandler(该类的源代码将在下面显示)。在成功验证后,它将生成 JWT 并设置 HTTP 响应头:

  • 头部名称Authorization

  • 头部值Bearer JWT

让我们看一下下面的代码:

public class JWTAuthSuccessHandler implements ServerAuthenticationSuccessHandler{
    @Override
    public Mono<Void> onAuthenticationSuccess(WebFilterExchange     
            webFilterExchange, Authentication authentication) {
        ServerWebExchange exchange = webFilterExchange.getExchange();
        exchange.getResponse()
            .getHeaders()
            .add(HttpHeaders.AUTHORIZATION, 
                    getHttpAuthHeaderValue(authentication));
        return webFilterExchange.getChain().filter(exchange);
    }
    private static String getHttpAuthHeaderValue(Authentication authentication){
        return String.join(" ","Bearer",tokenFromAuthentication(authentication));
    }
    private static String tokenFromAuthentication(Authentication authentication){
        return new JWTUtil().generateToken(
            authentication.getName(),
            authentication.getAuthorities());
    }
}

JWTUtil类包含许多处理 JWT 的实用方法,例如生成令牌、验证令牌等。JWTUtil类中的generateToken方法如下所示:

public static String generateToken(String subjectName, Collection<? extends             GrantedAuthority> authorities) {
    JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
        .subject(subjectName)
        .issuer("javacodebook.com")
        .expirationTime(new Date(new Date().getTime() + 30 * 1000))
        .claim("auths", authorities.parallelStream().map(auth ->                             (GrantedAuthority) auth).map(a ->                                 
            a.getAuthority()).collect(Collectors.joining(",")))
        .build();
    SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256),         claimsSet);
    try {
        signedJWT.sign(JWTUtil.getJWTSigner());
    } catch (JOSEException e) {
        e.printStackTrace();
    }
    return signedJWT.serialize();
}

自定义 WebFilter,即 JWTAuthWebFilter

我们的自定义WebFilter,名为JWTAuthWebFilter,负责将接收到的 JWT 令牌转换为 Spring Security 理解的适当类。它使用了一个名为JWTAuthConverter的转换器,该转换器执行了许多操作,如下所示:

  • 获取授权payload

  • 通过丢弃Bearer字符串来提取令牌

  • 验证令牌

  • 创建一个 Spring Security 理解的UsernamePasswordAuthenticationToken

下面的代码片段显示了JWTAuthWebFilter类及其上面列出的操作的重要方法。

public class JWTAuthConverter implements Function<ServerWebExchange,             
        Mono<Authentication>> {
    @Override
    public Mono<Authentication> apply(ServerWebExchange serverWebExchange) {
        return Mono.justOrEmpty(serverWebExchange)
            .map(JWTUtil::getAuthorizationPayload)
            .filter(Objects::nonNull)
            .filter(JWTUtil.matchBearerLength())
            .map(JWTUtil.getBearerValue())
            .filter(token -> !token.isEmpty())
            .map(JWTUtil::verifySignedJWT)
            .map(JWTUtil::getUsernamePasswordAuthenticationToken)
            .filter(Objects::nonNull);
    }
}

在此转换之后,使用 Spring Security 进行实际的身份验证,该身份验证在应用程序中设置了SecurityContext,如下面的代码片段所示:

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
    return this.getAuthMatcher().matches(exchange)
        .filter(matchResult -> matchResult.isMatch())
        .flatMap(matchResult -> this.jwtAuthConverter.apply(exchange))
        .switchIfEmpty(chain.filter(exchange).then(Mono.empty()))
        .flatMap(token -> authenticate(exchange, chain, token));
}
//..more methods
private Mono<Void> authenticate(ServerWebExchange exchange,
                              WebFilterChain chain, Authentication token) {
    WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
    return this.reactiveAuthManager.authenticate(token)
      .flatMap(authentication -> onAuthSuccess(authentication, 
          webFilterExchange));
}
private Mono<Void> onAuthSuccess(Authentication authentication, WebFilterExchange 
        webFilterExchange) {
    ServerWebExchange exchange = webFilterExchange.getExchange();
    SecurityContextImpl securityContext = new SecurityContextImpl();
    securityContext.setAuthentication(authentication);
    return this.securityContextRepository.save(exchange, securityContext)
        .then(this.authSuccessHandler
        .onAuthenticationSuccess(webFilterExchange, authentication))
        .subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(
            Mono.just(securityContext)));
}

JWTAuthWebFilter类的过滤器方法进行必要的转换,然后authenticate方法进行实际的身份验证,最后调用onAuthSuccess方法。

新的控制器类

我们有两个控制器,分别是DefaultController(映射到//login路径)和AuthController(映射到/auth主路由和/token子路由)。/auth/token路径可用于检索令牌,该令牌可用于后续的 API 调用(Bearer <Token>)。AuthController的代码片段如下所示:

@RestController
@RequestMapping(path = "/auth", produces = { APPLICATION_JSON_UTF8_VALUE })
public class AuthController {

    @Autowired
    private MapReactiveUserDetailsService userDetailsRepository;
        @RequestMapping(method = POST, value = "/token")
        @CrossOrigin("*")
        public Mono<ResponseEntity<JWTAuthResponse>> token(@RequestBody     
                JWTAuthRequest jwtAuthRequest) throws AuthenticationException {
            String username =  jwtAuthRequest.getUsername();
            String password =  jwtAuthRequest.getPassword();
            return userDetailsRepository.findByUsername(username)
               .map(user -> ok().contentType(APPLICATION_JSON_UTF8).body(
                 new JWTAuthResponse(JWTUtil.generateToken(user.getUsername(),                  user.getAuthorities()), user.getUsername())))
                 .defaultIfEmpty(notFound().build());
        }
    }
}

运行应用程序并进行测试

使用下面显示的 Spring Boot 命令运行应用程序:

mvn spring-boot:run

我将使用 Postman 执行 REST 端点。

您可以通过以下两种方法获得令牌,并在随后的调用中包含它:

  • 如果使用基本身份验证凭据访问任何路由,在响应头中,您应该获得令牌。我将使用/login路径与基本身份验证(授权头)获取令牌,如图所示:

图 4:在 Postman 中使用基本身份验证获取令牌

  • 使用 JSON 形式的基本身份验证凭据(使用JWTAuthRequest类),如图所示,在 Postman 中访问/auth/token端点:

图 5:使用 JSON 中的基本身份验证凭据使用/auth/token 端点获取令牌

使用检索到的令牌,如图所示,在 Postman 中调用电影端点:

图 6:在 Postman 中使用 JWT 令牌检索电影列表

这完成了我们正在构建的示例。在这个示例中,我们使用 JWT 保护了 REST API,并使用 Spring Security 进行了验证。如前所述,这是您可以使用 Spring Security 和 JWT 保护 REST API 的基本方法。

高级 REST API 安全性

REST API 可以通过您的 Web 应用程序中的另一种机制进行保护,即 OAuth。

OAuth 是一个授权框架,允许其他应用程序使用正确的凭据访问存储在 Google 和 Facebook 等平台上的部分/有限用户配置文件详细信息。认证部分被委托给这些服务,如果成功,适当的授权将被授予调用客户端/应用程序,这可以用来访问受保护的资源(在我们的情况下是 RESTful API)。

我们已经在第三章中看到了使用公共身份验证提供程序的 OAuth 安全性,使用 CAS 和 JAAS 进行身份验证(在OAuth 2 和 OpenID 连接部分)。但是,我们不需要使用这些公共提供程序;您可以选择使用自己的提供程序。在本章中,我们将涵盖一个这样的示例,我们将使用自己的身份验证提供程序并保护基于 Spring Boot 的响应式 REST 端点。

在进入示例之前,我们需要更多地了解 OAuth,并且需要了解它的各个组件。我们已经在第三章中详细介绍了 OAuth 的许多细节,使用 CAS 和 JAAS 进行身份验证。我们将在本节中添加这些细节,然后通过代码示例进行讲解。

OAuth2 角色

OAuth 为用户和应用程序规定了四种角色。这些角色之间的交互如下图所示:

图 7:OAuth 角色交互

我们将详细了解这些 OAuth 角色中的每一个。

资源所有者

这是拥有所需受保护资源的消费客户端应用程序的用户。如果我们以 Facebook 或 Google 作为身份验证提供程序,资源所有者就是在这些平台上保存数据的实际用户。

资源服务器

这是以托管 API 的形式拥有受保护资源的服务器。如果以 Google 或 Facebook 为例,它们以 API 的形式保存配置文件信息以及其他信息。如果客户端应用程序成功进行身份验证(使用用户提供的凭据),然后用户授予适当的权限,他们可以通过公开的 API 访问这些信息。

客户端

这是用于访问资源服务器上可用的受保护资源的应用程序。如果用户成功验证并且客户端应用程序被用户授权访问正确的信息,客户端应用程序可以检索数据。

授权服务器

这是一个验证和授权客户端应用程序访问资源所有者和资源服务器上拥有的受保护资源的服务器。同一个服务器执行这两个角色并不罕见。

要参与 OAuth,您的应用程序必须首先向服务提供商(如 Google、Facebook 等)注册,以便通过提供应用程序名称、应用程序 URL 和回调 URL 进行身份验证。成功注册应用程序与服务提供商后,您将获得两个应用程序唯一的值:client application_idclient_secretclient_id可以公开,但client_secret保持隐藏(私有)。每当访问服务提供商时,都需要这两个值。以下图显示了这些角色之间的交互:

图 8:OAuth 角色交互

前面图中的步骤在这里有详细介绍:

  1. 客户端应用程序请求资源所有者授权它们访问受保护资源

  2. 如果资源所有者授权,授权授予将发送到客户端应用程序

  3. 客户端应用程序请求令牌,使用资源所有者提供的授权以及来自授权服务器的身份验证凭据

  4. 如果客户端应用程序的凭据和授权有效,授权服务器将向客户端应用程序发放访问令牌

  5. 客户端应用程序使用提供的访问令牌访问资源服务器上的受保护资源

  6. 如果客户端应用程序发送的访问令牌有效,资源服务器将允许访问受保护资源

授权授予类型

如图所示,为了让客户端开始调用 API,它需要以访问令牌的形式获得授权授予。OAuth 提供了四种授权类型,可以根据不同的应用程序需求使用。关于使用哪种授权授予类型的决定留给了客户端应用程序。

授权码流程

这是一种非常常用的授权类型,它在服务器上进行重定向。它非常适用于服务器端应用程序,其中源代码托管在服务器上,客户端上没有任何内容。以下图解释了授权码授权类型的流程:

图 9:授权码流程

前面图中的步骤在这里有详细介绍:

  1. 受保护资源的资源所有者将在浏览器中呈现一个屏幕,以授权请求。这是一个示例授权链接:https://<DOMAIN>/oauth/authorize?response_type=code&client_id=<CLIENT_ID>&redirect_uri=<CALLBACK_URL>&scope=<SCOPE>

这是上述链接中的重要查询参数:

    • client_id:我们在向服务提供商注册应用程序时获得的客户端应用程序 ID
  • redirect_uri:成功授权后,服务器将重定向到提供的 URL

  • response_type:客户端用来向服务器请求授权码的非常重要的参数

  • scope:指定所需的访问级别

  1. 如果资源所有者(用户)允许,他们点击授权链接,该链接被发送到授权服务器。

  2. 如果发送到授权服务器的授权请求经过验证并且成功,客户端将从授权服务器接收授权码授权,附加为回调 URL(<CALLBACK_URL>?code=<AUTHORIZATION_CODE>)中的查询参数,指定在步骤 1中。

  3. 使用授权授予,客户端应用程序从授权服务器请求访问令牌(https://<DOMAIN>/oauth/token?client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>&grant_type=authorization_code&code=<AUTHORIZATION_CODE>&redirect_uri=CALLBACK_URL)。

在此 URL 中,还必须传递客户端应用程序的client_secret,以及声明传递的代码是授权代码的grant_type参数。

  1. 授权服务器验证凭据和授权授予,并向客户端应用程序发送访问令牌,最好以 JSON 形式。

  2. 客户端应用程序使用步骤 5中收到的访问令牌调用资源服务器上的受保护资源。

  3. 如果步骤 5中提供的访问令牌有效,则资源服务器允许访问受保护资源。

隐式流

这在移动和 Web 应用程序中通常使用,并且也基于重定向工作。以下图表解释了隐式代码授权类型的流程:

图 10:隐式流

前面图表中的步骤在这里进行了详细解释:

  1. 资源所有者被呈现一个屏幕(浏览器)来授权请求。这是一个示例授权链接:https://<DOMAIN>/oauth/authorize?response_type=token&client_id=CLIENT_ID&redirect_uri=CALLBACK_URL&scope=<SCOPE>

重要的是要注意,前面链接中指定的response_typetoken。这表示服务器应该给出访问令牌(这是与前一节讨论的授权代码流授权类型的主要区别之一)。

  1. 如果资源所有者(用户)允许此操作,则点击授权链接,该链接将发送到授权服务器。

  2. 用户代理(浏览器或移动应用程序)在指定的CALLBACK_URL中接收访问令牌(https://<CALLBACK_URL>#token=<ACCESS_TOKEN>)。

  3. 用户代理转到指定的CALLBACK_URL,保留访问令牌。

  4. 客户端应用程序打开网页(使用任何机制),从CALLBACK_URL中提取访问令牌。

  5. 客户端应用程序现在可以访问访问令牌。

  6. 客户端应用程序使用访问令牌调用受保护的 API。

客户端凭据

这是最简单的授权方式之一。客户端应用程序将凭据(客户端的服务帐户)与client_IDclient_secret一起发送到授权服务器。如果提供的值有效,授权服务器将发送访问令牌,该令牌可用于访问受保护的资源。

资源所有者密码凭据

这是另一种简单易用的类型,但被认为是所有类型中最不安全的。在这种授权类型中,资源所有者(用户)必须直接在客户端应用程序界面中输入他们的凭据(请记住,客户端应用程序可以访问资源所有者的凭据)。然后客户端应用程序使用这些凭据发送到授权服务器以获取访问令牌。只有在资源所有者完全信任他们提供凭据给服务提供者的应用程序时,这种授权类型才有效,因为这些凭据通过客户端应用程序的应用服务器传递(因此如果客户端应用程序决定的话,它们可以被存储)。

访问令牌和刷新令牌

客户端应用程序可以使用访问令牌从资源服务器检索信息,该信息在令牌被视为有效的规定时间内。之后,服务器将使用适当的 HTTP 响应错误代码拒绝请求。

OAuth 允许授权服务器在访问令牌的同时发送另一个令牌,即刷新令牌。当访问令牌过期时,客户端应用程序可以使用第二个令牌请求授权服务器提供新的访问令牌。

Spring Security OAuth 项目

目前在 Spring 生态系统中,OAuth 支持已扩展到许多项目,包括 Spring Security Cloud、Spring Security OAuth、Spring Boot 和 Spring Security(5.x+)的版本。这在社区内造成了很多混乱,没有单一的所有权来源。Spring 团队采取的方法是整合这一切,并开始维护与 Spring Security 有关的所有 OAuth 内容。预计将在 2018 年底之前将 OAuth 的重要组件,即授权服务器、资源服务器以及对 OAuth2 和 OpenID Connect 1.0 的下一级支持,添加到 Spring Security 中。Spring Security 路线图清楚地说明,到 2018 年中期,将添加对资源服务器的支持,并在 2018 年底之前添加对授权服务器的支持。

在撰写本书时,Spring Security OAuth 项目处于维护模式。这意味着将发布用于修复错误/安全性问题的版本,以及一些较小的功能。未来不计划向该项目添加重大功能。

各种 Spring 项目中提供的完整 OAuth2 功能矩阵可以在github.com/spring-projects/spring-security/wiki/OAuth-2.0-Features-Matrix找到。

在撰写本书时,我们需要实现 OAuth 的大多数功能都作为 Spring Security OAuth 项目的一部分可用,该项目目前处于维护模式。

OAuth2 和 Spring WebFlux

在撰写本书时,Spring Security 中尚未提供 Spring WebFlux 应用程序的全面 OAuth2 支持。但是,社区对此非常紧迫,并且许多功能正在逐渐加入 Spring Security。许多示例也正在 Spring Security 项目中展示使用 Spring WebFlux 的 OAuth2。在第五章中,与 Spring WebFlux 集成,我们详细介绍了一个这样的示例。在撰写本书时,Spring Security OAuth2 对 Spring MVC 有严格的依赖。

Spring Boot 和 OAuth2

在撰写本书时,Spring Boot 宣布不再支持 Spring Security OAuth 模块。相反,它将从现在开始使用 Spring Security 5.x OAuth2 登录功能。

一个名为 Spring Security OAuth Boot 2 Autoconfig 的新模块(其在 pom.xml 中的依赖如下代码片段所示),从 Spring Boot 1.5.x 移植而来,可用于将 Spring Security 与 Spring Boot 集成:

<dependency>
 <groupId>org.springframework.security.oauth.boot</groupId>
 <artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>

项目源代码可以在github.com/spring-projects/spring-security-oauth2-boot找到。此模块的完整文档可以在docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/找到。

示例项目

在我们的示例项目中,我们将设置自己的授权服务器,我们将对其进行授权,以授权通过我们的资源服务器公开的 API。我们在我们的资源服务器上公开了电影 API,并且客户端应用程序将使用应用程序进行身份验证(客户端应用程序受 Spring Security 保护),然后尝试访问其中一个电影 API,此时 OAuth 流程将启动。成功与授权服务器进行授权检查后,客户端将获得对所请求的电影 API 的访问权限。

我们将有一个包含三个 Spring Boot 项目的父项目:oauth-authorization-serveroauth-resource-serveroauth-client-app

图 11:IntelliJ 中的项目结构

我们现在将在后续章节中查看每个单独的 Spring Boot 项目。完整的源代码可在书的 GitHub 页面上的spring-boot-spring-security-oauth项目下找到。

授权服务器

这是一个传统的 Spring Boot 项目,实现了授权服务器 OAuth 角色。

Maven 依赖

要包含在 Spring Boot 项目的pom.xml文件中的主要依赖项如下面的代码片段所示:

<!--Spring Boot-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--OAuth-->
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
  <version>2.3.2.RELEASE</version>
</dependency>
<!--JWT-->
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-jwt</artifactId>
  <version>1.0.9.RELEASE</version>
</dependency>

Spring Boot 运行类

这个 Spring Boot 的run类并没有什么特别之处,如下面的代码片段所示:

@SpringBootApplication
public class OAuthAuthorizationServerRun extends SpringBootServletInitializer {
  public static void main(String[] args) {
      SpringApplication.run(OAuthAuthorizationServerRun.class, args);
  }
}

Spring 安全配置

Spring 安全配置类扩展了WebSecurityConfigurerAdapter。我们将重写三个方法,如下面的代码片段所示:

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
  @Autowired
  private BCryptPasswordEncoder passwordEncoder;
  @Autowired
  public void globalUserDetails(final AuthenticationManagerBuilder auth) throws 
        Exception {
      auth
          .inMemoryAuthentication()
          .withUser("user").password(passwordEncoder.encode("password"))
          .roles("USER")
          .and()
          .withUser("admin").password(passwordEncoder.encode("password"))
          .roles("USER", "ADMIN");
  }
  //...
}

我们autowire密码编码器。然后我们重写以下方法:globalUserDetailsauthenticationManagerBeanconfigure。这里没有什么特别要提到的。我们在内存中定义了两个用户(用户和管理员)。

授权服务器配置

这是这个 Spring Boot 项目中最重要的部分,我们将在其中设置授权服务器配置。我们将使用一个新的注解@EnableAuthorizationServer。我们的配置类将扩展AuthorizationServerConfigurerAdapter。我们将使用 JWT 令牌存储,并展示一个令牌增强器,使用它可以在需要时增强 JWT 令牌的声明。这个配置类中最重要的方法被提取为下面的代码片段:

@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws 
        Exception {
  clients.inMemory()
     .withClient("oAuthClientAppID")
     .secret(passwordEncoder().encode("secret"))
     .authorizedGrantTypes("password", "authorization_code", "refresh_token")
     .scopes("movie", "read", "write")
     .accessTokenValiditySeconds(3600)
     .refreshTokenValiditySeconds(2592000)
     .redirectUris("http://localhost:8080/movie/", 
        "http://localhost:8080/movie/index");
}

这是我们设置与客户端相关的 OAuth 配置的地方。我们只设置了一个客户端,并使用内存选项使示例更容易理解。在整个应用程序中,我们将使用BCrypt作为我们的密码编码器。我们的客户端应用程序的客户端 ID 是oAuthClientAppID,客户端密钥是secret。我们设置了三种授权类型,访问客户端时需要指定必要的范围(movie、read 和 write)。执行成功后,授权服务器将重定向到指定的 URL(http://localhost:8080/movie/http://localhost:8080/movie/index)。如果客户端没有正确指定 URL,服务器将抛出错误。

JWT 令牌存储和增强相关方法如下面的代码片段所示:

@Bean
@Primary
public DefaultTokenServices tokenServices() {
  final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
  defaultTokenServices.setTokenStore(tokenStore());
  defaultTokenServices.setSupportRefreshToken(true);
  return defaultTokenServices;
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) 
    throws Exception {
  final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
  tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), 
    accessTokenConverter()));
  endpoints.tokenStore(tokenStore()).tokenEnhancer(tokenEnhancerChain)
    .authenticationManager(authenticationManager);
}
@Bean
public TokenStore tokenStore() {
  return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
  final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
  converter.setSigningKey("secret");
  return converter;
}
@Bean
public TokenEnhancer tokenEnhancer() {
  return new CustomTokenEnhancer();
}

在这段代码中,我们指定了将在tokenStore方法中使用的令牌存储,并声明了一个tokenEnhancer bean。为了展示令牌增强器,我们将使用一个名为CustomTokenEnhancer的自定义类;该类如下面的代码片段所示:

public class CustomTokenEnhancer implements TokenEnhancer {
  @Override
  public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, 
    OAuth2Authentication authentication) {
      final Map<String, Object> additionalInfo = new HashMap<>();
      additionalInfo.put("principalinfo", 
        authentication.getPrincipal().toString());
      ((DefaultOAuth2AccessToken)accessToken)
        .setAdditionalInformation(additionalInfo);
      return accessToken;
  }
}

自定义令牌enhancer类实现了TokenEnhancer。我们只是将新信息(principalinfo)添加到包含principal对象的toString版本的 JWT 令牌中。

应用程序属性

由于我们在本地运行了所有三个服务器,我们必须指定不同的端口。此外,授权服务器运行在不同的上下文路径上是很重要的。下面的代码片段显示了我们在application.properties文件中的内容:

server.servlet.context-path=/oauth-server
server.port=8082

作为一个 Spring Boot 项目,可以通过执行mvn spring-boot:run命令来运行。

资源服务器

这是一个传统的 Spring Boot 项目,实现了资源服务器 OAuth 角色。

Maven 依赖

在我们的pom.xml中,我们不会添加任何新的内容。我们在授权服务器项目中使用的相同依赖项也适用于这里。

Spring Boot 运行类

这是一个典型的 Spring Boot run类,我们在其中放置了@SpringBootApplication注解,它在幕后完成了所有的魔术。同样,在我们的 Spring Boot 运行类中没有特定于这个项目的内容。

资源服务器配置

这是主要的资源服务器配置类,我们在其中使用@EnableResourceServer注解,并将其扩展自ResourceServerConfigurerAdapter,如下面的代码片段所示:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
  @Autowired
  private CustomAccessTokenConverter customAccessTokenConverter;
  @Override
  public void configure(final HttpSecurity http) throws Exception {
      http.sessionManagement()
        .sessionCreationPolicy(SessionCreationPolicy.ALWAY)
        .and()
        .authorizeRequests().anyRequest().permitAll();
  }
  @Override
  public void configure(final ResourceServerSecurityConfigurer config) {
      config.tokenServices(tokenServices());
  }
  @Bean
  public TokenStore tokenStore() {
      return new JwtTokenStore(accessTokenConverter());
  }
  @Bean
  public JwtAccessTokenConverter accessTokenConverter() {
      final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
      converter.setAccessTokenConverter(customAccessTokenConverter);
      converter.setSigningKey("secret");
      converter.setVerifierKey("secret");
      return converter;
  }
  @Bean
  @Primary
  public DefaultTokenServices tokenServices() {
      final DefaultTokenServices defaultTokenServices = 
        new DefaultTokenServices();
      defaultTokenServices.setTokenStore(tokenStore());
      return defaultTokenServices;
  }
}

Spring 安全配置

作为资源服务器,我们启用了全局方法安全,以便每个暴露 API 的方法都受到保护,如下面的代码片段所示:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends GlobalMethodSecurityConfiguration {
  @Override
  protected MethodSecurityExpressionHandler createExpressionHandler() {
      return new OAuth2MethodSecurityExpressionHandler();
  }
}

在这里,我们使用OAuth2MethodSecurityExpressionHandler作为方法安全异常处理程序,以便我们可以使用注解,如下所示:

@PreAuthorize("#oauth2.hasScope('movie') and #oauth2.hasScope('read')")

Spring MVC 配置类

我们在之前的章节中详细介绍了 Spring MVC 配置。在我们的示例中,这是一个非常基本的 Spring MVC config类,其中使用了@EnableWebMvc并实现了WebMvcConfigurer

控制器类

我们有一个控制器类,只公开一个方法(我们可以进一步扩展以公开更多的 API)。这个方法列出了硬编码的电影列表中的所有电影,位于 URL/movie下,如下面的代码片段所示:

@RestController
public class MovieController {
   @RequestMapping(value = "/movie", method = RequestMethod.GET)
   @ResponseBody
   @PreAuthorize("#oauth2.hasScope('movie') and #oauth2.hasScope('read')")
   public Movie[] getMovies() {
      initIt();//Movie list initialization
      return movies;
   }
   //…
}

我们使用了一个Movie模型类,利用了lombok库的所有功能,如下面的代码片段所示:

@Data
@ToString
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Movie {
  private Long id;
  private String title;
  private String genre;
}

它有三个属性,注解将完成所有的魔术并保持模型简洁。

应用程序属性

与授权服务器类似,application.properties只有上下文路径和端口分配。

作为一个 Spring Boot 项目,可以通过执行mvn spring-boot:run命令来运行。

客户端应用程序

这是一个传统的 Spring Boot 项目,实现了客户端 OAuth 角色。

Maven 依赖项

在我们的 Spring Boot pom.xml文件中,添加了Thymeleaflombok库的新 Maven 依赖项。其余部分都是典型的 Spring Boot pom.xml文件,你现在已经熟悉了。

Spring Boot 类

在我们的示例 Spring Boot run类中,没有什么值得一提的。这是一个简单的类,包含了至关重要的main方法和@SpringBootApplication注解。

OAuth 客户端配置

这是客户端应用程序中的主配置类,使用了@EnableOAuth2Client注解,如下面的代码片段所示:

@Configuration
@EnableOAuth2Client
public class OAuthClientConfig {
  @Autowired
  private OAuth2ClientContext oauth2ClientContext;

  @Autowired
  @Qualifier("movieAppClientDetails")
  private OAuth2ProtectedResourceDetails movieAppClientDetails;

  @ConfigurationProperties(prefix = "security.oauth2.client.movie-app-client")
  @Bean
  public OAuth2ProtectedResourceDetails movieAppClientDetails() {
      return new AuthorizationCodeResourceDetails();
  }
  @Bean
  public BCryptPasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
  }
  @Bean
  public OAuth2RestTemplate movieAppRestTemplate() {
      return new OAuth2RestTemplate(movieAppClientDetails, oauth2ClientContext);
  }
}

在这个类中要注意的重要方面是,我们通过在application.yml文件中配置客户端详细信息来初始化 OAuth2 REST 模板。

Spring 安全配置

在 Spring 安全config类中,我们设置了可以用于登录到应用程序并访问受保护资源的用户凭据(内存中)。在configure方法中,一些资源被标记为受保护的,一些资源被标记为未受保护的。

控制器类

我们有两个控制器类,SecuredControllerNonSecuredController。顾名思义,一个用于声明的受保护路由,另一个用于未受保护的路由。我们感兴趣的受保护控制器中的main方法如下面的代码片段所示:

@RequestMapping(value = "/movie/index", method = RequestMethod.GET)
@ResponseBody
public Movie[] index() {
  Movie[] movies = movieAppRestTemplate
    .getForObject(movieApiBaseUri, Movie[].class);
  return movies;
}

我们将资源服务器项目中使用的model类复制到客户端应用程序项目中。在理想情况下,所有这些共同的东西都将被转换为可重用的 JAR,并设置为两个项目的依赖项。

模板

模板非常简单。应用程序的根上下文将用户重定向到一个未安全的页面。我们有自己的自定义登录页面,登录成功后,用户将被导航到一个包含指向受保护的 OAuth 支持的电影列表 API 的链接的受保护页面。

应用程序属性

在这个项目中,我们使用application.yml文件,代码如下:

server:
  port: 8080
spring:
  thymeleaf:
    cache: false
security:
  oauth2:
    client:
      movie-app-client:
        client-id: oAuthClientAppID
        client-secret: secret
        user-authorization-uri: http://localhost:8082/oauth-server/oauth/authorize
        access-token-uri: http://localhost:8082/oauth-server/oauth/token
        scope: read, write, movie
        pre-established-redirect-uri: http://localhost:8080/movie/index
movie:
  base-uri: http://localhost:8081/oauth-resource/movie

这个 YML 文件的非常重要的方面是movie-app-client属性设置。同样,作为一个 Spring Boot 项目,可以通过执行mvn spring-boot:run命令来运行。

运行项目

使用 Spring Boot 的mvn spring-boot:run命令分别启动所有项目。我在 IntelliJ 中使用 Spring Dashboard,可以启动所有项目,如下面的截图所示:

图 12:IntelliJ 中的 Spring Dashboard

导航到http://localhost:8080,您将被重定向到客户端应用程序的未安全页面,如下所示:

图 13:客户端应用程序的未安全页面

点击链接,您将被带到自定义登录页面,如下所示:

图 14:客户端应用程序的自定义登录页面

根据页面上的要求输入用户名/密码;然后,点击登录将带您到安全页面,如下所示:

图 15:客户端应用程序中的安全页面

点击电影 API 链接,您将被带到 OAuth 流程,然后到授权服务器的默认登录页面以输入凭据,如下所示:

图 16:授权服务器登录页面

输入用户名/密码(我们将其保留为 user/password),然后点击登录按钮。您将被带到授权页面,如下面的截图所示:

图 17:授权服务器上的授权页面

点击授权,您将被带回客户端应用程序页面,显示来自资源服务器的所有电影,如下所示:

图 18:客户端应用程序中显示资源服务器上暴露的电影 API 的电影列表页面

通过这样,我们完成了我们的示例应用程序,其中我们实现了 OAuth 的所有角色。

总结

我们在本章开始时向您介绍了一些需要跟进的重要概念。然后我们介绍了现代 Web 应用程序所需的重要特征。我们迅速介绍了一个称为SOFEA的架构,它恰当地介绍了我们想要构建现代应用程序的方式。然后我们通过最简单的方式实现了 REST API 的安全性。

在接下来的部分中,我们介绍了如何以更高级的方式使用 OAuth 来保护 REST API,使用 JWT。我们通过介绍了许多关于 OAuth 的概念开始了本节,最后以使用 OAuth 和 JWT 的完整示例项目结束了本章。

阅读完本章后,您应该对 REST、OAuth 和 JWT 有清晰的理解。您还应该在下一章中,对在应用程序中暴露的 RESTful 端点使用 Spring Security 感到舒适。

第七章:Spring 安全附加组件

在之前的章节中,我们介绍了核心安全方面(如身份验证和授权)使用 Spring 安全的多种方式的实现细节。在这样做时,我们只是浅尝辄止了 Spring 安全可以实现的能力的一层薄薄的表面。在本章中,我们将简要介绍 Spring 安全提供的一些其他能力。

此外,本章介绍了许多产品(开源和付费版本),可以考虑与 Spring 安全一起使用。我不支持这些产品中的任何一个,但我确实认为它们是实现您所寻找的技术能力的强有力竞争者。我们将通过简要介绍产品的技术能力的要点来介绍一个产品,然后简要介绍给您。

在本章中,我们将涵盖以下主题:

  • 记住我认证

  • 会话管理

  • CSRF

  • CSP

  • 通道安全

  • CORS 支持

  • 加密模块

  • 秘密管理

  • HTTP 数据完整性验证器

  • 自定义 DSL

记住我认证

我们将重用并增强我们在第二章中构建的示例,深入 Spring 安全jetty-db-basic-authentication),以解释 Spring 安全如何用于实现记住我或持久登录功能。在我们要重用的示例中,我们使用了基本身份验证,其中用户凭据存储在 MySQL 数据库中。

在 Spring 安全中,通过在用户选择在客户端记住他/她的凭据时向浏览器发送 cookie 来实现记住我功能。可以配置 cookie 在浏览器中存储一段时间。如果 cookie 存在且有效,用户下次访问应用程序时,将直接进入用户的主页,避免使用用户名/密码组合进行显式认证。

可以使用两种方法实现记住我功能:

  • 基于哈希的令牌:用户名、过期时间、密码和私钥被哈希并作为令牌发送到客户端

  • 持久令牌:使用持久存储机制在服务器上存储令牌

现在,我们将通过一个简单的持久令牌方法来详细解释这个概念。

在 MySQL 数据库中创建一个新表

我们将使用与我们在第二章中使用的 MySQL 数据库相同的模式。保持一切不变,然后在 MySQL 工作台中执行以下 DDL 语句来创建一个新表,用于存储持久令牌:

create table persistent_logins(
     series varchar(64) not null primary key,   
     username varchar(75) not null,
     token varchar(100) not null,
     last_used timestamp not null
);

Spring 安全配置

在第二章中,深入 Spring 安全(在Sample应用程序部分的 Spring 安全设置子部分),我们看到了基本身份验证,我们在 Spring 安全配置类的 configure 方法中进行了配置。在这个例子中,我们将创建一个自定义登录页面,并将登录机制更改为基于表单的。打开SpringSecurityConfig类,并按照下面的代码片段更改 configure 方法。然后,添加我们将使用的tokenRepository bean 来实现记住我功能:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http.csrf().disable();
  http.authorizeRequests().anyRequest().hasAnyRole("ADMIN", "USER")
      .and()
      .authorizeRequests().antMatchers("/login**").permitAll()
      .and()
      .formLogin()
      .loginPage("/login").loginProcessingUrl("/loginProc").permitAll()
      .and()
      .logout().logoutSuccessUrl("/login").permitAll()
      .and()
      .rememberMe()
      .rememberMeParameter("rememberme").tokenRepository(tokenRepository());
}
@Bean
public PersistentTokenRepository tokenRepository() {
  JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl=new JdbcTokenRepositoryImpl();
  jdbcTokenRepositoryImpl.setDataSource(dataSource);
  return jdbcTokenRepositoryImpl;
}

自定义登录页面

src/main/webapp/WEB-INF/view文件夹中创建一个新页面,名为login.jsp。页面的主要部分包含usernamepasswordrememberme字段,如下面的代码片段所示:

<form action='<spring:url value="/loginProc"/>' method="post">
  <table>
      <tr>
          <td>Username</td>
          <td><input type="text" name="username"></td>
      </tr>
      <tr>
          <td>Password</td>
          <td><input type="password" name="password"></td>
      </tr>
      <tr>
          <td><input type="checkbox" name="rememberme"></td>
          <td>Remember me</td>
      </tr>
      <tr>
          <td><button type="submit">Login</button></td>
      </tr>
  </table>
</form>

确保您将记住我复选框的名称与您在 Spring 安全配置中指定的名称相同。

运行应用程序并进行测试

通过执行以下命令来运行项目:

mvn jetty:run

等待控制台打印[INFO] Started Jetty Server。

打开浏览器(我在测试时使用 Firefox 的隐私模式)并导航到http://localhost:8080,你将看到你创建的自定义登录页面,如下截图所示:

图 1: 自定义登录页面

输入user/user@password作为用户名和密码。点击Remember me并点击Login按钮,你将被导航到用户主页,如下所示:

图 2: 用户主页

查询你的 MySQL 数据库的persistent_logins表,你将看到一个新记录,如下截图所示:

图 3: MySQLWorkbench 查询新的 persistent_logins 表

现在,打开浏览器中的开发者工具并检查 cookies。根据你使用的浏览器,你应该看到类似于这样的东西:

图 4: 浏览器 cookie 设置以实现记住我功能

这个示例的整个项目可以在书的 GitHub 页面的jetty-db-basic-authentication-remember-me项目中找到。

会话管理

Spring Security 允许你只需一些配置就可以管理服务器上的会话。以下是一些最重要的会话管理活动:

  • 会话创建: 这决定了何时需要创建会话以及您可以与之交互的方式。在 Spring Security 配置中,输入以下代码:
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);

有四种会话创建策略可供选择。它们如下:

    • ALWAYS: 如果不存在会话,总是创建一个会话。
  • IF_REQUIRED: 如果需要,会创建一个会话。

  • NEVER: 永远不会创建会话;相反,它将使用已存在的会话。

  • 无状态: 不会创建或使用会话。

  • invalidSession: 这控制着服务器检测到无效会话时如何通知用户:

http.sessionManagement().invalidSessionUrl("/invalidSession");
  • 会话超时: 这控制着用户在会话过期时如何被通知。

  • 并发会话: 这允许控制用户在应用程序中可以启动多少个会话。如果最大会话数设置为1,当用户第二次登录时,先前的会话将被失效,用户将被注销。如果指定的值大于1,则允许用户同时拥有这么多会话:

http.sessionManagement().maximumSessions(1);

下面的截图显示了默认的错误屏幕,当同一用户创建了超过所配置的期望数量的会话时弹出:

图 5: 用户访问多个会话时抛出的错误

  • 会话固定: 这与并发会话控制非常相似。此设置允许我们控制用户启动新会话时会发生什么。我们可以指定以下三个值:

  • migrateSession: 在成功认证后创建新会话时,旧会话将被失效,并且所有属性将被复制到新会话:

http.sessionManagement().sessionFixation().migrateSession();
  • newSession: 创建一个新会话,而不复制先前有效会话的任何属性:
http.sessionManagement().sessionFixation().newSession();
  • : 旧会话被重用并且不会失效:
http.sessionManagement().sessionFixation().none();

CSRF

跨站请求伪造CSRF)(www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF))是一种攻击,它迫使最终用户在当前已认证的 Web 应用程序上执行不需要的操作。CSRF 攻击专门针对改变状态的请求,而不是数据的窃取,因为攻击者无法看到伪造请求的响应。

开放 Web 应用安全项目OWASP)认为 CSRF 是 Web 应用程序中最常见的安全风险之一。OWASP 每年发布一个名为 OWASP 十大的列表,突出显示困扰 Web 应用程序的前 10 个安全风险,它认为 CSRF 位列第五位。

在 Spring Security 中,默认情况下启用 CSRF。如果需要(我们已在许多示例中禁用了这一点,以便能够集中精力关注示例应传达的主要概念),我们可以通过在 Spring Security 配置中添加以下代码片段来显式禁用它:

http
  .csrf().disable();

即使 CSRF 默认启用,但为了使其正常工作,每个请求都需要提供 CSRF 令牌。如果未将 CSRF 令牌发送到服务器,服务器将拒绝请求并抛出错误。如果您将Java 服务器页面JSP)作为视图,只需包含隐藏输入,如下面的代码片段所示,许多事情都会自动发生:

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />

如果您使用 AJAX 请求调用服务器,可以以 HTTP 标头的形式提供 CSRF 令牌,而不是隐藏输入。您可以将与 CSRF 相关的标头声明为元标记,如下面的代码片段所示:

<head>
     <meta name="_csrf" content="${_csrf.token}"/>
     <meta name="_csrf_header" content="${_csrf.headerName}"/>
     <!-- ... -->
 </head>

之后,在调用服务器时,将这些(_csrf_csrf_header)作为标头包含进去,您将被允许调用所需的端点。

如果您想要持久保存 CSRF 令牌,Spring Security 允许您通过调整配置来实现,如下面的代码片段所示:

http
  .csrf()
  .csrfTokenRepository(new CookieCsrfTokenRepository());

在执行此操作时,CSRF 令牌将作为 cookie 持久保存,服务器可以读取并验证(所有这些都是自动完成的)。

CSP

内容安全策略CSP)(developer.mozilla.org/en-US/docs/Web/HTTP/CSP)是一种增加的安全层,有助于检测和缓解某些类型的攻击,包括跨站脚本XSS)和数据注入攻击。这些攻击用于从数据窃取到网站损坏或恶意软件分发等各种用途。

在应用程序中进行适当的 CSP 设置可以处理内容注入漏洞,并是减少 XSS 的好方法。XSS 在 OWASP 十大中排名第二。

CSP 并非处理所有注入漏洞的解决方案,但可以用作减少注入攻击的工具之一。

CSP 是一种声明性策略,使用 HTTP 标头实现。它可以在应用程序中以两种模式运行:

  • 生产模式(声明为 CSP)

  • 仅报告模式(用于测试并声明为Content-Security-Policy-Report-Only

CSP 包含一组安全策略指令,负责对 Web 资源施加适当的限制,然后在违规时相应地通知客户端(用户代理)。例如,以下安全策略片段从定义的受信任域加载脚本:

Content-Security-Policy: script-src https://trusted-domain.com

如果发生违规行为,用户代理将阻止它,如果策略指定了report-uri参数,如下例所示,它将以 JSON 的形式向该 URI 报告违规行为:

Content-Security-Policy: script-src https://trusted-domain.com; report-uri /csp-report-api/

前面的示例展示了 CSP 在生产模式下的工作。如果您想首先测试安全策略,然后在一段时间后将这些策略转为生产模式,CSP 提供了一种机制,如下面的代码片段所示:

Content-Security-Policy-Report-Only: script-src https://trusted-domain.com; report-uri /csp-report-api/

在仅报告模式下,当检测到违规行为时,报告将以 JSON 格式发布到report-uri,如下例所示:

{"csp-report":
    {"document-uri":"...",
    "violated-directive":"script-src https://trusted-domain.com",
    "original-policy":"...",
    "blocked-uri":"https://untrusted-domain.com"}
}

除了前面示例中详细介绍的安全指令之外,还有一些安全指令可在设置 CSP 时使用。有关完整的指令列表,请参阅content-security-policy.com/

与 CSRF 令牌类似,CSP 也可以用于确保在访问服务器时特定资源包含一个令牌。以下示例显示了使用这种 nonce 方法:

Content-Security-Policy: script-src 'self' 'nonce-<cryptographically generated random string>'

与 CSRF 令牌类似,这个 nonce 必须在服务器中的任何资源访问中包含,并且在加载页面时必须新生成。

CSP 还允许您仅在资源与服务器期望的哈希匹配时加载资源。以下策略用于实现这一点:

Content-Security-Policy: script-src 'self' 'sha256-<base64 encoded hash>'

CSP 受到几乎所有现代浏览器的支持。即使某些浏览器不支持某些安全指令,其他支持的指令也可以正常工作。处理这个问题的最佳方式是通过解密用户代理,只发送浏览器肯定支持的安全指令,而不是在客户端上抛出错误。

使用 Spring Security 的 CSP

使用 Spring Security 配置 CSP 非常简单。默认情况下,CSP 是未启用的。您可以在 Spring Security 配置中启用它,如下所示:

http
     .headers()
         .contentSecurityPolicy("script-src 'self' https://trusted-domain.com; report-uri /csp-report-api/");

Spring Security 配置中的报告仅 CSP 如下:

http
     .headers()
         .contentSecurityPolicy("script-src 'self' https://trusted-domain.com; report-uri /csp-report-api/")
        .reportOnly();

通道安全

除了身份验证和授权之外,Spring Security 还可以用于检查每个到达服务器的请求是否具有任何额外的属性。它可以检查协议(传输类型、HTTP 或 HTTPS)、某些 HTTP 头的存在等。SSL 现在是任何 Web 应用程序(或网站)遵守的事实标准,并且许多搜索引擎(例如 Google)甚至会对您的网站不使用 HTTPS 进行惩罚。SSL 用于保护从客户端到服务器以及反之的数据流通道。

Spring Security 可以配置为显式检查 URL 模式,并在使用 HTTP 协议访问时显式将用户重定向到 HTTPS。

这可以通过在 Spring Security 配置中配置适当的 URL 模式来轻松实现,如下所示:

http.authorizeRequests()
      .requiresChannel().antMatchers("/httpsRequired/**").requiresSecure();

当用户访问/httpsRequired/**URL 模式并且协议是 HTTP 时,Spring Security 将用户重定向到相同的 URL,使用 HTTPS 协议。以下配置用于保护所有请求:

http.authorizeRequests()
      .requiresChannel().anyRequest().requiresSecure();

要明确指定某些 URL 为不安全的,请使用以下代码:

.requiresChannel().antMatchers("/httpRequired/**").requiresInsecure();

以下代码片段显示了如何指定任何请求为 HTTP(不安全):

.requiresChannel().anyRequest().requiresInsecure();

CORS 支持

跨域资源共享(CORS)(developer.mozilla.org/en-US/docs/Web/HTTP/CORS)是一种机制,它使用额外的 HTTP 头来告诉浏览器,让一个在一个源(域)上运行的 Web 应用程序有权限访问来自不同源服务器的选定资源。当 Web 应用程序请求具有不同源(域、协议和端口)的资源时,它会发出跨源 HTTP 请求。

在本节中,我们不会创建完整的项目来解释 CORS 的工作原理。我们将使用代码片段,并解释每一部分代码,以便本节简洁明了。

根据以下代码片段更改 Spring Security 配置:

@EnableWebSecurity
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

 @Override
 protected void configure(HttpSecurity http) throws Exception {
    http.cors();
 }
 @Bean
 CorsConfigurationSource corsConfigurationSource() {
    UrlBasedCorsConfigurationSource urlCorsConfigSrc = new
          UrlBasedCorsConfigurationSource();
    urlCorsConfigSrc.registerCorsConfiguration("/**", 
        new CorsConfiguration().applyPermitDefaultValues());
    return urlCorsConfigSrc;
 }
}

在上述代码中,我们在 Spring Security 的configure方法中配置了 CORS。然后我们创建了一个新的 bean,corsConfigurationSource,在其中启用了*/***路径以供其他域访问。在许多情况下,这并不是真正理想的,下面的代码片段显示了更加强化的CorsConfiguration类:

CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(new ArrayList<String>(Arrays.asList("*")));
configuration.setAllowedHeaders(new ArrayList<String>        
    (Arrays.asList("Authorization", "Cache-Control", "Content-Type")));
configuration.setAllowedMethods(new ArrayList<String>(Arrays.asList("HEAD", 
    "GET", "POST", "PUT", "DELETE", "PATCH")));
configuration.setAllowCredentials(true);

如果是 Spring MVC 应用程序,可以通过创建一个 bean 来指定 CORS 映射,如下所示:

@Configuration
public class SpringMVCConfig {
  @Bean
  public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
          .allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE",
            "PATCH","OPTIONS");
      }
    };
  }
}

我从第二章中复制了一个先前的示例,深入 Spring 安全,并在本章中创建了一个新项目,其中包含spring-boot-in-memory-basic-authentication-with-cors的完整源代码。我们在这里所做的是通过声明CorsConfigurationSource bean 来设置 CORS 全局配置。

加密模块

Spring Security Crypto 模块允许您进行密码编码、对称加密和密钥生成。该模块作为核心 Spring Security 提供的一部分捆绑在一起,不依赖于其他 Spring Security 代码。

密码编码

现代化的密码编码是 Spring Security 5 的新功能之一。Spring Security 的PasswordEncoder接口是其核心,它使用各种算法对密码进行单向哈希处理,然后可以安全地存储。Spring Security 支持多种密码编码算法:

  • BcryptPasswordEncoder:这使用 Bcrypt 强哈希函数。您可以选择性地提供强度参数(默认值为 10);值越高,哈希密码所需的工作量就越大。

  • Pbkdf2PasswordEncoder:这使用基于密码的密钥派生函数 2PKDF2),具有可配置的迭代次数和 8 字节的随机盐值。

  • ScryptPasswordEncoder:这使用 Scrypt 哈希函数。在哈希过程中,客户端可以提供 CPU 成本参数、内存成本参数和并行化参数。当前实现使用 Bouncy Castle 库。

加密

Spring Security 的org.springframework.security.crypto.encrypt.Encryptors类有工厂方法,可以用来创建对称加密器。该类支持两种加密器:

  • BytesEncryptor:用于对原始字节数组形式的数据进行对称数据加密的服务接口。

  • TextEncryptor:用于对文本字符串进行对称数据加密的服务接口:

密钥生成

如前面加密部分所示,Spring Security 有一个类,即org.springframework.security.crypto.keygen.KeyGenerators,它有许多工厂方法,可以用来构造应用程序所需的许多密钥。

以下是支持的两种密钥生成器类型:

  • BytesKeyGenerator:用于生成唯一基于字节数组的密钥的生成器。

  • StringKeyGenerator:用于生成唯一字符串密钥的生成器:

图 7:BytesKeyGenerator 和 StringKeyGenerator 工厂方法

秘密管理

在应用程序中,我们需要处理各种形式的 API 密钥、其他应用程序密码等秘密/安全数据。通常情况下,对于部署和运行在生产环境中的应用程序,将这些数据以明文形式存储可能导致安全漏洞。如今,自动化技术变得非常便宜,对于现代应用程序来说,安全地存储这些数据并进行访问控制是必不可少的。

加密是被广泛接受的,但是对于解密,需要传播一个密钥,而这个密钥的传播通常是一个大问题。如果一个人决定将密钥带出组织,就会出现严重问题。

HashiCorp 的 Vault 是解决此问题的一个非常有力的竞争者,并且可以帮助轻松管理这些秘密,并具有非常严格的控制。它提供了基于设置策略的访问 API。它还具有提供访问控制的功能,并且还具有开箱即用的加密功能。此外,它具有各种持久后端支持,例如 Consul(来自 HashiCorp)等,使企业可以轻松采用它。Vault 是用 Go 编写的,并且可以在许多平台上下载到二进制文件,并且可以从其网站下载。在本节中,我们将快速介绍 Vault 产品本身,然后通过一个示例,我们将创建一个 Spring Boot 项目,并安全地访问 Vault 中存储的一些秘密。言归正传,让我们开始实际的代码。

从解封 Vault 开始

从 Vault 项目的网站(www.vaultproject.io/downloads.html)下载最新的二进制文件,根据您的操作系统进行安装。要启动 Vault,您需要一个文件vault.conf,其中我们将指定 Vault 启动所需的一些选项。以下是一个示例vault.conf文件,您可以使用:

backend "inmem" {
} 
listener "tcp" {
  address = "0.0.0.0:8200"
  tls_disable = 1
}
disable_mlock = true

vault.conf文件中,我们明确设置它将监听的地址,并且还禁用了 TLS/SSL(以便以纯文本模式运行)。

通过以下命令指定vault.conf文件的位置启动 Vault:

./vault server -config vault.conf

如下面的屏幕截图所示,Vault 以纯文本模式运行(已禁用 TLS/SSL):

图 8:启动和配置 Vault

打开一个新的命令提示符,我们现在将开始管理 Vault。通过执行以下命令设置一个环境变量,让客户端知道他们必须使用纯文本连接到 Vault(因为我们已禁用了 TLS/SSL):

export VAULT_ADDR=http://127.0.0.1:8200

之后,通过执行以下命令初始化 Vault 密钥生成:

图 9:初始化 Vault

我们使用的命令给了我们五个密钥份额和两个密钥阈值。重要的是要注意,一旦 Vault 被初始化,就无法更改这些值(输出仅显示一次)。务必收集必要的信息;否则,您将无法检索存储在 Vault 中的任何数据。如前面的屏幕截图所示,Vault 的init命令为我们提供了解封 Vault 所需的密钥和令牌。在我们可以使用 Vault 之前,必须对其进行解封。

解封 (www.vaultproject.io/docs/concepts/seal.html) 是构建读取解密密钥以解密数据所必需的主密钥的过程,从而允许访问 Vault。在解封之前,几乎无法对 Vault 进行任何操作。

您可以通过执行以下命令并提供在 Vault 初始化过程中生成的任何密钥来解封 Vault:

./vault unseal <any key generated using initialization>

以下屏幕截图显示了上述命令的成功执行:

图 10:解封 Vault

一旦解封,您的 Vault 现在已准备好存储您可能想在应用程序中使用的秘密数据。

成功解封 Vault 后,要存储任何数据,您首先需要进行身份验证。当我们初始化 Vault 时,会显示一个令牌(在屏幕上),此令牌用于进行身份验证。使用此令牌进行身份验证的最简单方法之一是设置一个新的环境变量(VAULT_TOKEN)。执行以下命令,当 Vault 启动时,它将使用此环境变量并进行身份验证:

export VAULT_TOKEN=ee60f275-7b16-48ea-0e74-dc48b4b3729c

执行上述命令后,现在可以通过执行以下命令编写您的秘密:

./vault write secret/movie-application password=randomstring

输入命令后,您应该收到以下输出:

图 11:将秘密写入 Vault

令牌是 Vault 中进行身份验证的主要方式。除此之外,还有其他机制,如 LDAP 和用户名/密码,可以进行身份验证。

Spring Boot 项目

Spring 有一个专门的模块,称为 Spring Cloud Vault,可以轻松地在应用程序中使用 Vault。Spring Cloud Vault 非常易于使用,我们将在本节中介绍如何使用它。

Spring Cloud Vault Configcloud.spring.io/spring-cloud-vault/)为分布式系统中的外部化配置提供了客户端支持。使用 HashiCorp 的 Vault,您可以管理应用程序在所有环境中的外部秘密属性的中心位置。Vault 可以管理静态和动态秘密,如远程应用程序/资源的用户名/密码,并为 MySQL、PostgreSQL、Apache Cassandra、MongoDB、Consul、AWS 等外部服务提供凭据。

我们将使用 Spring Boot 项目(使用 Spring Initializr 生成,start.spring.io)。在应用程序启动时,Vault 会启动并获取所有的秘密:

图 12:创建一个空的 Spring Initializr 项目

通过执行以下命令解压下载的 Spring Initializr 项目:

unzip -a spring-boot-spring-cloud-vault.zip

在您喜欢的 IDE 中导入项目(我在使用 IntelliJ)。

Maven 依赖项

确保您的项目的pom.xml中添加了以下 Maven 依赖项:

<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-vault-config</artifactId>
 <version>2.0.0.RELEASE</version>
</dependency>

当 Spring Boot 项目启动时,如果 Vault 服务器运行在端口8200上,它将选择默认的 Vault 配置。如果您想自定义这些属性,可以指定bootstrap.ymlbootstrap.properties。在我们的示例中,我们将明确设置bootstrap.yml文件,内容如下:

spring:
  application:
      name: movie-application
spring.cloud.vault:
  host: localhost # hostname of vault server
  port: 8200  # vault server port
  scheme: http # connection scheme http or https
  uri: http://localhost:8200 # vault endpoint
  connection-timeout: 10000 # connection timeout in milliseconds
  read-timeout: 5000  # read timeout in milliseconds
  config:
      order: -10  # order for property source
  token: ee60f275-7b16-48ea-0e74-dc48b4b3729c
health.vault.enabled: true  # health endpoint enabled using spring actuator

我们将使用 HTTP 方案,因为我们以纯文本模式启动了 Vault。如果您想使用 HTTPS,这也很容易做到,因为大多数操作都是通过提供的脚本完成的。这是 Vault 运行的默认方案,在生产设置中必须是这样。在实现实际用例时,让我们先了解这个概念,然后再深入一点。

如果您想在 HTTPS 方案中运行 Vault,Spring Cloud Vault 在其源代码的src/test/bash目录下提供了许多脚本(github.com/spring-cloud/spring-cloud-vault/tree/master/src/test/bash),可以用于创建必要的证书,然后在此方案下运行 Vault。为了简洁起见,我们不会在这里详细介绍这个方面。

.yml文件中,我们使用了作为 Vault 初始化的一部分创建的根令牌。如果需要,可以通过执行以下命令获取新令牌:

./vault token create

以下截图显示了token create命令的成功执行:

图 13:创建新的 Vault 令牌

在您的 Spring Boot 项目中,在应用程序运行类SpringBootSpringCloudVaultApplication中添加以下代码片段:

@Value("${password}")
String password;

@PostConstruct
private void postConstruct() {
 System.out.println("Secret in Movie application password is: " + password);
}

在此代码中,password字段将由 Spring Cloud Vault 填充,如果您运行应用程序(使用命令mvn spring-boot:run),您应该看到 Spring Cloud Vault 连接到运行的 Vault(使用bootstrap.yml文件中的配置)并检索我们为movie-application写入 Vault 的值。

这结束了我们对使用 Spring Boot 和 Spring Cloud Vault 的基本应用程序的介绍。您可以在本书的 GitHub 页面中的本章项目中查看完整的源代码,名称为spring-boot-spring-cloud-vault

HTTP 数据完整性验证器

Spring Security 帮助我们通过简单的方式和最少的代码来丰富我们的应用程序的常见安全功能。然而,Spring Security 正在逐渐赶上现代应用程序中需要的许多额外安全功能。这些应用程序大多部署在云上,并且每天都有大量的变更推送到生产环境。HTTP 数据完整性验证器HDIV)是一个可以用来进一步丰富您的应用程序安全性的产品。

什么是 HDIV?

HDIV 最初是作为一个开源项目诞生的,当时由 Roberto Velasco、Gotzon Illarramendi 和 Gorka Vicente 开发,以应对在生产环境中检测到的安全问题。第一个稳定版本 1.0 于 2008 年发布,以安全库的形式集成到 Web 应用程序中。2011 年,HDIV 正式与 Spring MVC 集成,这是最常用的 Java 解决方案,用于 Web 应用程序开发。2012 年,HDIV 与 Grails 集成。2015 年,HDIV 被包含在 Spring Framework 官方文档中,作为与 Web 安全相关的解决方案。基于全球的兴趣和对高市场需求的回应,创始人成立了HDIV Security(hdivsecurity.com/)公司,并于 2016 年推出了 HDIV 的商业版本。HDIV 解决方案在开发过程中构建到应用程序中,以提供最强大的运行时应用程序自我保护RASP)来抵御 OWASP 十大威胁。

HDIV 诞生的目的是保护应用程序免受参数篡改攻击。它的第一个目的(从首字母缩写来看)是保证服务器生成的所有数据的完整性(不进行数据修改)。HDIV 通过添加安全功能来扩展 Web 应用程序的行为,同时保持 API 和框架规范。HDIV 逐渐增加了 CSRF、SQL 注入SQLi)和 XSS 保护等功能,从而提供了更高的安全性,不仅仅是一个 HTTP 数据完整性验证器。

攻击成本越来越低,自动化程度越来越高。手动安全测试变得成本高昂。Spring Security 通过轻松实现最重要的安全方面(如身份验证和授权)来保护应用程序,但不能保护应用程序代码中常见的安全漏洞和设计缺陷。这就是集成已经使用 Spring Security 进行保护的 Spring 应用程序可以引入 HDIV 的地方。我们将通过一个非常简单的示例来展示 HDIV 的一些亮点。以下是他们网站上详细介绍的一些优势:

  • HDIV 在利用源代码之前检测安全漏洞,使用运行时数据流技术报告漏洞的文件和行号。开发人员在开发过程中可以立即在 Web 浏览器或集中式 Web 控制台中进行报告。

  • 它可以保护免受业务逻辑缺陷的影响,无需学习应用程序,并提供检测和保护免受安全漏洞的影响,而无需更改源代码。

  • HDIV 使得渗透测试工具(Burp Suite)与应用程序之间的集成成为可能,向渗透测试人员传递有价值的信息。它避免了许多手工编码的步骤,将渗透测试人员的注意力和努力集中在最脆弱的入口点上。

有关更多信息,您可以查看以下链接:hdivsecurity.com/

让我们开始构建一个简单的示例,展示 HDIV 在保护应用程序中链接和表单数据方面的保护。

Bootstrap 项目

我们将使用通过 Spring Initializr 创建的基础项目来创建我们的 HDIV 示例,如下所示:

图 14:基本的 Spring Initializr 项目设置

Maven 依赖项

在以下代码中,我们正在调用我们需要作为项目一部分的显式依赖项,即 HDIV:

<!--HDIV dependency-->
<dependency>
   <groupId>org.hdiv</groupId>
   <artifactId>spring-boot-starter-hdiv-thymeleaf</artifactId>
   <version>1.3.1</version>
   <type>pom</type>
</dependency>

HDIV 支持多种 Web 应用程序框架。在我们的示例中,我们将使用 Spring MVC 以及 Thymeleaf 和上述依赖项来处理这个问题。

Spring Security 配置

到目前为止,您已经知道 Spring Security 配置文件中包含什么。我们将进行内存身份验证,并配置两个用户(与本书中一直在做的类似)。我们将进行基于表单的登录,并将创建自己的登录页面。

Spring MVC 配置

到目前为止,我们一直在看的 Spring MVC 配置非常基本。这里没有什么值得特别提及的。我们只需要确保附加到登录页面的控制器被明确定义。

HDIV 配置

这个神奇的类将在不太麻烦的情况下为您的应用程序带来 HDIV 功能。完整的类如下所示:

@Configuration
@EnableHdivWebSecurity
public class HdivSecurityConfig extends HdivWebSecurityConfigurerAdapter {
    @Override
    public void addExclusions(final ExclusionRegistry registry) {
        registry.addUrlExclusions("/login");
    }
}

这个类的重要工作是由我们正在扩展的类HdivWebSecurityConfigurerAdapter完成。此外,@EnableHdivWebSecurity注解确保大部分设置会自动处理。我们只需要确保我们的登录页面 URL 的配置通过覆盖addExclusions方法来排除 HDIV 安全。

模型类

我们将使用本书中一直在使用的相同模型类Movie。为了简化编码,我们将使用 Lombok 库,该库通过查看类中配置的各种注释来完成所有魔术。

控制器类

我们只会有一个控制器类,我们将在这个示例中映射所有要创建的页面。为展示 HDIV 的功能,我们将看到 HDIV 在两种情况下的运行:

  • 一个电影创建页面(电影 bean),显示在包含表单的页面中 HDIV 的工作

  • 一个显示 HDIV 拦截并在有人操纵实际链接时抛出错误的链接页面

该类非常简单,这里不需要详细说明。

页面

如前所述,我们将在我们的示例中创建以下页面:

  • login.html:我们将用于用户登录应用程序的自定义登录页面

  • main.html:成功登录后用户导航到的页面,包含指向电影创建和链接页面的链接

  • links.html:用户单击链接 URL 时导航到的页面

  • movie.html:电影创建页面,包含两个字段——标题和类型

运行应用程序

通过执行以下命令运行应用程序,就像运行任何其他 Spring Boot 项目一样:

mvn spring-boot:run

转到浏览器并导航到http://localhost:8080,您将看到一个登录页面,如下所示:

图 15:登录页面

如前面的截图所示,输入用户名/密码并单击“登录”按钮,您将被导航到主页:

图 16:成功登录后呈现给用户的主页

单击链接导航到创建新电影的页面。您将被导航到以下截图中显示的页面。仔细观察 URL,您将看到已添加新的查询参数_HDIV_STATE_。服务器通过查看该值来验证并确保提交的表单是真实的:

图 17:创建电影屏幕,展示 HDIV_STATE 查询字符串

现在返回主页并单击链接页面。您将被导航到以下页面:

图 18:链接页面,显示 HDIV_STATE 查询字符串

如页面所述,尝试操纵链接(更改_HDIV_STATE_值),您将被带到 HDIV 错误页面:

图 19:HDIV 错误页面,在错误条件下显示

此示例展示了 HDIV 在与 Spring Security 一起工作时显示其价值的两种情况。有关更多详细信息,请查看 HDIV 网站和文档,网址如下:

自定义 DSL

Spring Security 允许您编写自己的领域特定语言DSL),该语言可用于配置应用程序中的安全性。当我们使用 OKTA 实现 SAML 身份验证时,我们已经看到了自定义 DSL 的实际应用。我们使用了由 OKTA 提供的自定义 DSL 来配置 Spring Security。

要编写自己的自定义 DSL,您可以扩展AbstractHttpConfigurer 并覆盖其中的一些方法,如下所示:

public class CustomDSL extends AbstractHttpConfigurer<CustomDSL, HttpSecurity> {
    @Override
    public void init(HttpSecurity builder) throws Exception {
       // Any configurations that you would like to do (say as default) can be  
       configured here
    }

    @Override
    public void configure(HttpSecurity builder) throws Exception {
       // Can add anything specific to your application and this will be honored
    }
}

在您的 Spring Security 配置类(configure 方法)中,您可以使用自定义 DSL,如下所示:

@Override
 protected void configure(HttpSecurity http) throws Exception {
     http
         .apply(<invoke custom DSL>)
         ...;
 }

当 Spring Security 看到自定义 DSL 设置时,代码的执行顺序如下:

  1. 调用 Spring Security 配置类的configure方法

  2. 调用自定义 DSL 的init方法

  3. 调用自定义 DSL 的configure方法

Spring Security 使用这种方法来实现authorizeRequests()

摘要

本章向您介绍了 Spring Security 的一些其他功能,这些功能可以在您的应用程序中使用。通过示例,我们介绍了如何在应用程序中实现记住我功能。我们还简要涉及了 CSRF、CORS、CSP、通道安全和会话管理等概念。我们还简要介绍了 Spring Security 中的加密模块。

我们通过介绍了两种可以与 Spring Security 一起使用的产品来结束了本章——HashiCorp Vault(用于秘密管理)和 HDIV(用于附加安全功能)。

阅读完本章后,您应该清楚地了解了一些可以使用 Spring Security 实现的附加功能。您还应该对可以与 Spring Security 一起使用以实现现代应用程序所需的一些最重要的技术功能有很好的理解。

现在,如果您正在阅读本章,那么请为自己鼓掌,因为通过本章,我们完成了本书。我希望您已经享受了本书的每一部分,并且希望您已经学到了一些可以用于创建精彩和创新的新应用程序的新知识。

谢谢阅读!

posted @ 2024-05-24 10:56  绝不原创的飞龙  阅读(27)  评论(0编辑  收藏  举报