Spring Security 用户管理三
wptr33 2024-11-27 21:39 23 浏览
指导 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 实现的查询(例如,身份验证使用新的表结构)。
相关推荐
- VPS主机搭建Ghost环境:Nginx Node.js MariaDB
-
Ghost是一款个人博客系统,它是使用Node.js语言和MySQL数据库开发的,同时支持MySQL、MariaDB、SQLite和PostgreSQL。用户可以在支持Node.js的服务器上使用自己...
- centos7飞速搭建zabbix5.0并添加windows、linux监控
-
一、环境zabbix所在服务器系统为centos7,监控的服务器为windows2016和centos7。二、安装zabbix官方安装帮助页面...
- Zabbix5.0安装部署
-
全盘展示运行状态,减轻运维人员的重复性工作量,提高系统排错速度,加速运维知识学习积累。1.png...
- MariaDB10在CentOS7系统下,迁移数据存储位置
-
背景在CentOS7下如果没有默认安装MySQL数据库,可以选择安装MariaDB,最新的版本现在是10可以选择直接yum默认安装的方式yum-yinstallmariadbyum-yi...
- frappe项目安装过程
-
1,准备一台虚拟机,debian12或者ubuntusever22.04.3可以用virtualbox/qemu,或者你的超融合服务器安装一些常用工具和依赖库我这里选择server模式安装,用tab...
- 最新zabbix一键安装脚本(基于centos8)
-
一、环境准备注意:操作系统必须是centos8及以上的,因为我配的安装源是centos8的。并且必须连接互联网,脚本是基于yum安装的!!!...
- ip地址管理之phpIPAM保姆级安装教程 (原创)
-
本教程基于Ubuntu24.04LTS,安装phpIPAM(最新稳定版1.7),使用Apache、PHP8.3和MariaDB,遵循最佳实践,确保安全性和稳定性。一、环境准备1....
- centos7傻瓜式安装搭建zabbix5.0监控服务器教程
-
zabbix([`zaebiks])是一个基于WEB界面的提供分布式系统监视...
- zabbix7.0LTS 保姆级安装教程 小白也能轻松上手安装
-
系统环境:rockylinux9.4(yumupdate升级到最新版本)数据库:mariadb10.5.22第一步:关闭防火墙和selinux使用脚本关闭...
- ubuntu通过下载安装包安装mariadb10.4
-
要在Ubuntu18.04上安装MariaDB10.4.34,用的是那个tar.gz的安装包。步骤大概是:...
- 从0到1:基于 Linux 快速搭建高可用 MariaDB Galera 集群(实战指南)
-
在企业生产环境中,数据库的高可用性至关重要。今天带你从0到1,手把手在Linux系统上快速搭建一个高可用MariaDBGaleraCluster,实现数据库同步复制、故障自动恢复,保障业务...
- Windows 中安装 MariaDB 数据库
-
mariadb在Windows下的安装非常简单,下载程序双击运行就可以了。需要注意:mariadb和MySQL数据库在Windows下默认是不区分大小写的,但是在Linux下是区分...
- SQL执行顺序(SqlServer)
-
学习SQL这么久,如果突然有人问你SQL的执行顺是怎么样的?是不是很多人会觉得C#、JavaScript都是根据编程顺序来处理的,那么SQL也是根据编程顺序来执行的吗?...
- C# - StreamWriter与StreamReader 读写文件 101
-
读写文本文件的方式:1)File静态类的File.ReadAllLines();与File.WriteAllLines();方法进行读写...
- C#中的数组探究与学习
-
C#中的数组一般分为:...
- 一周热门
-
-
C# 13 和 .NET 9 全知道 :13 使用 ASP.NET Core 构建网站 (1)
-
因果推断Matching方式实现代码 因果推断模型
-
git pull命令使用实例 git pull--rebase
-
git pull 和git fetch 命令分别有什么作用?二者有什么区别?
-
面试官:git pull是哪两个指令的组合?
-
git 执行pull错误如何撤销 git pull fail
-
git fetch 和git pull 的异同 git中fetch和pull的区别
-
git pull 之后本地代码被覆盖 解决方案
-
还可以这样玩?Git基本原理及各种骚操作,涨知识了
-
git命令之pull git.pull
-
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mysql max (33)
- vba instr (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)