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

Spring Security 用户管理三

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

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

相关推荐

redis的八种使用场景

前言:redis是我们工作开发中,经常要打交道的,下面对redis的使用场景做总结介绍也是对redis举报的功能做梳理。缓存Redis最常见的用途是作为缓存,用于加速应用程序的响应速度。...

基于Redis的3种分布式ID生成策略

在分布式系统设计中,全局唯一ID是一个基础而关键的组件。随着业务规模扩大和系统架构向微服务演进,传统的单机自增ID已无法满足需求。高并发、高可用的分布式ID生成方案成为构建可靠分布式系统的必要条件。R...

基于OpenWrt系统路由器的模式切换与网页设计

摘要:目前商用WiFi路由器已应用到多个领域,商家通过给用户提供一个稳定免费WiFi热点达到吸引客户、提升服务的目标。传统路由器自带的Luci界面提供了工厂模式的Web界面,用户可通过该界面配置路...

这篇文章教你看明白 nginx-ingress 控制器

主机nginx一般nginx做主机反向代理(网关)有以下配置...

如何用redis实现注册中心

一句话总结使用Redis实现注册中心:服务注册...

爱可可老师24小时热门分享(2020.5.10)

No1.看自己以前写的代码是种什么体验?No2.DooM-chip!国外网友SylvainLefebvre自制的无CPU、无操作码、无指令计数器...No3.我认为CS学位可以更好,如...

Apportable:拯救程序员,IOS一秒变安卓

摘要:还在为了跨平台使用cocos2d-x吗,拯救objc程序员的奇葩来了,ApportableSDK:FreeAndroidsupportforcocos2d-iPhone。App...

JAVA实现超买超卖方案汇总,那个最适合你,一篇文章彻底讲透

以下是几种Java实现超买超卖问题的核心解决方案及代码示例,针对高并发场景下的库存扣减问题:方案一:Redis原子操作+Lua脚本(推荐)//使用Redis+Lua保证原子性publicbo...

3月26日更新 快速施法自动施法可独立设置

2016年3月26日DOTA2有一个79.6MB的更新主要是针对自动施法和快速施法的调整本来内容不多不少朋友都有自动施法和快速施法的困扰英文更新日志一些视觉BUG修复就不翻译了主要翻译自动施...

Redis 是如何提供服务的

在刚刚接触Redis的时候,最想要知道的是一个’setnameJhon’命令到达Redis服务器的时候,它是如何返回’OK’的?里面命令处理的流程如何,具体细节怎么样?你一定有问过自己...

lua _G、_VERSION使用

到这里我们已经把lua基础库中的函数介绍完了,除了函数外基础库中还有两个常量,一个是_G,另一个是_VERSION。_G是基础库本身,指向自己,这个变量很有意思,可以无限引用自己,最后得到的还是自己,...

China&#39;s top diplomat to chair third China-Pacific Island countries foreign ministers&#39; meeting

BEIJING,May21(Xinhua)--ChineseForeignMinisterWangYi,alsoamemberofthePoliticalBureau...

移动工作交流工具Lua推出Insights数据分析产品

Lua是一个适用于各种职业人士的移动交流平台,它在今天推出了一项叫做Insights的全新功能。Insights是一个数据平台,客户可以在上面实时看到员工之间的交流情况,并分析这些情况对公司发展的影响...

Redis 7新武器:用Redis Stack实现向量搜索的极限压测

当传统关系型数据库还在为向量相似度搜索的性能挣扎时,Redis7的RedisStack...

Nginx/OpenResty详解,Nginx Lua编程,重定向与内部子请求

重定向与内部子请求Nginx的rewrite指令不仅可以在Nginx内部的server、location之间进行跳转,还可以进行外部链接的重定向。通过ngx_lua模块的Lua函数除了能实现Nginx...