使用 Spring Boot 构建 REST API

打印 上一主题 下一主题

主题 1688|帖子 1688|积分 5064

官网学习地址:结论 - Spring Academy
使用 Spring Boot 构建 REST API

1. Spring Initializr构建springboot

使用示例构建


  • Project: Gradle - Groovy
  • Language: Java
  • SpringBoot: Choose the latest 3.3.X version

    • Group: example
    • Artifact: cashcard
    • Name: CashCard
    • Description: CashCard service for Family Cash Cards
    • Packaging: Jar
    • Java: 17
    ADD DEPENDENCIES
       

    • Web options: Spring Web

  1. curl -o 'cashcard.zip' 'https://start.spring.io/starter.zip?type=gradle-project&language=java&dependencies=web&name=CashCard&groupId=example&artifactId=cashcard&description=CashCard+service+for+Family+Cash+Cards&packaging=jar&packageName=example.cashcard&javaVersion=17' && unzip -d 'cashcard' 'cashcard.zip'
复制代码
  1. [~] $ cd cashcard
  2. [~/cashcard] $
复制代码
Next, run the ./gradlew build command:
  1. [~/cashcard] $ ./gradlew build
复制代码
The output shows that the application passed the tests and was successfully built.
  1. [~/cashcard] $ ./gradlew build
  2. Downloading https://services.gradle.org/distributions/gradle-bin.zip............10%............20%............30%.............40%............50%............60%............70%.............80%............90%............100%Welcome to Gradle!...Starting a Gradle Daemon (subsequent builds will be faster)> Task :test...BUILD SUCCESSFUL in 39s7 actionable tasks: 7 executed
复制代码
2. API 条约 & JSON

API 协定

软件行业已经采用了多种模式来捕获文档和代码中商定的 API 行为。这些协议通常称为 “条约”。两个示例包括 Consumer Driven Contracts 和 Provider Driven Contracts。我们将为这些模式提供资源,但不会在本课程中详细讨论它们。相反,我们将讨论一个称为 API 协定的轻量级概念。
我们将 API 左券定义为软件提供者和消耗者之间的正式协议,该协议抽象地传达了怎样相互交互。此协定定义了 API 提供者和使用者怎样交互、数据交换是什么样子,以及怎样传告竣功和失败案例。
提供者和使用者不必共享雷同的编程语言,只需共享雷同的 API 协定。对于 Family Cash Card 域,我们假设当前 Cash Card 服务与使用它的所有服务之间有一个条约。下面是第一个 API 协定的示例。如果您不了解整个条约,请不要担心。在您完成本课程时,我们将介绍以下信息的各个方面。
  1. Request
  2.   URI: /cashcards/{id}
  3.   HTTP Verb: GET
  4.   Body: None
  5. Response:
  6.   HTTP Status:
  7.     200 OK if the user is authorized and the Cash Card was successfully retrieved
  8.     401 UNAUTHORIZED
  9. if the user is unauthenticated or unauthorized
  10.     404 NOT FOUND if the user is authenticated and authorized but the Cash Card cannot be found
  11.   Response Body Type: JSON
  12.   Example Response Body:
  13.     {
  14.       "id": 99,
  15.       "amount": 123.45
  16.     }
复制代码
为什么 API 协定很重要?
API 协定很重要,因为它们传达 REST API 的行为。它们提供有关正在交换的每个下令和参数的序列化 (或反序列化) 数据的特定详细信息。API 协定的编写方式可以很容易地转换为 API 提供者和使用者功能,以及相应的自动化测试。我们将在实验室中实现 API 提供程序功能和自动化测试。
什么是 JSON?

JSON(Javascript 对象表现法)提供了一种数据交换格式,它以易于阅读和明白的格式表现对象的特定信息。我们将使用 JSON 作为 Family Cash Card API 的数据交换格式。
这是我们上面使用的示例:
  1. {
  2.   "id": 99,
  3.   "amount": 123.45
  4. }
复制代码
其他流行的数据格式包括 YAML (Yet Another Markup Language) 和 XML (Extensible Markup Language)。与 XML 相比,JSON 的读取和写入速度更快,更易于使用,占用的空间更少。您可以将 JSON 与大多数现代编程语言和所有重要平台一起使用。它还可以与基于 Javascript 的应用程序无缝协作。
由于这些缘故原由,JSON 在很大水平上取代了 XML,成为 Web 应用程序(包括 REST API)使用的 API 使用最广泛的格式。
3.先测试

什么是测试驱动开发?

软件开发团队通常会编写自动化测试套件来防止回归。通常,这些测试是在编写应用程序功能代码之后编写的。我们将采用另一种方法:在实现应用程序代码之前编写测试。这称为测试驱动开发 (TDD)。
为什么要应用 TDD?通过在实现所需功能之前断言预期行为,我们根据我们希望它做什么来设计体系,而不是体系已经做什么。
“测试驱动”应用程序代码的另一个好处是,测试会引导您编写满足实现所需的最少代码。当测试通过时,您将拥有一个有效的实现 (应用程序代码),并防止将来引入错误 (测试)。
测试金字塔

可以在体系的不同级别编写不同的测试。在每个级别上,实行速度、维护测试的 “成本” 以及它为体系正确性带来的信心之间都存在平衡。此条理布局通常表现为 “测试金字塔”。

**单元测试:**单元测试实行体系的一个小“单元”,该单元与体系的其余部分隔离。它们应该简朴快捷。您希望在测试金字塔中具有高比例的单元测试,因为它们是设计高度内聚、松散耦合软件的关键。
**集成测试:**集成测试实行体系的子集,并可能在一个测试中实行单元组。它们的编写和维护更复杂,并且运行速度比单元测试慢。
**端到端测试:**端到端测试使用与用户雷同的界面(如 Web 浏览器)来实行体系作。虽然端到端测试非常彻底,但可能非常痴钝和脆弱,因为它们在可能复杂的 UI 中使用模拟的用户交互。实施最少数目的这些测试。
Red, Green, Refactor 循环

软件开发团队喜高兴速举措。那么,怎样永远走得快呢?通过不断改进和简化您的代码 - 重构。您可以安全地重构的唯一方法之一是拥有可信的测试套件。因此,重构您当前关注的代码的最佳时间是在 TDD 周期内。这称为 Red, Green, Refactor 开发循环:

  • **红:**为所需的功能编写失败的测试。
  • **绿:**实现可以使测试通过的最简朴方法。
  • **重构:**寻找机会来简化、减少重复或以其他方式改进代码,而无需更改任何行为 - 重构。
  • 重复!
创建测试例子
src/test/java/example/cashcard directory.

  • Create the test class CashCardJsonTest.
失败用例
  1. package example.cashcard;
  2. import org.junit.jupiter.api.Test;
  3. import static org.assertj.core.api.Assertions.assertThat;
  4. class CashCardJsonTest {
  5.    @Test
  6.    void myFirstTest() {
  7.       assertThat(1).isEqualTo(42);
  8.    }
  9. }
复制代码
  1. [~/exercises] $ ./gradlew test
复制代码
改为成功用例
  1. assertThat(42).isEqualTo(42);
复制代码
  1. [~/exercises] $ ./gradlew test
  2. > Task :testCashCardJsonTest > myFirstTest() PASSEDCashCardApplicationTests > contextLoads() PASSEDBUILD SUCCESSFUL in 4s
复制代码
4. 实施 GET

REST、CRUD 和 HTTP

让我们从 REST 的扼要定义开始:Representational State Transfer。在 RESTful 体系中,数据对象称为 资源表现。RESTful API(应用程序编程接口)的目的是管理这些资源的状态。
换句话说,你可以把 “state” 看作是 “value” 和 “Resource Representation” 是 “object” 或 “thing”。因此,REST 只是一种管理事物代价的方法。这些内容可以通过 API 访问,并且通常存储在持久性数据存储(如数据库)中。

在评论 REST 时,一个常常被提及的概念是 CRUD。CRUD 代表“创建、读取、更新和删除”。这是可以对数据存储中的对象实行的四个根本作。我们将了解 REST 有实现每个 REST 的特定准则。
与 REST 相干的另一个常见概念是超文本传输协议。在 HTTP 中,调用方向 URI 发送 Request。Web 服务器接收哀求,并将其路由到哀求处理程序。处理程序创建一个 Response,然后将其发送回给调用方。
哀求和相应的组件是:
哀求


  • 方法(也称为 Verb)
  • URI(也称为 Endpoint)
  • 哀求体
相应


  • 状态代码
  • 相应体
如果您想更深入地了解 Request 和 Response 方法,请查看 HTTP 标准
REST 的强大之处在于它引用 Resource 的方式,以及每个 CRUD作的 Request 和 Response 是什么样子的。让我们看一下完成本课程后 API 会是什么样子:


  • 对于 CREATE:使用 HTTP 方法 POST。
  • 对于 READ:使用 HTTP 方法 GET。
  • 对于 UPDATE:使用 HTTP 方法 PUT。
  • 对于 DELETE:使用 HTTP 方法 DELETE。
Cash Card 对象的终端节点 URI 以关键字开头。、 和作 要求我们提供目的资源的唯一标识符。应用程序需要此唯一标识符才气对正确的资源实行正确的作。例如,到 、 或标识符为 “42” 的 Cash Card,终端节点将为 ./cashcards/42
请注意,我们没有为作提供唯一标识符。正如我们将在以后的课程中更详细地学习的那样,将产生使用新唯一 ID 创建新的 Cash Card 的副作用。创建新的 Cash Card 时不应提供标识符,因为应用程序将为我们创建一个新的唯一标识符。
下表包含有关 RESTful CRUD作的更多详细信息。
操作API 终端节点HTTP 方法相应状态创造/cashcardsPOST201 (已创建)读/cashcards/{id}GET200 (确定)更新/cashcards/{id}PUT204 (无内容)删除/cashcards/{id}DELETE204 (无内容) 哀求正文

当按照 REST 约定创建或更新资源时,我们需要将数据提交到 API。这通常称为哀求正文。和作要求哀求正文包含正确创建或更新资源所需的数据。例如,新的 Cash Card 可能具有期初现金值金额,并且作可能会更改该金额。
现金卡示例

让我们以 Read 终端节点为例。对于 Read作,URI(端点)路径为 ,其中更换为现实的 Cash Card 标识符,不带大括号,HTTP 方法是 ./cashcards/{id}
在 requests 中,正文为空。因此,读取 ID 为 123 的 Cash Card 的哀求将是:
  1. Request:
  2.   Method: GET
  3.   URL: http://cashcard.example.com/cashcards/123
  4.   Body: (empty)
复制代码
对成功读取哀求的相应具有一个正文,其中包含所哀求资源的 JSON 表现形式,相应状态代码为 200 (OK)。因此,对上述 Read 哀求的相应将如下所示:
  1. Response:
  2.   Status Code: 200
  3.   Body:
  4.   {
  5.     "id": 123,
  6.     "amount": 25.00
  7.   }
复制代码
随着我们学习本课程的进度,您还将学习怎样实施所有剩余的 CRUD作。
Spring Boot 中的 REST

现在我们已经大抵讨论了 REST,让我们看看我们将用于实现 REST 的 Spring Boot 部分。让我们从讨论 Spring 的 IoC 容器开始。
Spring 注释和组件扫描

Spring 所做的重要工作之一是设置和实例化对象。这些对象称为 Spring Beans,通常由 Spring 创建(而不是使用 Java 关键字)。你可以通过多种方式指示 Spring 创建 Bean。
在本课中,您将使用 Spring Annotation 注释一个类,它指示 Spring 在 Spring 的 Component Scan 阶段创建该类的实例。这发生在应用程序启动时。Bean 存储在 Spring 的 IoC 容器中。从这里,可以将 bean 注入到哀求它的任何代码中。
Spring Web 控制器

在 Spring Web 中,哀求由 Controller 处理。在本课中,您将使用更具体的 :
  1. @RestController
  2. class CashCardController {
  3. }
复制代码
这就是告诉 Spring “创建一个 REST 控制器”所需要的一切。Controller 被注入到 Spring Web 中,Spring Web 将 API 哀求(由 Controller 处理)路由到正确的方法。

可以将 Controller 方法指定为处理程序方法,当收到该方法知道怎样处理的哀求(称为“匹配哀求”)时调用该方法。让我们编写一个 Read 哀求处理程序方法!这是一个开始:
  1. private CashCard findById(Long requestedId) {
  2. }
复制代码
由于 REST 表现读取端点应该使用 HTTP 方法,因此您需要告诉 Spring 仅在哀求时将哀求路由到该方法。你可以使用 annotation,它需要 URI 路径:
  1. @GetMapping("/cashcards/{requestedId}")private CashCard findById(Long requestedId) {
  2. }
复制代码
Spring 需要知道怎样获取参数的值。这是使用 annotation 完成的。参数名称与参数中的文本匹配这一事实允许 Spring 为变量分配(注入)正确的值:
  1. @GetMapping("/cashcards/{requestedId}")
  2. private CashCard findById(@PathVariable Long requestedId) {
  3. }
复制代码
REST 表现 Response 的正文中需要包含 Cash Card,并且 Response 代码为 200 (OK)。Spring Web 为此提供了该类。它还提供了多种实用程序方法来生成相应实体。在这里,您可以用于创建代码为 200 (OK) 的正文,以及包含 .终极实现如下所示:
写个测试例子
  1. package example.cashcard;
  2. import com.jayway.jsonpath.DocumentContext;
  3. import com.jayway.jsonpath.JsonPath;
  4. import org.junit.jupiter.api.Test;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.boot.test.context.SpringBootTest;
  7. import org.springframework.boot.test.web.client.TestRestTemplate;
  8. import org.springframework.http.HttpStatus;
  9. import org.springframework.http.ResponseEntity;
  10. import static org.assertj.core.api.Assertions.assertThat;
  11. @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  12. class CashCardApplicationTests {
  13.         @Autowired
  14.         TestRestTemplate restTemplate;
  15.         @Test
  16.         void contextLoads() {
  17.         }
  18.         @Test
  19.         void shouldReturnACashCardWhenDataIsSaved() {
  20.         ResponseEntity<String> response = restTemplate.getForEntity("/cashcards/99", String.class);
  21.         assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
  22.                 DocumentContext documentContext = JsonPath.parse(response.getBody());
  23. Number id = documentContext.read("$.id");
  24. // assertThat(id).isNotNull();
  25. assertThat(id).isEqualTo(99);
  26. Double amount = documentContext.read("$.amount");
  27. assertThat(amount).isEqualTo(123.45);
  28.     }
  29. }
复制代码
测试
  1. [~/exercises] $ ./gradlew test
复制代码
创建controller
  1. package example.cashcard;
  2. import org.springframework.http.ResponseEntity;
  3. import org.springframework.web.bind.annotation.GetMapping;
  4. import org.springframework.web.bind.annotation.PathVariable;
  5. import org.springframework.web.bind.annotation.RequestMapping;
  6. import org.springframework.web.bind.annotation.RestController;
  7. @RestController
  8. @RequestMapping("/cashcards")
  9. public class CashCardController {
  10.     @GetMapping("/{requestedId}")
  11.     private ResponseEntity<CashCard> findById() {
  12.         CashCard cashCard = new CashCard(99L, 123.45);
  13.         return ResponseEntity.ok(cashCard);
  14.      }
  15. }
复制代码
5. 存储库和Spring Data

在我们开发过程的这一点上,我们有一个体系,它从我们的 Controller 返回硬编码的 Cash Card 记载。然而,我们真正想要的是 从数据库返回真实数据。那么,让我们把注意力转移到数据库上,继续我们的 Steel Thread!
Spring Data 与 Spring Boot 配合使用,使数据库集成变得简朴。在我们开始之前,让我们简朴谈谈 Spring Data 的架构。
Controller-Repository 架构

关注点分离原则指出,设计良好的软件应该是模块化的,每个模块都有与任何其他模块不同的关注点。
到现在为止,我们的代码库只返返来自 Controller 的硬编码相应。这种设置违反了关注点分离原则,因为它混合了 Controller 的关注点(Web 界面的抽象)与将数据读写到数据存储(例如数据库)的关注点。为了解决这个题目,我们将使用一个通用的软件架构模式,通过 Repository 模式来强制进行数据管理分离。
通常按功能或值(如业务层、数据层和表现层)划分这些层的常见体系布局框架称为分层体系布局。在这方面,我们可以将 Repository 和 Controller 视为 Layered Architecture 中的两层。Controller 位于靠近客户端的层中(当它接收和相应 Web 哀求时),而 Repository 位于靠近数据存储的层中(当它读取和写入数据存储时)。也可能有中间层,具体取决于业务需求。我们不需要任何额外的层,至少现在不需要!
Repository 是应用程序和数据库之间的接口,为任何数据库提供通用抽象,从而在需要时更轻松地切换到其他数据库。

好消息是, Spring Data 提供了一系列强大的数据管理工具,包括 Repository 模式的实现。
选择数据库

对于数据库选择,我们将使用嵌入式内存数据库。“Embedded” 仅表现它是一个 Java 库,因此可以像任何其他依赖项一样将其添加到项目中。“内存中”意味着它仅将数据存储在内存中,而不是将数据持久生存在永世、持久的存储中。同时,我们的内存数据库在很大水平上与 MySQL、SQL Server 等生产级关系数据库管理体系 (RDBMS) 兼容。具体来说,它使用 JDBC(用于数据库毗连的标准 Java 库)和 SQL(标准数据库查询语言)。

使用内存数据库而不是持久性数据库需要权衡。一方面,in-memory 允许您在不安装单独的 RDBMS 的环境下进行开发,并确保数据库在每次测试运行时都处于雷同的状态(即空)。但是,您确实需要一个用于实时 “生产” 应用程序的持久数据库。这会导致 Dev-Prod Parity** 不匹配:您的应用程序在运行内存数据库时的行为可能与在生产环境中运行时的行为不同。
我们将使用的特定内存数据库是 H2。幸运的是,H2 与其他关系数据库高度兼容,因此 dev-prod 奇偶校验不会是一个大题目。为了方便当地开发,我们将使用 H2,但我们希望认识到权衡。
自动设置

在实验室中,要实现完整的数据库功能,我们只需添加两个依赖项即可。这精彩地展示了 Spring Boot 最强大的功能之一:自动设置。如果没有 Spring Boot,我们将不得不设置 Spring Data 才气与 H2 通信。但是,由于我们包含了 Spring Data 依赖项(以及特定的数据提供程序 H2),因此 Spring Boot 将自动设置您的应用程序以与 H2 通信。
Spring Data 的 CrudRepository

对于我们的 Repository 选择,我们将使用特定范例的 Repository: Spring Data 的 .乍一看,这有点神奇,但让我们来解开这种魔力。
以下是所有 CRUD作的完整实现,方法是 extend :CrudRepository
  1. interface CashCardRepository extends CrudRepository<CashCard, Long> {
  2. }
复制代码
只需使用上述代码,调用方就可以调用恣意数目的预定义方法,例如:CrudRepository findById
  1. cashCard = cashCardRepository.findById(99);
复制代码
您可能会立刻想知道:该方法的实现在哪里? 它继续的一切都是一个没有现实代码的 Interface!好吧,基于使用的特定 Spring Data 框架(对我们来说将是 Spring Data JDBC),Spring Data 在 IoC 容器启动期间为我们处理此实现。然后,Spring 运行时会将存储库公开为另一个 bean,您可以在应用程序中的任何需要的地方引用该 bean。CashCardRepository.findById() CrudRepository
正如我们所了解的,通常需要权衡弃取。例如,它会生成 SQL 语句来读取和写入数据,这在很多环境下都很有用,但有时您需要为特定使用案例编写自己的自定义 SQL 语句。现在,我们很高兴利用其方便、开箱即用的方法,所以让我们继续练习。CrudRepository
添加Spring数据依赖项

In build.gradle:
Editor: Select text in file “~/exercises/build.gradle”
  1. dependencies {
  2.    implementation 'org.springframework.boot:spring-boot-starter-web'
  3.    testImplementation 'org.springframework.boot:spring-boot-starter-test'
  4.    // Add the two dependencies below
  5.    implementation 'org.springframework.data:spring-data-jdbc'
  6.    implementation 'com.h2database:h2'
  7. }
复制代码
2025-05-06T03:42:47.459Z INFO 3540 — [ionShutdownHook] o.s.j.d.e.EmbeddedDatabaseFactory : Shutting down embedded database: url=‘jdbc:h2:mem:23a83549-637b-4181-82d2-a1422cd0532e;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false’
创建CrudRepository
  1. package example.cashcard;
  2. import org.springframework.data.repository.CrudRepository;
  3. interface CashCardRepository extends CrudRepository {
  4. }
复制代码
实行报错:
  1. [~/exercises] $ ./gradlew test
  2. ...CashCardApplicationTests > shouldNotReturnACashCardWithAnUnknownId() FAILED java.lang.IllegalStateException: Failed to load ApplicationContext for ...Caused by:java.lang.IllegalArgumentException: Could not resolve domain type of interface example.cashcard.CashCardRepository...
复制代码
修改CrudRepository指定实体类,id
  1. interface CashCardRepository extends CrudRepository<CashCard, Long> {
  2. }
复制代码
修改cashcard,增加@Id
  1. package example.cashcard;
  2. // Add this import
  3. import org.springframework.data.annotation.Id;
  4. record CashCard(@Id Long id, Double amount) {
  5. }
复制代码
修改CashCardController注入CashCardRepository
  1. private final CashCardRepository cashCardRepository;
  2.    private CashCardController(CashCardRepository cashCardRepository) {
  3.       this.cashCardRepository = cashCardRepository;
  4.    }
复制代码
实行test
  1. [~/exercises] $ ./gradlew test
  2. ...BUILD SUCCESSFUL in 4s
复制代码
修改CashCardController的findById方法
  1. import java.util.Optional;
  2. @GetMapping("/{requestedId}")
  3.     private ResponseEntity<CashCard> findById(@PathVariable Long requestedId) {
  4.         Optional<CashCard> cashCardOptional = cashCardRepository.findById(requestedId);
  5.         if (cashCardOptional.isPresent()) {
  6.             return ResponseEntity.ok(cashCardOptional.get());
  7.         } else {
  8.             return ResponseEntity.notFound().build();
  9.         }
  10.     }
复制代码
实行test
  1. CashCardApplicationTests > shouldReturnACashCardWhenDataIsSaved() FAILED
  2.    org.opentest4j.AssertionFailedError:
  3.    expected: 200 OK
  4.    but was: 500 INTERNAL_SERVER_ERROR
复制代码
修改 build.gradle设置输出更多信息
  1. // Change from false to true
  2.      showStandardStreams = true
复制代码
输出:
  1. org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "CASH_CARD" not found (this database is empty); SQL statement:
  2. SELECT "CASH_CARD"."ID" AS "ID", "CASH_CARD"."AMOUNT" AS "AMOUNT" FROM "CASH_CARD" WHERE "CASH_CARD"."ID" = ? [42104-214]
  3. The cause of our test failures is clear: Table "CASH_CARD" not found means we don't have a database nor any data.
复制代码
创建数据库表。schema.sql注释打开
实行test
  1. CashCardApplicationTests > shouldReturnACashCardWhenDataIsSaved() FAILED
  2. org.opentest4j.AssertionFailedError:
  3. expected: 200 OK
  4.   but was: 404 NOT_FOUND
复制代码
添加数据
  1. # 创建src/test/resources/data.sql,添加数据
  2. INSERT INTO CASH_CARD(ID, AMOUNT) VALUES (99, 123.45);
复制代码
测试成功
6. 简朴的 Spring Security

让我们将注意力转回到 Steel Thread,专注于架构的另一个组件:安全性。
什么是安全性?

软件安全可能意味着很多事情。该范畴是一个巨大的话题,值得有自己的课程。在本课中,我们将讨论 Web 安全性。更具体地说,我们将介绍 HTTP 身份验证和授权的工作原理、Web 生态体系容易受到攻击的常见方式,以及我们怎样使用 Spring Security 来防止未经授权访问我们的家庭现金卡服务。
认证

API 的用户现实上是一个人或其他程序,因此我们常常使用术语 Principal 作为“user”的同义词。身份验证是 Principal 向体系证实其身份的行为。一种方法是提供凭据(例如,使用根本身份验证的用户名和密码)。我们说,一旦提供了正确的凭证,Principal 就通过了身份验证,大概换句话说,用户已成功登录。
HTTP 是一种无状态协议,因此每个哀求都必须包含证实它来自经过身份验证的 Principal 的数据。只管可以在每个哀求上提供凭证,但这样做效率低下,因为它需要在服务器上进行更多处理。相反,在用户进行身份验证时会创建一个身份验证会话(或身份验证会话,或简称为会话)。会话可以通过多种方式实现。我们将使用一种通用机制:生成并放置在 Cookie 中的 Session Token(一串随机字符)。Cookie 是存储在 Web 客户端(例如浏览器)中的一组数据,并与特定 URI 相干联。
关于 Cookie 的几个优点:


  • Cookie 会随每个哀求自动发送到服务器(无需编写额外的代码即可实现)。只要服务器查抄 Cookie 中的 Token 是否有效,就可以拒绝未经身份验证的哀求。
  • Cookie 可以保留一段时间,纵然网页已关闭并随后重新访问。此功能通常会改善 Web 站点的用户体验。
Spring 安全性和身份验证

Spring SecurityFilter Chain 中实现身份验证。Filter Chain 是 Java Web 体系布局的一个组件,它允许程序员定义在 Controller 之前调用的一系列方法。链中的每个过滤器都决定是否允许哀求处理继续。Spring Security 插入一个过滤器,该过滤器查抄用户的身份验证,如果哀求未通过身份验证,则返回相应。
  1. 401 UNAUTHORIZED
复制代码
授权

到现在为止,我们已经讨论了身份验证。但现实上,身份验证只是第一步。授权发生在身份验证之后,并允许同一体系的不同用户具有不同的权限。
Spring Security 通过基于角色的访问控制 (RBAC) 提供授权。这意味着 Principal 具有很多 Role。每个资源(或作)都指定 Principal 必须具有哪些 Role 才气在获得适当授权的环境下实行作。例如,具有 Administrator Role 的用户可能比具有 Card Owner Role 的用户被授权实行更多的作。您可以在全局级别和按方法设置基于角色的授权。
同源策略

Web 是一个危险的地方,不良行为者不断试图利用安全毛病。最根本的保护机制依赖于实施同源策略 (SOP) 的 HTTP 客户端和服务器。此策略规定,仅允许网页中包含的脚本向网页的泉源 (URI) 发送哀求。
SOP 对网站的安全性至关重要,因为如果没有该策略,任何人都可以编写包含脚本的网页,该脚本将哀求发送到任何其他站点。例如,让我们看一个典型的银行网站。如果用户登录其银行账户并访问恶意网页(在不同的浏览器选项卡或窗口中),则恶意哀求可能会(使用 Auth Cookie)发送到银行网站。这可能会导致不需要的作——比如从用户的银行账户提款!
跨域资源共享

有时,一个体系由运行在多台具有不同 URI 的计算机(即微服务)上的服务组成。跨域资源共享 (CORS) 是浏览器和服务器可以合作放宽 SOP 的一种方式。服务器可以明白允许来自服务器外部源的哀求的 “允许的泉源” 列表。
Spring Security 提供了 Comments,允许您指定允许的站点列表。警惕!如果您使用不带任何参数的注释,它将允许所有泉源,因此请记住这一点!
  1. @CrossOrigin
复制代码
常见的 Web 毛病

除了利用已知的安全毛病外,Web 上的恶意行为者还不断发现新的毛病。值得庆幸的是,Spring Security 提供了一个强大的工具集来防范常见的安全毛病。让我们讨论两种常见的毛病,它们的工作原理以及 Spring Security 怎样帮助缓解它们。
跨站点哀求伪造

一种范例的毛病是跨站点哀求伪造 (CSRF),通常发音为“Sea-Surf”,也称为会话骑行。Session Riding 现实上是由 Cookie 启用的。当恶意代码向用户进行身份验证的服务器发送哀求时,就会发生 CSRF 攻击。当服务器收到身份验证 Cookie 时,它无法知道受害者是否偶然中发送了有害哀求。
为了防止 CSRF 攻击,您可以使用 CSRF Token。CSRF 令牌与身份验证令牌不同,因为每个哀求都会生成唯一的令牌。这使得外部参与者更难将自己插入客户端和服务器之间的 “对话” 中。
值得庆幸的是, Spring Security 内置了对 CSRF 令牌的支持,默认环境下是启用的。您将在即将到来的实验中了解更多信息。
跨站点脚本

大概比 CSRF 毛病更危险的是**跨站脚本 (XSS)。**当攻击者能够以某种方式“诱骗”受害者应用程序实行恣意代码时,就会发生这种环境。有很多方法可以做到这一点。一个简朴的示例是将字符串生存在包含标签的数据库中,然后等候该字符串呈现在网页上,从而实行脚本
XSS 可能比 CSRF 更危险。在 CSRF 中,只能实行用户有权实行的作。但是,在 XSS 中,恣意恶意代码**会在客户端或服务器上实行。别的,XSS 攻击不依赖于 Authentication。相反,XSS 攻击依赖于由不良编程实践引起的安全“毛病”。
防范 XSS 攻击的重要方法是正确处理来自外部泉源(如 Web 表单和 URI 查询字符串)的所有数据。在我们的标签示例中,可以通过在呈现字符串时正确转义特殊 HTML 字符来缓解攻击。
添加Spring安全依赖项

build.gradle中添加依赖
  1. dependencies {
  2.     implementation 'org.springframework.boot:spring-boot-starter-web'
  3.     // Add the following dependency
  4.     implementation 'org.springframework.boot:spring-boot-starter-security'
  5.     ...
复制代码
添加设置文件SecurityConfig
  1. package example.cashcard;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.config.Customizer;
  5. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  6. import org.springframework.security.core.userdetails.User;
  7. import org.springframework.security.core.userdetails.UserDetails;
  8. import org.springframework.security.core.userdetails.UserDetailsService;
  9. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  10. import org.springframework.security.crypto.password.PasswordEncoder;
  11. import org.springframework.security.provisioning.InMemoryUserDetailsManager;
  12. import org.springframework.security.web.SecurityFilterChain;
  13. // Add this Annotation
  14. @Configuration
  15. class SecurityConfig {
  16. // Add this Annotation
  17. @Bean
  18.    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  19.        return http.build();
  20.    }
  21.    @Bean
  22.    PasswordEncoder passwordEncoder() {
  23.        return new BCryptPasswordEncoder();
  24.    }
  25. }
复制代码
test测试
  1. [~/exercises] $ ./gradlew test
  2. ...CashCardApplicationTests > shouldCreateANewCashCard() FAILED    org.opentest4j.AssertionFailedError:    expected: 201 CREATED     but was: 403 FORBIDDEN...11 tests completed, 1 failed
复制代码
修改SecurityConfig.filterChain
  1. @Bean
  2. SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  3.      http
  4.              .authorizeHttpRequests(request -> request
  5.                      .requestMatchers("/cashcards/**")
  6.                      .authenticated())
  7.              .httpBasic(Customizer.withDefaults())
  8.              .csrf(csrf -> csrf.disable());
  9.      return http.build();
  10. }
复制代码
test测试
  1. [~/exercises] $ ./gradlew test
  2. ...expected: 200 OK  but was: 401 UNAUTHORIZED
复制代码
修改 src/test/resources/data.sql
  1. INSERT INTO CASH_CARD(ID, AMOUNT, OWNER) VALUES (100, 1.00, 'sarah1');
复制代码
添加SecurityConfig中bean
  1. @Bean
  2.   UserDetailsService testOnlyUsers(PasswordEncoder passwordEncoder) {
  3.    User.UserBuilder users = User.builder();
  4.    UserDetails sarah = users
  5.      .username("sarah1")
  6.      .password(passwordEncoder.encode("abc123"))
  7.      .roles() // No roles for now
  8.      .build();
  9.    return new InMemoryUserDetailsManager(sarah);
  10.   }
复制代码
测试权限通过的环境
  1. void shouldReturnACashCardWhenDataIsSaved() {
  2.     ResponseEntity<String> response = restTemplate
  3.             .withBasicAuth("sarah1", "abc123") // Add this
  4.             .getForEntity("/cashcards/99", String.class);
  5.     assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
  6.     ...
复制代码
测试权限不通过的环境
  1.     @Test
  2. void shouldNotReturnACashCardWhenUsingBadCredentials() {
  3.     ResponseEntity<String> response = restTemplate
  4.       .withBasicAuth("BAD-USER", "abc123")
  5.       .getForEntity("/cashcards/99", String.class);
  6.     assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
  7.     response = restTemplate
  8.       .withBasicAuth("sarah1", "BAD-PASSWORD")
  9.       .getForEntity("/cashcards/99", String.class);
  10.     assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
  11. }
复制代码
添加角色测试
  1. ...
  2. @Bean
  3. UserDetailsService testOnlyUsers(PasswordEncoder passwordEncoder) {
  4.   User.UserBuilder users = User.builder();
  5.   UserDetails sarah = users
  6.     .username("sarah1")
  7.     .password(passwordEncoder.encode("abc123"))
  8.     .roles("CARD-OWNER") // new role
  9.     .build();
  10.   UserDetails hankOwnsNoCards = users
  11.     .username("hank-owns-no-cards")
  12.     .password(passwordEncoder.encode("qrs456"))
  13.     .roles("NON-OWNER") // new role
  14.     .build();
  15.   return new InMemoryUserDetailsManager(sarah, hankOwnsNoCards);
  16. }
复制代码
测试角色
  1. .withBasicAuth("hank-owns-no-cards", "qrs456")
复制代码
  1. [~/exercises] $ ./gradlew test
  2. ...CashCardApplicationTests > shouldRejectUsersWhoAreNotCardOwners() FAILED org.opentest4j.AssertionFailedError: expected: 403 FORBIDDEN  but was: 200 OK
复制代码
修改权限设置类
  1. @Bean
  2. SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  3.      http
  4.              .authorizeHttpRequests(request -> request
  5.                      .requestMatchers("/cashcards/**")
  6.                      .hasRole("CARD-OWNER")) // enable RBAC: Replace the .authenticated() call with the hasRole(...) call.
  7.              .httpBasic(Customizer.withDefaults())
  8.              .csrf(csrf -> csrf.disable());
  9.      return http.build();
  10. }
复制代码
继续测试,被拒绝访问,验证成功
  1. @Testvoid shouldRejectUsersWhoAreNotCardOwners() {    ResponseEntity<String> response = restTemplate      .withBasicAuth("hank-owns-no-cards", "qrs456")
  2.       .getForEntity("/cashcards/99", String.class);    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);}
复制代码
更新src/test/resources/data.sql
  1. ...
  2. INSERT INTO CASH_CARD(ID, AMOUNT, OWNER) VALUES (102, 200.00, 'kumar2');
复制代码
test测试
  1. @Test
  2. void shouldNotAllowAccessToCashCardsTheyDoNotOwn() {
  3.     ResponseEntity<String> response = restTemplate
  4.       .withBasicAuth("sarah1", "abc123")
  5.       .getForEntity("/cashcards/102", String.class); // kumar2's data
  6.     assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
  7. }
复制代码
POST、PUT、PATCH 和 CRUD作 - 总结

上述部分可以使用下表进行总结:
HTTP 方法操作资源 URI 的定义它有什么作用?相应状态代码相应正文POST创造Server 生成并返回 URI创建子资源(“在”或“内”通报的 URI 中”)201 CREATED创建的资源PUT创造客户端提供 URI创建资源(在哀求 URI 处)201 CREATED创建的资源PUT更新客户端提供 URI更换资源:整个记载被 Request 中的对象更换204 NO CONTENT(空)PATCH更新客户端提供 URIPartial Update:仅修改现有记载的哀求中包含的字段200 OK更新的资源
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

冬雨财经

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表