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

Spring Security 用户管理三

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

指导 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 实现的查询(例如,身份验证使用新的表结构)。

相关推荐

oracle数据导入导出_oracle数据导入导出工具

关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...

继续学习Python中的while true/break语句

上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个...

python continue和break的区别_python中break语句和continue语句的区别

python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...

简单学Python——关键字6——break和continue

Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...

2-1,0基础学Python之 break退出循环、 continue继续循环 多重循

用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...

Python 中 break 和 continue 傻傻分不清

大家好啊,我是大田。...

python中的流程控制语句:continue、break 和 return使用方法

Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...

L017:continue和break - 教程文案

continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...

作为前端开发者,你都经历过怎样的面试?

已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...

面试被问 const 是否不可变?这样回答才显功底

作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...

2023金九银十必看前端面试题!2w字精品!

导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。...

前端面试总结_前端面试题整理

记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...

由浅入深,66条JavaScript面试知识点(七)

作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录...

2024前端面试真题之—VUE篇_前端面试题vue2020及答案

添加图片注释,不超过140字(可选)...

今年最常见的前端面试题,你会做几道?

在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...