当前位置: 首页 > news >正文

使用 Spring Boot 构建 REST API

使用 Spring Boot 构建 REST API

    • 使用 Spring Boot 构建 REST API
    • 1. Spring Initializr构建springboot
    • 2. API 合同 & JSON
      • API 协定
      • 什么是 JSON?
    • 3.先测试
      • 什么是测试驱动开发?
      • 测试金字塔
      • Red, Green, Refactor 循环
    • 4. 实施 GET
      • REST、CRUD 和 HTTP
        • 请求正文
        • 现金卡示例
      • Spring Boot 中的 REST
        • Spring 注释和组件扫描
        • Spring Web 控制器
    • 5. 存储库和Spring Data
      • Controller-Repository 架构
      • 选择数据库
      • 自动配置
      • Spring Data 的 CrudRepository
      • 添加Spring数据依赖项
    • 6. 简单的 Spring Security
      • 什么是安全性?
      • 认证
      • Spring 安全性和身份验证
      • 授权
      • 同源策略
        • 跨域资源共享
      • 常见的 Web 漏洞
        • 跨站点请求伪造
        • 跨站点脚本
      • 添加Spring安全依赖项
      • POST、PUT、PATCH 和 CRUD作 - 总结

官网学习地址:结论 - 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
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'
[~] $ cd cashcard
[~/cashcard] $

Next, run the ./gradlew build command:

[~/cashcard] $ ./gradlew build

The output shows that the application passed the tests and was successfully built.

[~/cashcard] $ ./gradlew build
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 39s
7 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 协定的示例。如果您不了解整个合同,请不要担心。在您完成本课程时,我们将介绍以下信息的各个方面。

RequestURI: /cashcards/{id}HTTP Verb: GETBody: NoneResponse:HTTP Status:200 OK if the user is authorized and the Cash Card was successfully retrieved401 UNAUTHORIZED if the user is unauthenticated or unauthorized404 NOT FOUND if the user is authenticated and authorized but the Cash Card cannot be foundResponse Body Type: JSONExample Response Body:{"id": 99,"amount": 123.45}

为什么 API 协定很重要?

API 协定很重要,因为它们传达 REST API 的行为。它们提供有关正在交换的每个命令和参数的序列化 (或反序列化) 数据的特定详细信息。API 协定的编写方式可以很容易地转换为 API 提供者和使用者功能,以及相应的自动化测试。我们将在实验室中实现 API 提供程序功能和自动化测试。

什么是 JSON?

JSON(Javascript 对象表示法)提供了一种数据交换格式,它以易于阅读和理解的格式表示对象的特定信息。我们将使用 JSON 作为 Family Cash Card API 的数据交换格式。

这是我们上面使用的示例:

{"id": 99,"amount": 123.45
}

其他流行的数据格式包括 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 开发循环:

  1. **红:**为所需的功能编写失败的测试。
  2. **绿:**实现可以使测试通过的最简单方法。
  3. **重构:**寻找机会来简化、减少重复或以其他方式改进代码,而无需更改任何行为 - 重构。
  4. 重复!

创建测试例子

src/test/java/example/cashcard directory.

  1. Create the test class CashCardJsonTest.

失败用例

package example.cashcard;import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;class CashCardJsonTest {@Testvoid myFirstTest() {assertThat(1).isEqualTo(42);}
}
[~/exercises] $ ./gradlew test

改为成功用例

assertThat(42).isEqualTo(42);
[~/exercises] $ ./gradlew test> 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 data flow using HTTP

在谈论 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 的请求将是:

Request:Method: GETURL: http://cashcard.example.com/cashcards/123Body: (empty)

对成功读取请求的响应具有一个正文,其中包含所请求资源的 JSON 表示形式,响应状态代码为 200 (OK)。因此,对上述 Read 请求的响应将如下所示:

Response:Status Code: 200Body:{"id": 123,"amount": 25.00}

随着我们学习本课程的进度,您还将学习如何实施所有剩余的 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 处理。在本课中,您将使用更具体的 :

@RestController
class CashCardController {
}

这就是告诉 Spring “创建一个 REST 控制器”所需要的一切。Controller 被注入到 Spring Web 中,Spring Web 将 API 请求(由 Controller 处理)路由到正确的方法。

Spring Web Controller image

可以将 Controller 方法指定为处理程序方法,当收到该方法知道如何处理的请求(称为“匹配请求”)时调用该方法。让我们编写一个 Read 请求处理程序方法!这是一个开始:

private CashCard findById(Long requestedId) {
}

由于 REST 表示读取端点应该使用 HTTP 方法,因此您需要告诉 Spring 仅在请求时将请求路由到该方法。你可以使用 annotation,它需要 URI 路径:

@GetMapping("/cashcards/{requestedId}")
private CashCard findById(Long requestedId) {
}

Spring 需要知道如何获取参数的值。这是使用 annotation 完成的。参数名称与参数中的文本匹配这一事实允许 Spring 为变量分配(注入)正确的值:

@GetMapping("/cashcards/{requestedId}")
private CashCard findById(@PathVariable Long requestedId) {
}

REST 表示 Response 的正文中需要包含 Cash Card,并且 Response 代码为 200 (OK)。Spring Web 为此提供了该类。它还提供了多种实用程序方法来生成响应实体。在这里,您可以用于创建代码为 200 (OK) 的正文,以及包含 .最终实现如下所示:

写个测试例子

package example.cashcard;import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;import static org.assertj.core.api.Assertions.assertThat;@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CashCardApplicationTests {@AutowiredTestRestTemplate restTemplate;@Testvoid contextLoads() {}@Testvoid shouldReturnACashCardWhenDataIsSaved() {ResponseEntity<String> response = restTemplate.getForEntity("/cashcards/99", String.class);assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);DocumentContext documentContext = JsonPath.parse(response.getBody());
Number id = documentContext.read("$.id");
// assertThat(id).isNotNull();
assertThat(id).isEqualTo(99);
Double amount = documentContext.read("$.amount");
assertThat(amount).isEqualTo(123.45);}}

测试

[~/exercises] $ ./gradlew test

创建controller

package example.cashcard;import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/cashcards")
public class CashCardController {@GetMapping("/{requestedId}")private ResponseEntity<CashCard> findById() {CashCard cashCard = new CashCard(99L, 123.45);return ResponseEntity.ok(cashCard);}
}

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 是应用程序和数据库之间的接口,为任何数据库提供通用抽象,从而在需要时更轻松地切换到其他数据库。

Controller 层和 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

interface CashCardRepository extends CrudRepository<CashCard, Long> {
}

只需使用上述代码,调用方就可以调用任意数量的预定义方法,例如:CrudRepository findById

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”

dependencies {implementation 'org.springframework.boot:spring-boot-starter-web'testImplementation 'org.springframework.boot:spring-boot-starter-test'// Add the two dependencies belowimplementation 'org.springframework.data:spring-data-jdbc'implementation 'com.h2database:h2'
}

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

package example.cashcard;import org.springframework.data.repository.CrudRepository;interface CashCardRepository extends CrudRepository {
}

执行报错:

[~/exercises] $ ./gradlew test
...
CashCardApplicationTests > shouldNotReturnACashCardWithAnUnknownId() FAILEDjava.lang.IllegalStateException: Failed to load ApplicationContext for ...Caused by:
java.lang.IllegalArgumentException: Could not resolve domain type of interface example.cashcard.CashCardRepository
...

修改CrudRepository指定实体类,id

interface CashCardRepository extends CrudRepository<CashCard, Long> {
}

修改cashcard,增加@Id

package example.cashcard;// Add this import
import org.springframework.data.annotation.Id;record CashCard(@Id Long id, Double amount) {
}

修改CashCardController注入CashCardRepository

 private final CashCardRepository cashCardRepository;private CashCardController(CashCardRepository cashCardRepository) {this.cashCardRepository = cashCardRepository;}

执行test

[~/exercises] $ ./gradlew test
...
BUILD SUCCESSFUL in 4s

修改CashCardController的findById方法

import java.util.Optional;@GetMapping("/{requestedId}")private ResponseEntity<CashCard> findById(@PathVariable Long requestedId) {Optional<CashCard> cashCardOptional = cashCardRepository.findById(requestedId);if (cashCardOptional.isPresent()) {return ResponseEntity.ok(cashCardOptional.get());} else {return ResponseEntity.notFound().build();}}

执行test

CashCardApplicationTests > shouldReturnACashCardWhenDataIsSaved() FAILEDorg.opentest4j.AssertionFailedError:expected: 200 OKbut was: 500 INTERNAL_SERVER_ERROR

修改 build.gradle配置输出更多信息

 // Change from false to trueshowStandardStreams = true

输出:

org.h2.jdbc.JdbcSQLSyntaxErrorException: Table "CASH_CARD" not found (this database is empty); SQL statement:SELECT "CASH_CARD"."ID" AS "ID", "CASH_CARD"."AMOUNT" AS "AMOUNT" FROM "CASH_CARD" WHERE "CASH_CARD"."ID" = ? [42104-214]
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

CashCardApplicationTests > shouldReturnACashCardWhenDataIsSaved() FAILEDorg.opentest4j.AssertionFailedError:expected: 200 OKbut was: 404 NOT_FOUND

添加数据

# 创建src/test/resources/data.sql,添加数据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 插入一个过滤器,该过滤器检查用户的身份验证,如果请求未通过身份验证,则返回响应。

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,允许您指定允许的站点列表。小心!如果您使用不带任何参数的注释,它将允许所有来源,因此请记住这一点!

@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中添加依赖

dependencies {implementation 'org.springframework.boot:spring-boot-starter-web'// Add the following dependencyimplementation 'org.springframework.boot:spring-boot-starter-security'...

添加配置文件SecurityConfig

package example.cashcard;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
// Add this Annotation
@Configuration
class SecurityConfig {
// Add this Annotation
@BeanSecurityFilterChain filterChain(HttpSecurity http) throws Exception {return http.build();}@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

test测试

[~/exercises] $ ./gradlew test
...
CashCardApplicationTests > shouldCreateANewCashCard() FAILEDorg.opentest4j.AssertionFailedError:expected: 201 CREATEDbut was: 403 FORBIDDEN
...
11 tests completed, 1 failed

修改SecurityConfig.filterChain

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(request -> request.requestMatchers("/cashcards/**").authenticated()).httpBasic(Customizer.withDefaults()).csrf(csrf -> csrf.disable());return http.build();
}

test测试

[~/exercises] $ ./gradlew test
...
expected: 200 OKbut was: 401 UNAUTHORIZED

修改 src/test/resources/data.sql

INSERT INTO CASH_CARD(ID, AMOUNT, OWNER) VALUES (100, 1.00, 'sarah1');

添加SecurityConfig中bean

 @BeanUserDetailsService testOnlyUsers(PasswordEncoder passwordEncoder) {User.UserBuilder users = User.builder();UserDetails sarah = users.username("sarah1").password(passwordEncoder.encode("abc123")).roles() // No roles for now.build();return new InMemoryUserDetailsManager(sarah);}

测试权限通过的情况

void shouldReturnACashCardWhenDataIsSaved() {ResponseEntity<String> response = restTemplate.withBasicAuth("sarah1", "abc123") // Add this.getForEntity("/cashcards/99", String.class);assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);...

测试权限不通过的情况

    @Test
void shouldNotReturnACashCardWhenUsingBadCredentials() {ResponseEntity<String> response = restTemplate.withBasicAuth("BAD-USER", "abc123").getForEntity("/cashcards/99", String.class);assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);response = restTemplate.withBasicAuth("sarah1", "BAD-PASSWORD").getForEntity("/cashcards/99", String.class);assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}

添加角色测试

...
@Bean
UserDetailsService testOnlyUsers(PasswordEncoder passwordEncoder) {User.UserBuilder users = User.builder();UserDetails sarah = users.username("sarah1").password(passwordEncoder.encode("abc123")).roles("CARD-OWNER") // new role.build();UserDetails hankOwnsNoCards = users.username("hank-owns-no-cards").password(passwordEncoder.encode("qrs456")).roles("NON-OWNER") // new role.build();return new InMemoryUserDetailsManager(sarah, hankOwnsNoCards);
}

测试角色

.withBasicAuth("hank-owns-no-cards", "qrs456")
[~/exercises] $ ./gradlew test
...
CashCardApplicationTests > shouldRejectUsersWhoAreNotCardOwners() FAILEDorg.opentest4j.AssertionFailedError:expected: 403 FORBIDDENbut was: 200 OK

修改权限配置类

@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(request -> request.requestMatchers("/cashcards/**").hasRole("CARD-OWNER")) // enable RBAC: Replace the .authenticated() call with the hasRole(...) call..httpBasic(Customizer.withDefaults()).csrf(csrf -> csrf.disable());return http.build();
}

继续测试,被拒绝访问,验证成功

@Test
void shouldRejectUsersWhoAreNotCardOwners() {ResponseEntity<String> response = restTemplate.withBasicAuth("hank-owns-no-cards", "qrs456").getForEntity("/cashcards/99", String.class);assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}

更新src/test/resources/data.sql

...
INSERT INTO CASH_CARD(ID, AMOUNT, OWNER) VALUES (102, 200.00, 'kumar2');

test测试

@Test
void shouldNotAllowAccessToCashCardsTheyDoNotOwn() {ResponseEntity<String> response = restTemplate.withBasicAuth("sarah1", "abc123").getForEntity("/cashcards/102", String.class); // kumar2's dataassertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}

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更新的资源
http://www.xdnf.cn/news/304723.html

相关文章:

  • SpringBoot教学管理平台源码设计开发
  • leetcode 24. 两两交换链表中的节点
  • 分库分表后复杂查询的应对之道:基于DTS实时性ES宽表构建技术实践
  • 简说Policy Gradient (1) —— 入门
  • [蓝桥杯 2025 省 B] 水质检测(暴力 )
  • python--------修改桌面文件内容
  • 第2章 神经网络的数学基础
  • 神经网络之激活函数:解锁非线性奥秘的关键
  • Linux开发工具【上】
  • 2025年LangChain(V0.3)开发与综合案例
  • 接口自动化工具如何选择?以及实战介绍
  • windows操作系统开机自启(自动启动) 运行窗口 shell:startup 指令调出开机自启文件夹
  • 驱动开发系列57 - Linux Graphics QXL显卡驱动代码分析(四)显示区域绘制
  • 使用原生javascript手动实现一个可选链运算符
  • [论文阅读]MCP Guardian: A Security-First Layer for Safeguarding MCP-Based AI System
  • 【Spring Boot 注解】@Configuration与@AutoConfiguration
  • vue2项目中使用pag格式动图
  • GMRES算法处理多个右端项的Block与PseudoBlock变体
  • 【已解决】Neo4j Desktop打不开,不断网解决
  • 一种基于条件生成对抗网络(cGAN)的CT重建算法
  • Hadoop架构再探讨
  • keil+vscode+腾讯ai助手
  • 【prometheus+Grafana篇】基于Prometheus+Grafana实现Linux操作系统的监控与可视化
  • 【程序员AI入门:基础】5.提示工程怎么释放LLM的潜力
  • WT2606B显示驱动TFT语音芯片IC:重塑电子锁交互体验的技术革新
  • 神经网络之训练的艺术:反向传播与常见问题解决之道
  • 数据库实验10 函数存储
  • Dify - Stable Diffusion
  • 《数据分析与可视化》(清华)ch-6 作业 三、绘图题
  • 解决Centos连不上网