第一部分:使用 Java 和 Spring 的后端深入教程:构建现代全栈 Web 应用程序使用 Java 和 Spring 的后端选择框架初始化 Spring 项目编程后端组织项目文件夹结构构建应用程序

2025-06-09

第一部分:使用 Java 和 Spring 的后端

深入教程:构建现代全栈 Web 应用程序

使用 Java 和 Spring 的后端

选择框架

初始化 Spring 项目

后端编程

组织项目文件夹结构

构建应用程序

深入教程:构建现代全栈 Web 应用程序

在本系列中,我希望构建一个现代、可扩展且简单的系统,以便快速构建和部署 Web 前端、后端和数据库。虽然这个项目可以作为未来项目的模板,但我们仍然需要设定一个目标。这就是为什么我们要创建一个有史以来最简单的待办事项应用。待办事项应用是一个很好的用例,因为它简单却涵盖了现代应用程序的大部分功能。我们需要:

  • 连接数据库来存储待办事项
  • 通过读取、创建、更新和删除条目来使用该数据库
  • 创建一个后端,为我们的前端公开 REST-API
  • 妥善保护我们的后端
  • 构建一个可以很好地处理 API 数据的前端

构建这个现代 Web 应用程序的方法有很多种。我选择了以下框架,每个框架都包含一个教程:

先决条件

  • 熟悉面向对象编程和 Java
  • 熟练掌握 JavaScript
  • 终端基础知识
  • Mac - 虽然所有这些也应该在 Windows 上运行,但我没有花任何时间检查或提供适用于 Windows 的解决方案

使用 Java 和 Spring 的后端

在本部分中,我们将分四个部分从头开始初始化和开发我们的 Web 后端:

  • 选择框架
  • 初始化 Spring 项目
  • 后端编程
  • 构建后端

选择框架

使用各种语言构建 Web 后端的方法有数百种。没有绝对的对错,我们可以将用于后端的框架用于前端。在本系列中,我希望选择两个独立的框架,它们易于设置、现代化,并且可以用于任何项目,即使项目可能突然快速增长。因此,我们将使用 Java 和 Spring 作为后端。我之前使用过 Spring,觉得它非常简单易用。

初始化 Spring 项目

在开始之前,让我们先了解一下 Spring 到底是什么。

什么是 Spring?

总的来说,Spring是一个能够解耦程序的框架。它通过使用依赖注入,让组件或实现的切换更加便捷。我们稍后会详细介绍。然而,这并非我选择 Spring 作为后端的主要原因。Spring 提供了一个包含各种现成组件的平台,这些组件对 Web 应用非常有用。例如,Spring Security使访问控制变得非常简单且健壮,而Spring Data在连接数据库时非常有用。

要初始化一个新的 Spring 项目,我们可以使用Spring Initializr。打开助手时,我们需要选择:

  • 项目
  • 语言
  • Spring Boot 版本
  • 项目元数据
  • 依赖项

让我们来分析一下。

项目

我们必须在MavenGradle项目之间做出选择。Gradle 和 Maven 都是自动化构建应用程序的工具。我们告诉它项目需要哪些依赖项,例如 Spring,它会将所有依赖项打包在一起(构建)。我对它们都不太了解,无法给出推荐,而且可能没有绝对的对错。和 Spring 一样,我们选择 Gradle,因为我以前用过,而且很喜欢它。

语言

这很容易理解。我们选择 java。

Spring Boot

在此步骤中,我们选择 Spring Boot 版本。我们将使用 2.2.4,因为它是当前版本。但是,什么是 Spring Boot?为什么我们需要它?

基本上,我们使用 Spring 来编写应用程序,并使用 Spring Boot 来处理应用程序的运行开销。Spring Boot会查看您的应用程序,进行检查、假设并提供标准配置以确保应用程序正常运行。例如,它内置了服务器功能,因此您无需担心。然后,Gradle 会将所有这些功能集成到一起,并构建一个 Java 应用程序,从而提供项目元数据。

项目元数据

前两个字段描述了软件的所有者及其名称。通常使用 com.[组织名称].[软件名称] 的模式。在我的例子中,它是 com.milanwittpohl.playground-web-backend。不过,更有趣的是打包和 Java 版本的选项。对于版本,我始终会选择最新的稳定版本,目前是 13。现在我们来看看打包。

包装

构建 Java 应用程序时,我们可以构建 .jar 或 .war 文件。一般来说,.war 文件是在应用服务器内部运行的 Web 应用程序存档。.jar 文件是 Java 存档文件,它既可以在应用服务器内部运行,也可以在用户计算机上运行。我们选择 .jar 文件。

依赖项

这使我们能够定义进一步的依赖项,例如 Spring Security 或 Spring Data。我们暂时将其留空,稍后在实际需要时手动添加必要的依赖项。

让我们生成项目。

后端编程

现在我们已经初始化了项目,接下来我们来实际编写后端程序。我们将分五个步骤完成。

  1. 组织项目文件夹结构
  2. 了解生成的内容
  3. 添加一个端点来感受它
  4. 添加数据库连接来存储数据
  5. 添加简单的身份验证以确保我们的端点是私有的

组织项目文件夹结构

在整个系列中,我们将使用以下文件夹结构:

  • 项目文件夹
    • 后端
    • 前端

将Spring intializr生成的文件夹移动到项目文件夹,并重命名为backend。

了解生成的内容

让我们在我们最喜欢的 Java IDE 中打开项目。对我来说,是IntelliJ。如果您尚未安装gradle,请确保安装。同时,请确保您安装了正确的 Java 版本。如果我们运行该项目,它将立即启动并终止,因为没有任何操作可执行。

在添加任何内容之前,让我们先看看到底生成了什么以及它的作用是什么。

让我们浏览根目录下的每个文件夹和文件。

.gradle

此文件夹用作 Gradle 的缓存。例如,每当 Gradle 解析依赖项时,它都会使用此文件夹来缓存它们。

。主意

此文件夹由 IntelliJ 用于存储项目设置,目前不相关。

建造

此文件夹是在我们运行项目时创建的,其中包含为运行项目而加载到 JVM 中的已编译的 Java 类。

gradle

要真正使用 gradle,我们需要使用 gradle-wrapper,它会提供我们所需的 gradle-build。您可以在这里阅读更多相关信息。

源码

这就是我们的源代码。我们稍后会查看生成的 Java 文件“PlaygroundWebBackendApplication.java”。

.gitignore

该文件列出了所有应该从 git 中排除的目录和文件。

构建.gradle

这里有四个部分。

插件

Gradle 中的插件扩展了我们项目的功能。正如这里所述,插件可以

  • 扩展 gradle
  • 配置项目
  • 应用特定配置

我们有三个插件。让我们看看它们的作用:

  • Spring Boot
  • Spring 依赖管理
    • 该插件允许我们在依赖项部分定义依赖项,而无需提供版本号
    • 然后,插件将匹配我们的 spring boot 版本和依赖项
  • Java
    • 你猜对了,这个插件是用来用 gradle 构建 Java 项目的

元数据

虽然组和版本应该是不言自明的,但源兼容性定义了您的源代码与哪个 Java 版本兼容。

存储库

在这里我们告诉 gradle在哪里寻找我们在依赖项下定义的那些依赖项。

依赖项

现在到了最精彩的部分。在这里,我们告诉 gradle 我们需要哪些依赖项,并让它帮我们获取它们。每个依赖项包含两个元素:配置和依赖项本身。依赖项名称已经很清晰了,我们来看看配置。配置告诉 gradle 该依赖项的用途。我们使用的配置实际上是由 java 插件提供的。在大多数情况下,我们会使用“实现”,因为依赖项仅用于我们程序的实现。

格拉德鲁

该shell脚本用于在macos或其他基于unix的系统上启动gradle-wrapper。

gradlew.bat

该脚本用于在Windows上启动gradle-wrapper。

帮助.md

由 spring Initalizr 生成的帮助文本文件。

设置.gradle

这里我们可以对gradle进行设置。

在我们(最终)开始编程之前,让我们看一下我们的一个 Java 源文件:PlaygroundWebBackendApplication.java

它包含 14 行。

    package com.milanwittpohl.playgroundwebbackend;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class PlaygroundWebBackendApplication {

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

    }

虽然大部分内容看起来都很容易理解,但有一个奇怪的注解 @SpringBootApplication。类似的注解在 Spring 中被广泛使用,我们稍后会详细介绍。通常,在 Java 中,注解可以用作可解释的标记。Spring 会检查你的文件并应用相应的逻辑。更多相关信息可以在这里找到。现在,让我们先来了解一下 @SpringBootApplication 的作用。

  • 首先,使用这个注解和使用这三个注解是一样的
    • @EnableAutoConfiguration
    • @ComponentScan
    • @配置
  • @EnableAutoConfiguration → 查看您的应用程序,进行检查、假设并提供标准配置以使您的应用程序运行
  • @ComponentScan → 在哪里查找带有 Spring 注解的类
  • @Configuration → 这告诉 Spring 此类用于配置

好的,内容有点多,正如之前所说,我们首先会创建一个简单的端点来感受一下。之后,我们会连接数据库,最后确保应用程序的安全。

添加单个端点

首先,我们需要告诉 Spring 我们想要开发 Web 功能。为此,我们只需在 build.gradle 文件中更改依赖项即可。

    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }

通过添加后缀-web,我们可以获得额外的库。重新导入依赖项后,我们就可以开始编码了。

在 Spring 中,端点被称为控制器

Spring 使用MVC 模型,即Spring Web MVC 框架。模型是我们应用程序提供的数据。视图可以是我们使用 Spring 渲染的网页。控制器通过引用模型获取数据,并向视图提供必要的信息。由于我们不使用 Spring 来渲染视图,所以我们根本没有视图。我们剩下的只是一个模型和一个控制器。因此,我们的端点就是控制器,因为它负责响应 Web 请求。要定义新的控制器,我们创建

  • 一个名为 controller 的新包
  • 一个名为 HelloWorldController 的新 Java 类

要定义端点,我们必须做三件事。

1. 将我们的新类声明为控制器

首先,我们需要告诉 Spring,我们新建的 HelloWorldController 是一个控制器。为此,我们使用一个构造型注解。定义控制器有两个注解:@Controller 和 @RestController。@Restcontroller 是 @Controller 注解的特化版本,它简化了我们的设置实现,因为我们不需要在端点上显式添加 @ResponseBody 注解。这里对此进行了更详细的解释。所以我们只需将注解添加到我们的类中即可。

    @RestController
    public class HelloWorldController {}

2. 创建端点

要创建端点,我们只需定义一个方法。此方法可以调用不同的类并返回任何类型的对象。为了简单起见,我们只返回一个字符串。

    public String sayHelloWorld(){
        return "Hello World!";
    }

3.定义端点的路径和请求类型

最后,我们需要告诉 Spring 应该将哪个路径和请求类型映射到这个端点。为了简单起见,我们将其定义为“/sayhello”下的 get 请求。正如你猜到的那样,我们使用注解来实现这一点。

    @GetMapping("/sayhello")
    public String sayHelloWorld(){
        return "Hello World!";
    }

你的课程现在应该是这样的

    package com.milanwittpohl.playgroundwebbackend.controller;

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    public class HelloWorldController {

        @GetMapping("/sayhello")
        public String sayHelloWorld(){
            return "Hello World!";
        }

    }

让我们开始运行吧!应用程序启动后,我们只需打开浏览器并输入localhost:8080/sayhello即可返回字符串。

处理数据

每个 Web 后端都需要在某个位置存储数据。为了构建一个 Todo 应用,我们将定义一个简单的 todo 对象,然后实现读取、写入、更新和删除条目的端点。在开始编码之前,我们需要告诉 Spring 我们想要处理数据。

为了简单起见,我们将使用 MongoDB,但其他数据库的设置也类似。要添加该功能,我们只需在 build.gradle 文件中添加另一个名为org.springframework.boot:spring-boot-starter-data-mongodb 的依赖项。

    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

您还需要在您的计算机上运行一个 mongoDB 实例。最简单的方法可能是使用一个简单的 docker-compose 文件。如果您不知道什么是 docker,不用担心。我们将在第三部分中详细介绍。现在安装docker,并在您的项目根目录(而不是 Spring 项目)中创建一个名为 docker-compose-dev.yml 的新文件。插入以下代码并替换占位符。

    version: '3'
    services:
      playground-web-db:
        image: mongo:4.2.2
        environment:
          MONGO_INITDB_DATABASE: playground-web
        ports:
          - 27017:27017

要启动数据库,请在项目根文件夹中的终端中运行以下命令。保持终端窗口打开。

    docker-compose -f docker-compose-dev.yml up

好的,回到 Spring。我们还应该思考一下如何构建我们的应用程序。

  • 我们将创建一个描述我们的 ToDo 对象的数据对象
  • 我们将创建一个与数据库对话的存储库
  • 我们将创建一个处理额外业务逻辑的服务
  • 我们将创建一个控制器来接受读取、创建、更新和删除条目的请求

创建数据对象

我们首先创建一个名为 data 的包。接下来,我们创建一个名为 ToDo 的 Java 类。这个类表示一个待办事项。为了简单起见,我们的 todo 对象只有一个唯一的 ID、一个标题和一个状态。这就得到了下面的类。

    package com.milanwittpohl.playgroundwebbackend.data;

    public class ToDo {

        private String id;

        private String title;

        private Boolean completed;

    }

虽然属性 title 和 completed 可以由用户设置,但 id 应该仅由框架修改,以确保其完整性。由于大多数对象都有 id,Spring 提供了一个便捷的注解@id。该注解将字段标记为主键,并自动为我们生成值。为了确保一切正常工作,我们还需要为所有属性设置构造函数和 getter 方法。

    package com.milanwittpohl.playgroundwebbackend.data;

    import org.springframework.data.annotation.Id;

    public class ToDo {

        @Id
        private String id;

        private String title;

        private Boolean completed;

        public ToDo(String title, Boolean completed){
            this.title = title;
            this.completed = completed;
        }

        public String getId() {
            return id;
        }

        public String getTitle() {
            return title;
        }

        public Boolean getCompleted() {
            return completed;
        }

    }

创建存储库

存储库与我们的数据库交互。这正是 Spring 真正闪耀的地方。要与 mongo 数据库交互,我们只需创建一个扩展 MonogRepository 的接口。我们将其命名为ToDoRepository,并将其放入名为 repository 的新包中。扩展 MonogRepository 时,我们必须提供两个参数,因为它使用了泛型。第一个参数是我们想要使用的实体,即 ToDo 类。第二个参数告诉 Spring id 属性的类型,在本例中是一个字符串。就是这样,是不是很简单?

    package com.milanwittpohl.playgroundwebbackend.repository;

    import com.milanwittpohl.playgroundwebbackend.data.ToDo;
    import org.springframework.data.mongodb.repository.MongoRepository;

    public interface ToDoRepository extends MongoRepository<ToDo, String> {}

创建服务

创建一个名为 service 的新包。在该包中,我们创建一个名为 ToDoService 的新 Java 类。这就是我们的业务逻辑所在。我们只能在这里处理数据。幸运的是,我们没有任何业务逻辑。但是,我们需要定义一些方法来读取、创建、更新和删除实体。相信我,这非常简单。

  • 首先,我们通过添加 @Service 注解来告诉 Spring,这个类是一个服务。这将通过 Spring 增加更多功能
  • 接下来,我们要将存储库添加为私有属性,以便使用。这时 Springs 的构造函数注入就派上用场了。我们只需添加注解 @Autowired,Spring 就会确保我们在运行时拥有一个实例。
  • 现在我们添加方法
    • 获取所有待办事项
    • 通过 id 获取单个 todo
    • 保存新的或现有的待办事项
    • 根据 ID 删除单个待办事项
  • 当我们扩展 MongoRepository 时,我们的存储库已经具备了我们需要的所有方法。
    package com.milanwittpohl.playgroundwebbackend.service;

    import com.milanwittpohl.playgroundwebbackend.data.ToDo;
    import com.milanwittpohl.playgroundwebbackend.exception.EntityNotFoundException;
    import com.milanwittpohl.playgroundwebbackend.repository.ToDoRepository;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;

    import java.util.List;

    @Service
    public class ToDoService {

        @Autowired
        private ToDoRepository toDoRepository;

        public List<ToDo> findAll(){
            return toDoRepository.findAll();
        }

        public ToDo findById(String id){
            return toDoRepository.findById(id).orElseThrow(EntityNotFoundException::new);
        }

        public ToDo save(ToDo toDo){
            return toDoRepository.save(toDo);
        }

        public void deleteById(String id){
            toDoRepository.deleteById(id);
        }

    }

除了 findById 方法之外,其他一切都应该非常简单。如果我们尝试通过 id 查找实体,可能会出现找不到与给定 id 匹配的实体的情况。这就是为什么我们的存储库返回 ToDo 的 Optional 类型。Optional 类型可以包含实体,也可以不包含。为了简单起见,我们告诉程序,如果没有匹配,则抛出一个新的异常。这个异常必须先在名为 Exception 的新包中创建

    package com.milanwittpohl.playgroundwebbackend.exception;

    public class EntityNotFoundException extends RuntimeException {}

创建控制器

我们在控制器包中创建了一个新的控制器,并将其命名为ToDoController。这同样非常简单,所以我只展示最终的代码,然后解释一些与 HelloWorldController 相比的新特性。你可能会问自己,为什么我们不直接在这里返回实体,而是使用服务呢?虽然现在这看起来很合理,但一旦我们有多个数据对象和控制器,就会变得非常混乱。

    package com.milanwittpohl.playgroundwebbackend.controller;

    import com.milanwittpohl.playgroundwebbackend.data.ToDo;
    import com.milanwittpohl.playgroundwebbackend.service.ToDoService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;

    import java.util.List;

    @RestController
    @RequestMapping("/api/todo")
    public class ToDoController {

        @Autowired
        private ToDoService toDoService;

        @GetMapping
        public List<ToDo> findAll(){
            return toDoService.findAll();
        }

        @GetMapping("/{id}")
        public ToDo findById(@PathVariable String id){
            return toDoService.findById(id);
        }

        @PostMapping
        public ToDo create(@RequestBody ToDo toDo){
            return toDoService.save(toDo);
        }

        @PutMapping("/{id}")
        public ToDo update(@RequestBody ToDo toDo){
            return toDoService.save(toDo);
        }

        @DeleteMapping("/{id}")
        public void deleteById(@PathVariable String id){
            toDoService.deleteById(id);
        }

    }
  • 首先,我们注意到该类有第二个名为 @RequestMapping 的注解。这样,所有端点都映射到/api/todo
  • 要获取特定的待办事项,我们需要通过 URL 传递 ID(例如 localhost:8080/api/todo/5d6e53da5c88f13387cb8fa3)。因此,我们需要告诉 Spring 该 URL 包含一个变量,即 ID。这可以通过在 get 映射注解中使用花括号以及在方法头中使用 @PathVariable 注解来实现。
  • 要创建或更新实体,我们需要传输包含信息的 JSON 主体。我们使用 @RequestBody 通知 Spring。
    // For creation
    {
        "title":"Finish it",
        "completed":false
    }
    // For updating
    {
        "id": "5d6e54bd5c88f133b8209f34",
        "title": "Finish it",
        "completed": true
    }

运行应用程序,您将看到您可以获取、创建、更新和删除待办事项。

    // Get all
    curl -X GET http://localhost:8080/api/todo

    // Create one
    curl -X POST \
      http://localhost:8080/api/todo/ \
      -H 'Content-Type: application/json' \
      -d '{
        "title":"Finish it",
        "completed":false
    }'

    // Get one
    curl -X GET http://localhost:8080/api/todo/<ID of ToDo>

    // Update one
    curl -X PUT \
      http://localhost:8080/api/todo/<ID of ToDo> \
      -H 'Content-Type: application/json' \
      -d '{
        "id": "<ID of ToDo>",
        "title": "Finish it",
        "completed": true
    }'

    // Delete one
    curl -X DELETE http://localhost:8080/api/todo/<ID of ToDo>

保护端点

在构建应用程序并完成本教程之前,我们需要确保端点是安全的。我们可以实现用户管理服务,但目前我们只需确保端点安全并添加一个用户进行身份验证即可。

首先,我们在 gradle 文件中添加一个名为org.springframework.boot:spring-boot-starter-security的新依赖项。接下来,我们需要配置 Spring 如何保护我们的应用程序。Spring 中的配置可以使用类和注解来完成。让我们创建一个名为 configuration 的新包,并在该包中创建一个新类,名为 WebSecurityConfiguration

  • 为了让 Spring 知道此类用于配置,我们向类添加了 @Configuration 注释
  • 由于我们想使用此配置来定义访问管理,我们的类需要扩展 WebSecurityConfigurerAdapter 类
    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {}
  • 为了真正保护我们的端点,我们需要重写 WebSecurityConfigurerAdapter 类的 configure 方法
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
             .authorizeRequests().antMatchers("/api/**").authenticated()
             .and()
             .httpBasic()
             .and()
             .exceptionHandling()
                 .authenticationEntryPoint(restAuthenticationEntryPoint)
             .and()
             .formLogin()
                     .loginProcessingUrl("/api/login")
                 .successHandler(loginSuccessHandler)
                 .failureHandler(new SimpleUrlAuthenticationFailureHandler());
     }
  • 在该方法中,我们为应用程序配置了 HttpSecurity 实例 http。我们使用 and 方法将不同的命令连接在一起。由于这里涉及的内容很多,我们将逐行讲解。大部分内容取自本教程,该教程也提供了更深入的讲解。

全面保护

  • 首先,我们告诉应用程序要保护(授权)所有对 /api/ 的请求。任何 http 请求都需要通过 http-basic 进行身份验证。
  • 当用户尝试访问受保护的网页时,用于返回 401 未授权响应的入口点
  • 虽然我们可以在每个请求中都发送凭证,但我们希望有一个登录页面,用户可以登录并获取令牌。这样,我们可以更轻松地识别每个用户会话,而不必每次都发送凭证。
  • 参数“loginProcessingUrl”告诉应用程序我们将凭证发送到的 URL。我们将在前端使用它。
  • 接下来我们定义两个处理程序,一个用于登录成功,一个用于登录失败

登录页面 - 成功处理程序

  • 我们的成功处理程序是我们配置包中的一个新类,它扩展了 SimpleUrlAuthenticationSuccessHandler。
  • 基本上,我们不希望后端在登录成功后返回 200 OK 响应。我们在这里扩展的 SimpleUrlAuthenticationSuccessHandler 实现了这个功能,但它还会将用户重定向到某个 URL。我们不希望这样,我们只想返回 200 响应。为了方便起见,我们只需复制 SimpleUrlAuthenticationSuccessHandler 的代码并删除重定向的逻辑。剩下的代码如下:
    @Component("loginSuccessHandler")
    public class LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

        private RequestCache requestCache = new HttpSessionRequestCache();

        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {

            SavedRequest savedRequest = requestCache.getRequest(request, response);

            if (savedRequest == null) {
                clearAuthenticationAttributes(request);
                return;
            }
            String targetUrlParam = getTargetUrlParameter();
            if (isAlwaysUseDefaultTargetUrl() || (targetUrlParam != null && StringUtils.hasText(request.getParameter(targetUrlParam)))) {
                requestCache.removeRequest(request, response);
                clearAuthenticationAttributes(request);
                return;
            }

            clearAuthenticationAttributes(request);
        }

        public void setRequestCache(RequestCache requestCache) {
            this.requestCache = requestCache;
        }

    }
  • 我们需要将这个类注入到我们的 WebSecurityConfigurerAdapter 中。因此,我们需要使用@Component注解来告诉 Spring,我们稍后需要注入这个类。

登录页面 - 失败处理程序

  • 如果登录失败,我们只想返回 401 Unauthorized 响应
  • 这里我们甚至不必实现我们自己的代码,我们只需使用 SimpleUrlAuthenticationFailureHandler 类。

异常处理 - RestAuthenticationEntryPoint

  • 如果用户试图访问未经授权的页面,我们只需返回 401 响应。
  • 创建一个名为 RestAuthenticationEntryPoint.java 的新类并插入以下代码。
@Component
public final class RestAuthenticationEntryPoint
        implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

在 WebSecurityConfiguration 中注入处理程序

要使用处理程序,我们需要将它们注入到我们的 WebSecurityConfiguration 中。

    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Autowired
        private LoginSuccessHandler loginSuccessHandler;

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ...

使用凭证定义用户

  • 如果我们现在运行应用程序,任何对 /api/todo/ 的请求都会失败,因为我们未授权。但是,我们的第一个端点 /sayHello 仍然可以访问。
  • 现在我们需要配置一个允许访问 /api/todo/ 的简单用户。为此,我们在 WebSecurityConfiguration 中添加了两个方法
    @Bean
    public UserDetailsService userDetailsService() {
      String password = "password";
      String username = "user";
      InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
      String encodedPassword = passwordEncoder().encode(password);
      manager.createUser(User.withUsername(username).password(encodedPassword).roles("USER").build());
      return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder();
    }
  • 通过第一种方法,我们告诉 Spring 将用户详细信息存储在应用程序内存中并创建一个新用户
  • 为了对密码进行编码(因为我们从不想以纯文本形式存储密码),我们在第二种方法中定义了一个密码编码器
  • 您可能已经注意到,我们在这两个方法上都使用了一个名为 @Bean 的新注解。通过该注解,我们告诉 Spring 创建一个Java Bean。如果没有该注解,我们的配置将无法访问这些方法。
  • 我们的配置类现在应该是这样的
    package com.milanwittpohl.playgroundwebbackend.configuration;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.User;
    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.access.intercept.FilterSecurityInterceptor;
    import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
    import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

    @Configuration
    public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

        @Autowired
        private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

        @Autowired
        private LoginSuccessHandler loginSuccessHandler;


        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests().antMatchers("/api/**").authenticated()
                    .and()
                    .httpBasic()
                    .and()
                    .exceptionHandling()
                        .authenticationEntryPoint(restAuthenticationEntryPoint)
                    .and()
                    .formLogin()
                        .loginProcessingUrl("/api/login")
                        .successHandler(loginSuccessHandler)
                        .failureHandler(new SimpleUrlAuthenticationFailureHandler());
        }

       @Bean
        public UserDetailsService userDetailsService() {
            String username = "user";
            String password = "password";
            InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
            String encodedPassword = passwordEncoder().encode(password);
            manager.createUser(User.withUsername(username).password(encodedPassword).roles("USER").build());
            return manager;
        }

        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
  • 如果我们现在运行我们的应用程序,我们的待办事项端点是安全的,并且只能使用基本身份验证进行访问。

太棒了,就是这样。现在我们要做的就是构建应用程序。

构建应用程序

这其实很简单。你可能还记得,我们决定使用 Gradle 来实现自动化构建。所以我们只需要打开终端,导航到我们的项目文件夹,然后执行

    gradle build

这会在 ./build/libs/ 下为我们构建一个 .jar 文件。

要运行我们的应用程序,只需执行

    java -jar ./build/libs/playground-web-backend-0.0.1-SNAPSHOT.jar

恭喜您完成本教程!!!

这是我的第一个教程系列,非常感谢大家的反馈。您可以留言,在TwitterInstagram上联系我,或者给我发邮件

本教程最初发布在我的个人网站上。

鏂囩珷鏉ユ簮锛�https://dev.to/milanwittpohl/part-i-the-backend-using-java-with-spring-3g8c
PREV
我在周末制作的一个简单的 COVID-19 仪表板
NEXT
Vue CLI 3.0 插件,用于使用 Atomic Design 和 Storybook 创建应用程序