百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

Spring Security 用户管理三

wptr33 2024-11-27 21:39 27 浏览

指导 Spring Security 如何管理用户

在前一节中,您实现了 UserDetails 契约来描述用户,使 Spring Security 能够理解他们。但是 Spring Security 如何管理用户呢?比较凭据时,它们是从哪里获取的?如何添加新用户或更改现有用户?在前面的文章中,您了解了框架定义了一个特定的组件,身份验证流程将用户管理委托给该组件: UserDetailsService 实例。我们甚至定义了一个 UserDetailsService 来覆盖 Spring Boot 提供的默认实现。

在本节中,我们将试验实现 UserDetailsService 类的各种方法。通过实现示例中 UserDetailsService 契约所描述的职责,您将了解用户管理是如何工作的。之后,您将了解 UserDetailsManager 接口如何将更多行为添加到 UserDetailsService 定义的契约中。在本文的最后,我们将使用 Spring Security 提供的 UserDetailsManager 接口的实现。我们将编写一个示例项目,其中我们将使用 Spring Security 提供的最著名的实现之一,JdbcUserDetailsManager。了解了这一点,您将知道如何告诉 Spring Security 在哪里查找用户,这在身份验证流中是至关重要的。

理解 UserDetailsService 契约

在本节中,您将了解 UserDetailsService 接口定义。在理解如何以及为什么要实现它之前,您必须首先了解契约。现在是详细介绍 UserDetailsService 以及如何使用该组件的实现的时候了。UserDetailsService 接口只包含一个方法,如下所示:

public interface UserDetailsService {

  	UserDetails loadUserByUsername(String username)  throws UsernameNotFoundException;
}

身份验证实现调用 loadUserByUsername(String username) 方法以获得具有给定用户名的用户的详细信息 ( 图 3.3 ) 。当然,用户名是唯一的。这个方法返回的用户是 UserDetails 契约的一个实现。如果用户名不存在,该方法将抛出 UsernameNotFoundException 异常。


AuthenticationProvider 是实现身份验证逻辑并使用 UserDetailsService 加载有关用户的详细信息的组件。 要通过用户名查找用户,它将调用 loadUserByUsername(String username) 方法。

注意: UsernameNotFoundException 是一个运行时异常。UserDetailsService 接口中的 throws 子句仅用于文档目的。UsernameNotFoundException 直接继承自 AuthenticationException 类型,该类型是所有与身份验证过程相关的异常的父类。AuthenticationException 继承了 RuntimeException 类。

实现 UserDetailsService 契约

在本节中,我们将通过一个实际示例来演示 UserDetailsService 的实现。 您的应用程序管理有关凭据和其他用户方面的详细信息。 这些可能存储在数据库中,或者由您通过 Web 服务或其他方式访问的其他系统处理(图3.3)。 无论系统中的情况如何,Spring Security 唯一需要您执行的都是一种通过用户名检索用户的实现。

在下一个示例中,我们编写一个 UserDetailsService,其中包含用户的内存列表。 在上篇文章中,您使用了提供的实现相同功能的实现 InMemoryUserDetailsManager。 因为您已经熟悉了此实现的工作原理,所以我选择了类似的功能,但这一次是我们自己实现。 当我们创建 UserDetailsService 类的实例时,我们提供用户列表。 如以下清单所示。

清单 3.12 UserDetails 接口的实现

public class User implements UserDetails {

      // User 类是不可变的。 在构建实例时,需要提供三个属性的值,这些值以后不能更改。
      private final String username;
      private final String password;
     // 为了简化示例,用户只有一个权限。
      private final String authority;

      public User(String username, String password, String authority) {
        this.username = username;
        this.password = password;
        this.authority = authority;
      }

      @Override
      public Collection<? extends GrantedAuthority> getAuthorities() {
        // 返回仅包含 GrantedAuthority 对象的列表,该列表具有您在创建实例时提供的名称
        return List.of(() -> authority);
      }

      @Override
      public String getPassword() {
        return password;
      }

      @Override
      public String getUsername() {
        return username;
      }

     // 帐号没有过期或被锁定。
      @Override
      public boolean isAccountNonExpired() {
        return true;
      }

      @Override
      public boolean isAccountNonLocked() {
        return true;
      }

      @Override
      public boolean isCredentialsNonExpired() {
        return true;
      }

      @Override
      public boolean isEnabled() {
        return true;
      }
}

在名为 services 的包中,我们创建一个名为 InMemoryUserDetailsService 的类。 以下清单显示了我们如何实现此类。

清单 3.13 UserDetailsService 接口的实现

public class InMemoryUserDetailsService implements UserDetailsService {

  // UserDetailsService 管理内存中的用户列表。
  private final List<UserDetails> users;

  public InMemoryUserDetailsService(List<UserDetails> users) {
    this.users = users;
  }

  @Override
  public UserDetails loadUserByUsername(String username) 
    throws UsernameNotFoundException {
    
    return users.stream()
      .filter(
         // 从用户列表中,过滤具有所需用户名的用户
         u -> u.getUsername().equals(username)
      )    
      .findFirst()  //如果有这样的用户,将其返回
      .orElseThrow(
          // 如果使用该用户名的用户不存在,则会引发异常
        () -> new UsernameNotFoundException("User not found")
      );    
   }
}

loadUserByUsername(String username) 方法在用户列表中搜索给定的用户名,并返回所需的 UserDetails 实例。 如果没有使用该用户名的实例,则会引发 UsernameNotFoundException。 现在,我们可以将此实现用作 UserDetailsService。 下一个清单显示了如何将其添加为配置类中的 Bean 并在其中注册一个用户。

清单 3.14 UserDetailsService 注册为配置类中的 bean

@Configuration
public class ProjectConfig {

  @Bean
  public UserDetailsService userDetailsService() {
    UserDetails u = new User("tom", "12345", "read");
    List<UserDetails> users = List.of(u);
    return new InMemoryUserDetailsService(users);
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }
}

最后,我们创建一个简单的端点并测试实现。 以下清单定义了端点。

清单 3.15 用于测试实现的端点的定义

@RestController
public class HelloController {

  @GetMapping("/hello")
  public String hello() {
    return "Hello!";
  }
}

当使用 cURL 调用端点时,我们观察到对于密码为 12345 的用户 tom,我们返回了 HTTP 200 OK 。 如果我们使用其他东西,则应用程序将返回 401未经授权 。

curl -u tom:12345 http://localhost:8080/hello

响应体

Hello!

实现 UserDetailsManager 契约

在本节中,我们将讨论 UserDetailsManager 接口的使用和实现。此接口扩展并向 UserDetailsService 契约添加更多方法。Spring Security 需要 UserDetailsService 契约来执行身份验证。但一般来说,在应用程序中,还需要管理用户。大多数情况下,应用程序应该能够添加新用户或删除现有用户。在本例中,我们实现了由 Spring Security 定义的更特殊的接口 UserDetailsManager。它扩展了 UserDetailsService 并添加了我们需要实现的更多操作。

public interface UserDetailsManager extends UserDetailsService {
  void createUser(UserDetails user);
  void updateUser(UserDetails user);
  void deleteUser(String username);
  void changePassword(String oldPassword, String newPassword);
  boolean userExists(String username);
}

我们在第二章中使用的 InMemoryUserDetailsManager 对象实际上是一个 UserDetailsManager。 当时,我们只考虑了它的 UserDetailsService 特性,但是现在您更好地理解了为什么我们能够在实例上调用 createUser() 方法。

使用 JdbcUserDetailsManager 进行用户管理

除了 InMemoryUserDetailsManager 之外,我们经常使用另一个 UserDetailManager, JdbcUserDetailsManager。JdbcUserDetailsManager 管理 SQL 数据库中的用户。它直接通过 JDBC 连接到数据库。通过这种方式,JdbcUserDetailsManager 独立于任何其他与数据库连接性相关的框架或规范。

要理解 JdbcUserDetailsManager 是如何工作的,最好通过示例将其付诸实践。在下面的示例中,您将实现一个应用程序,该应用程序使用 JdbcUserDetailsManager 管理 MySQL 数据库中的用户。图 3.4 概述了 JdbcUserDetailsManager 实现在身份验证流程中的位置。

通过创建一个数据库和两个表,您将开始处理关于如何使用 JdbcUserDetailsManager 的演示应用程序。在本例中,我们将数据库命名为 spring,并将其中一个表命名为users和其他权限。这些名称是 JdbcUserDetailsManager 已知的默认表名。正如您将在本节末尾了解到的,JdbcUserDetailsManager 实现是灵活的,如果您想重写这些默认名称,它允许您这样做。users 表的目的是保存用户记录。JdbcUserDetails Manager 实现期望在用户表中有三列:用户名、密码和启用,您可以使用它们来禁用用户。


Spring Security 认证流程。 在这里,我们使用 JDBCUserDetailsManager 作为我们的 UserDetailsService 组件。 JdbcUserDetailsManager 使用数据库来管理用户。

您可以选择使用数据库管理系统(DBMS)的命令行工具或客户端应用程序自行创建数据库及其结构。 例如,对于MySQL,您可以选择使用MySQL Workbench来执行此操作。 但是最简单的方法是让Spring Boot自己为您运行脚本。 为此,只需在资源文件夹中的项目中再添加两个文件:schema.sql和data.sql。 在schema.sql文件中,添加与数据库结构相关的查询,例如创建,更改或删除表。 在data.sql文件中,添加与表内的数据一起使用的查询,例如INSERT,UPDATE或DELETE。 启动应用程序时,Spring Boot会自动为您运行这些文件。 用于构建需要数据库的示例的一种更简单的解决方案是使用H2内存数据库。 这样,您无需安装单独的DBMS解决方案。

如果愿意,在开发本系列中介绍的应用程序时也可以使用 H2。 我选择使用外部 DBMS 来实现示例,以使其清楚地是系统的外部组件,从而避免造成混淆。

您可以使用下一个清单中的代码使用 MySQL 服务器创建 users 表。 您可以将此脚本添加到 Spring Boot 项目中的 schema.sql 文件中。

清单 3.16 用于创建用户表的SQL查询

CREATE TABLE IF NOT EXISTS `spring`.`users` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(45) NOT NULL,
  `password` VARCHAR(45) NOT NULL,
  `enabled` INT NOT NULL,
  PRIMARY KEY (`id`));

权限表存储每个用户的权限。 每个记录都存储一个用户名和使用该用户名授予用户的权限。

清单 3.17 用于创建权限表的 SQL 查询

CREATE TABLE IF NOT EXISTS `spring`.`authorities` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(45) NOT NULL,
  `authority` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`id`));

为简单起见,在本系列随附的示例中,我跳过了索引或外键的定义。

为了确保您有要测试的用户,请在每个表中插入一条记录。 您可以将这些查询添加到 Spring Boot 项目的 resources 文件夹中的 data.sql 文件中:

INSERT IGNORE INTO `spring`.`authorities` VALUES (NULL, 'tom', 'write');
INSERT IGNORE INTO `spring`.`users` VALUES (NULL, 'tom', '12345', '1');

对于您的项目,您至少需要添加以下清单中所述的依赖项。 检查您的 pom.xml 文件,以确保您添加了这些依赖项。

清单 3.18 开发示例项目所需的依赖关系

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <scope>runtime</scope>
</dependency>

在示例中,只要将正确的JDBC驱动程序添加到依赖项中,就可以使用任何SQL数据库技术。

您可以在项目的 application.properties 文件中配置数据源,也可以将其配置为单独的 Bean。 如果选择使用 application.properties 文件,则需要在该文件中添加以下行:

spring.datasource.url=jdbc:mysql://localhost/spring
spring.datasource.username=<your user>
spring.datasource.password=<your password>
spring.datasource.initialization-mode=always

在项目的配置类中,定义 UserDetailsService 和 PasswordEncoder。 JdbcUserDetailsManager 需要数据源才能连接到数据库。 数据源可以通过方法的参数(如下面的清单中所示)或通过类的属性自动装配。

清单 3.19 在配置类中注册 JdbcUserDetailsManager

@Configuration
public class ProjectConfig {

  @Bean
  public UserDetailsService userDetailsService(DataSource dataSource) {
    return new JdbcUserDetailsManager(dataSource);
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }
}

要访问应用程序的任何端点,您现在需要对存储在数据库中的用户之一使用 HTTP Basic 身份验证。 为了证明这一点,我们创建了一个新的端点,如下面的清单所示,然后使用 cURL 对其进行调用。

清单 3.20 用于检查实现的测试端点

@RestController
public class HelloController {

  @GetMapping("/hello")
  public String hello() {
    return "Hello!";
  }
}

在下一个代码段中,使用正确的用户名和密码调用端点时,您将找到结果:

curl -u tom:12345 http://localhost:8080/hello

调用的响应

Hello!

JdbcUserDetailsManager 还允许您配置所使用的查询。 在前面的示例中,我们确保为表和列使用了确切的名称,因为 JdbcUserDetailsManager 实现期望这些名称。 但是对于您的应用程序来说,这些名称并不是最佳选择。 下一个清单显示了如何覆盖 JdbcUserDetailsManager 的查询。

清单 3.21 更改 JdbcUserDetailsManager 的查询以查找用户

@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
  		String usersByUsernameQuery =  "select username, password, enabled from users where username = ?";
  		String authsByUserQuery = "select username, authority from spring.authorities where username = ?";
      
      var userDetailsManager = new JdbcUserDetailsManager(dataSource);
      userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
      userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
      return userDetailsManager;
}

以相同的方式,我们可以更改 JdbcUserDetailsManager 实现使用的所有查询。

练习:编写一个类似的应用程序,为其在数据库中使用不同的名称命名表和列。 覆盖对 JdbcUserDetailsManager 实现的查询(例如,身份验证使用新的表结构)。

相关推荐

SpringBoot 3 + Flutter3 实战低代码运营管理-10章

获课》aixuetang.xyz/5075/三天构建运营管理系统:SpringBoot3+Flutter3高效开发方法论...

SpringBoot探针实现:从零构建应用健康监控利器

SpringBoot探针实现:从零构建应用健康监控利器声明本文中的所有案例代码、配置仅供参考,如需使用请严格做好相关测试及评估,对于因参照本文内容进行操作而导致的任何直接或间接损失,作者概不负责。本文...

Spring Batch中的JobRepository:批处理的“记忆大师”是如何工作

一、JobRepository是谁?——批处理的“档案馆”JobRepository是SpringBatch的“记忆中枢”,负责记录所有Job和Step的执行状态。它像一位严谨的档案管理员,把任务执...

Github霸榜的SpringBoot全套学习教程,从入门到实战,内容超详细

前言...

还在为 Spring Boot3 技术整合发愁?一文解锁大厂都在用的实用方案

你在使用SpringBoot3开发后端项目时,是不是常常陷入这样的困境?想提升项目性能和功能,却不知道该整合哪些技术;好不容易选定技术,又在配置和使用上频频踩坑。其实,这是很多互联网大厂后端开发...

一文吃透!Spring Boot 项目请求日志记录,这几招你绝对不能错过!

在互联网应用开发的高速赛道上,系统的稳定性、可维护性以及安全性是每一位开发者都必须关注的核心要素。而请求日志记录,就如同系统的“黑匣子”,能够为我们提供排查故障、分析用户行为、优化系统性能等关键信息...

spring-boot-starter-actuator简单介绍

SpringBootActuator是SpringBoot的一个功能强大的子项目,它提供了一些有用的监控和管理SpringBoot应用程序的端点。SpringBootActuat...

使用SpringBoot钩子或Actuator实现优雅停机

服务如何响应停机信号在java中我们可以直接利用通过Runtime...

28-自定义Spring Boot Actuator指标

上篇我们学习了《27-自定义SpringBootActuator健康指示器》,本篇我们学习自定义SpringBootActuator指标(Metric)。...

如何在Spring Boot中整合Spring Boot Actuator进行服务应用监控?

监控是确保系统稳定性和性能的关键组成部分,而在SpringBoot中就提供了默认的应用监控方案SpringBootActuator,通过SpringBootActuator提供了开箱即用的应...

「Spring Boot」 Actuator Endpoint

Actuator官网地址:https://docs.spring.io/spring-boot/docs/2.5.6/reference/html/actuator.html目的监控并管理应用程序...

Spring Boot Actuator监控功能全面剖析

SpringBootActuator监控功能全面剖析在现代企业级Java开发中,SpringBoot以其轻量化、高效率的特性深受开发者青睐。而作为SpringBoot生态系统的重要组成部分,S...

1000字彻底搞懂SpringBootActuator组件!

SpringBootActuator组件SpringBootActuator通过HTTPendpoints或者JMX来管理和监控SpringBoot应用,如服务的审计、健康检查、指标统计和...

JavaScript数据类型(javascript数据类型介绍)

基本数据类型BooleanNullNumberStringSymbolUndefined对象数据类型ObjectArray定义:JavaScript数组是内置的对象之一,它可以用一个变量来存储多个同种...

能运行,不代表它是对的:5 个潜伏在正常功能下的 JavaScript 错误

JavaScript的动态性和复杂性意味着,代码虽然表面上正常运行,但一些深层次、隐蔽的陷阱往往让人意想不到,梳理了几个JavaScript开发中难以发现的隐蔽错误,旨在帮助我们写出更健壮、更可...