Spring Security 6 系列之二 - 基于数据库的用户认证和认证原理

news2024/12/17 16:29:45

之所以想写这一系列,是因为之前工作过程中使用Spring Security,但当时基于spring-boot 2.3.x,其默认的Spring Security是5.3.x。之后新项目升级到了spring-boot 3.3.0,结果一看Spring Security也升级为6.3.0,关键是其风格和内部一些关键Filter大改,导致在配置同样功能时,多费了些手脚,因此花费了些时间研究新版本的底层原理,这里将一些学习经验分享给大家。

注意由于框架不同版本改造会有些使用的不同,因此本次系列中使用基本框架是 spring-boo-3.3.0(默认引入的Spring Security是6.3.0),JDK版本使用的是19,所有代码都在spring-security-study项目上:https://github.com/forever1986/spring-security-study.git

目录

  • 1 用户读取的基本原理
  • 2 基于内存的用户配置
  • 3 基于数据库的用户配置
    • 3.1 Spring Security默认的JdbcUserDetailsManager
    • 3.2 自定义基于数据库的用户配置
  • 4 Spring Security认证底层原理
  • 5 密码加密方式PasswordEncoder
    • 5.1 密码加密原理
    • 5.2 DelegatingPasswordEncoder原理
    • 5.3 指定PasswordEncoder

上一章中,我们讲了基本入门,spring-boot如何通过默认配置集成Spring Security,也讲了如何自定义配置用户名和密码。在上一章中留下一个问题,就是用户名密码都是配置在项目里面,但实际项目中,我们都是放在数据库中,那么Spring Security如何配置数据库以及其认证原理是如何的,我们在这一章揭晓。

1 用户读取的基本原理

我们先来了解Spring Security如何读取到我们在yml文件中的数据。

1)我们看一下UserDetailsServiceAutoConfiguration这个类,其中有一个inMemoryUserDetailsManager方法
在这里插入图片描述

从上图中,我们可以看到,方法inMemoryUserDetailsManager注入了一个Bean,是一个InMemoryUserDetailsManager,其数据来自SecurityProperties(这个类在系列一中讲过,是读取yml或生成默认用户名密码的)。也就是说Spring Security默认构建了一个基于内存的用户密码管理类。

2)我们再看看InMemoryUserDetailsManager继承哪些类或者实现哪些接口。
在这里插入图片描述

上图中,我们可以看到InMemoryUserDetailsManager类实现了UserDetailsManager接口,而UserDetailsManager接口继承了UserDetailsService接口。关键点:UserDetailsService接口只有一个loadUserByUsername方法(通过用户名获得UserDetails)。UserDetailsManager接口只是在UserDetailsService接口的基础上增加了对用户的增删改查。
那么可以理解,只需要我们自己实现一个实现UserDetailsService接口或者UserDetailsManager接口的Bean,即可替换原先的配置

3)UserDetails接口,我们看到UserDetailsService接口的loadUserByUsername方法返回一个对象是UserDetails,其也是一个接口。该接口定义了一系列获取用户信息相关的方法
在这里插入图片描述

4)而我们在UserDetailsServiceAutoConfiguration这个类中的inMemoryUserDetailsManager方法看到,使用了一个User类创建了一个用户。这个User类其实就是实现了UserDetails接口

User.withUsername(user.getName())
			.password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
			.roles(StringUtils.toStringArray(roles))
			.build()

5)总结:Spring Security是通过调用UserDetailsService接口的实现类,获得一个UserDetails,里面包括用户信息,可以用于认证的。而Spring Security默认有2个实现类InMemoryUserDetailsManager和JdbcUserDetailsManager,分别对应基于内存的用户认证和基于数据库的用户认证。另外内置一个User类,实现最简单的UserDetails信息。

2 基于内存的用户配置

通过上面对其基本原理的理解,我们知道只需要实现一个UserDetailsService接口的类,就可以替换原来的用户密码配置,下面我们就开始实现。

代码参考lesson02子模块

1)新建一个子模块lesson02,其pom文件引入

<dependencies>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
     <!--Spring Boot 提供的 Security 启动器 -->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
     </dependency>
 </dependencies>

2)我们只需要新增一个名称service的pakage,在下面新增一个类InMemoryUserDetailsServiceImpl,实现UserDetailsService 接口

@Service
public class InMemoryUserDetailsServiceImpl implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return User.withUsername("test")
                .password("{noop}4321")
                .build();
    }
}

注意:需要将该类设置注解@Service,这样就纳入spring的Bean管理,同时也屏蔽原先默认配置的Bean
密码部分:{noop}4321,前面那个{noop}代表未加密密码,为什么要加入这个东西,后面讲解Spring Security认证流程原理在仔细讲解

3)跟lesson01一样,定义一个demo的controller和启动类
4)访问:http://127.0.0.1:8080/demo 我们可以看见,是使用新的test用户名和4321的密码才能登陆成功,原先控制台或者yml配置都失效了。

3 基于数据库的用户配置

3.1 Spring Security默认的JdbcUserDetailsManager

上面分别讲述了读取用户的原理以及基于内存读取用户的方法。那么基于数据库的用户配置才是今天的主菜。我们看到中Spring Security已经有一个JdbcUserDetailsManager实现类,如果我们要使用该类,需要按照其默认的配置创建表,创建表的语句如下位置:

在这里插入图片描述

注意:如果使用默认JdbcUserDetailsManager,可以去看其源码,定义了很多SQL语句,且使用的是传统JdbcTemplate的方式。而在真正项目中,我们一般会引入如mybatis框架,以及用户表会有自己的一些格外信息,所以JdbcUserDetailsManager大部分时候都是不符合我们的要求,因此实际中,我们会自定义自身的用户表以及基于数据库的UserDetailsService

3.2 自定义基于数据库的用户配置

前提条件:基于mysql数据库创建一个数据库,名为spring_security_study,创建用户表t_user

-- spring_security_study.t_user definition
CREATE TABLE `t_user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(100) NOT NULL,
  `password` varchar(100) NOT NULL,
  `email` varchar(100) DEFAULT NULL,
  `phone` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

INSERT INTO spring_security_study.t_user (username, password, email, phone)VALUES('test', '{noop}1234','test@test.com','13788888888');

下面开始说明基于自定义数据库的用户配置

代码参考lesson03子模块

1)新建子模块lesson03,其pom引入以下依赖:(引入mybatis-plus、mysql-connector、druid连接池、lombok)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--Spring Boot 提供的 Security 启动器 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>
</dependencies>

2)在resources下面创建yaml文件,配置数据库连接和mybatis-plus相关配置

server:
  port: 8080
spring:
  # 配置数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/spring_security_study?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: root
    druid:
      initial-size: 5
      min-idle: 5
      maxActive: 20
      maxWait: 3000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: select 'x'
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: false
      filters: stat,wall,slf4j
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;socketTimeout=10000;connectTimeout=1200

mybatis-plus:
  global-config:
    banner: false
  mapper-locations: classpath:mappers/*.xml
  type-aliases-package: com.demo.lesson03.entity
  configuration:
    cache-enabled: false
    local-cache-scope: statement

3)创建package:entity和mapper,分别定义TUser和TUserMapper,读取数据库用户数据。

@Data
public class TUser {

    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    private String username;

    private String password;

    private String email;

    private String phone;
}

TUserMapper定义个通过用户名获取用户的方法

@Mapper
public interface TUserMapper {

    // 根据用户名,查询用户信息
    @Select("select * from t_user where username = #{username}")
    TUser selectByUsername(String username);

}

4)新建package:service,新建一个JdbcUserDetailsServiceImpl,通过mapper获取用户并返回

@Service
public class JdbcUserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private TUserMapper tUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询自己数据库的用户信息
        TUser user = tUserMapper.selectByUsername(username);
        if(user == null){
            throw new UsernameNotFoundException(username);
        }
        return User.builder().username(user.getUsername()).password(user.getPassword()).roles("USER").build();
    }

}

5)和lesson02一样定义一个demo的controller接口和启动类SecurityLesson03Application,并启动
6)访问:http://127.0.0.1:8080/demo 我们可以看见,是使用新的test用户名和1234的密码才能登陆成功,原先控制台或者yml配置都失效了。

4 Spring Security认证底层原理

上面讲解了如何通过内存或者数据库配置登录用户信息,那么Spring Security是如何做用户认证的呢?
首先我们要先了解到Spring Security是一系列的Filter过滤器组成的链路,每个过滤器处理器对应的功能,如果符合则往下走,不符合则返回。关于这部分,我们在下一章中在着重讲。
之前说过Spring Security支持的认证有多种,还可以自定义认证。我们这里以用户名和密码的认证方式为例,讲解一下其主要的原理。我们主要关注Spring Security的认证过滤器(UsernamePasswordAuthenticationFilter)。

1)我们可以debug一下HttpSecurity下面这行代码,里面可以看到Spring Security默认配置定义了哪些Filter过滤器。

在这里插入图片描述
2)我们可以截图看到以下默认配置下有16个过滤器(不同Security版本可能不同,我这6.3.0默认什么都不配置是16),其过滤器的各自用途我们下一章讲。这一章我们主要看UsernamePasswordAuthenticationFilter

在这里插入图片描述
3)UsernamePasswordAuthenticationFilter是继承AbstractAuthenticationProcessingFilter,我们看看AbstractAuthenticationProcessingFilter的doFilter方法,里面有2个关键内容,一个是调用attemptAuthentication方法做认证(这个方法具体实现是在UsernamePasswordAuthenticationFilter),一个successfulAuthentication方法做后续认证成功处理(包括保存SecurityContext,调用securityContextRepository存储session)
在这里插入图片描述

4)我们回到认证流程,刚才说AbstractAuthenticationProcessingFilter调用attemptAuthentication,其实就是调用UsernamePasswordAuthenticationFilter里面的attemptAuthentication方法就是执行关键。看下图解释attemptAuthentication的流程
在这里插入图片描述
5)上图的最后一步验证,其实是调用AuthenticationManager的authenticate方法,AuthenticationManager是一个接口,该接口实现类有几个,Spring Security默认提供ProviderManager
6)ProviderManager的authenticate方法中,真正认证的是result = provider.authenticate(authentication);这一句,而provider是一个叫AuthenticationProvider接口
在这里插入图片描述
7)AuthenticationProvider接口,Spring Security默认提供抽象实现类AbstractUserDetailsAuthenticationProvider,我们关注其authenticate方法,方法里面有2个调用值得我们注意,一个是retrieveUser(这个类获得用户信息)和一个additionalAuthenticationChecks(做用户认证)。
在这里插入图片描述

8)我们可以看到retrieveUser和additionalAuthenticationChecks方法在AbstractUserDetailsAuthenticationProvider只是一个定义,还需要其子类实现,而Spring Security默认提供DaoAuthenticationProvider实现类
在这里插入图片描述

从上图我们就可以理解,为什么我们实现UserDetailsService接口,就能够修改用户信息的来源(比如说数据库),因为在retrieveUser方法中就是调用UserDetailsService接口去获取用户信息。
另外一个就是用户认证additionalAuthenticationChecks方法,通过一个PasswordEncoder去做用户密码匹对。

9)总结:通过UsernamePasswordAuthenticationFilter过滤器做认证,UsernamePasswordAuthenticationFilter调用AuthenticationManager管理器的authenticate方法,而AuthenticationManager是调用AuthenticationProvider的authenticate方法,AuthenticationProvider是通过其实现类DaoAuthenticationProvider提供最终的认证。如下流程图:
在这里插入图片描述

5 密码加密方式PasswordEncoder

5.1 密码加密原理

我们在前面分析认证原理中,DaoAuthenticationProvider的additionalAuthenticationChecks方法就是实现其密码匹配的。我们可以看到里面有一个PasswordEncoder。从名字来看就是密码加密
在这里插入图片描述
Spring Security提供了PasswordEncoder接口,并通过实现该接口内置很多密码加密验证方式,比如不加密、对称加密、非对称加密甚至可以自定义。这里我们着重了解3个实现类NoOpPasswordEncoder、BCryptPasswordEncoder和DelegatingPasswordEncoder
在这里插入图片描述
上图中就是3个常用的内置加密方式,他们分别代表NoOpPasswordEncoder(不加密)、BCryptPasswordEncoder(使用SHA-256 +随机盐+密钥对密码进行加密)、DelegatingPasswordEncoder(代理类,通过其前缀判断不同加密方式)。Spring Security默认情况下,采用的是DelegatingPasswordEncoder,下面我们了解一下DelegatingPasswordEncoder。

5.2 DelegatingPasswordEncoder原理

DelegatingPasswordEncoder可以通过给密码前面增加一个{前缀},同时支持多种密码加密验证,而Spring Security默认的方式就是DelegatingPasswordEncoder。下图就是不同加密方式的前缀。

注意:前面我们在密码之前增加{noop},其实就是采用不加密方式,当然这是非常不安全的,只有在演示中使用

在这里插入图片描述

5.3 指定PasswordEncoder

你可以指定自己的密码加密方式,只需要继承PasswordEncoder接口,并实现其方法即可。比如指定BCryptPasswordEncoder

@Bean
public BCryptPasswordEncoder createPasswordEncoder(){
    return new BCryptPasswordEncoder();
}

在实际项目中,密码一定是要加密,且最好不可逆和难以破解的方式,比如SHA-256。

结语:至此,我们终于将Spring Security的认证底层原理讲了一遍。如果不了解的朋友,可以多debug几次就能够明白其中原理。到目前为止,我们只是配置了用户读取方式,其它的Spring Security配置都还是默认的,我们也可以看到默认情况下,Spring Security就加载了16个过滤器,说明有很多功能还没有讲,那么下一章,就是了解Spring Security底层原理以及常见Filter过滤的作用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2261160.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ubuntu下anconda装pytorch

1、禁用nouveau sudo vim /etc/modprobe.d/blacklist.conf 在文件最后部分插入以下两行内容 blacklist nouveau options nouveau modeset0 更新系统 sudo update-initramfs -u 重启系统 2、装nvidia驱动 卸载原来驱动 sudo apt-get remove nvidia-* &#xff08;若安装…

Pytest-Bdd-Playwright 系列教程(17):标签管理(Tags)

Pytest-Bdd-Playwright 系列教程&#xff08;17&#xff09;&#xff1a;标签管理&#xff08;Tags&#xff09; 前言一、创建Feature文件二、创建步骤定义文件三、pytest.ini 配置文件四、conftest.py文件五、运行测试5.1 运行带有特定标签的测试5.2 运行带有多个标签的测试5.…

基于深度学习的猫狗识别系统【深度学习课设】

&#x1f3c6; 作者简介&#xff1a;席万里 ⚡ 个人网站&#xff1a;https://dahua.bloggo.chat/ ✍️ 一名后端开发小趴菜&#xff0c;同时略懂Vue与React前端技术&#xff0c;也了解一点微信小程序开发。 &#x1f37b; 对计算机充满兴趣&#xff0c;愿意并且希望学习更多的技…

java常见的集合框架

常见的集合框架 JAVA的集合框架可以分成两类。 Collection&#xff0c;主要有List、vector、set、queue List代表有序&#xff0c;可重复的集合&#xff0c;像动态数组ArrayList和链表LinkedList Set代表无序不可重复的集合。像HashSet、TreeSet Queue代表队列&#xff0c;像…

期末速成C++【类和对象】

目录 1.面向对象的编程思想 2.面向对象的三大特征 3.封装 4.类的定义 5.成员函数 6.对象的创建和使用 7.字符串string的使用 8.this指针 9.构造函数VS析构函数 9.1&#x1f387;构造函数 9.1.1无参构造函数&有参构造函数​ 9.1.2构造函数的初始化表 9.1.3重…

牛客周赛 Round 72 <字符串>

<1>小红的01串&#xff08;一&#xff09; #include<stdio.h> #include<string.h> char ch[100]; int main() {scanf("%s",ch);int count0;int lenstrlen(ch);for(int i0;i<len-1;i){if(ch[i]!ch[i1]){count;} }printf("%d",count);…

2024.12.14 TCP/IP 网络模型有哪几层?

2024.12.14 TCP/IP 网络模型有哪几层? 2024.12.14 今天周六 看到大伙都在考六级&#xff0c;我来复盘小林coding的计算机网络的知识点&#xff1a; TCP/IP 网络模型有哪几层? 问大家&#xff0c;为什么要有 TCP/IP 网络模型? 对于同一台设备上的进程间通信&#xff0c;有…

将 Ubuntu 22.04 LTS 升级到 24.04 LTS

Ubuntu 24.04 LTS 将支持 Ubuntu 桌面、Ubuntu 服务器和 Ubuntu Core 5 年&#xff0c;直到 2029 年 4 月。 本文将介绍如何将当前 Ubuntu 22.04 系统升级到最新 Ubuntu 24.04 LTS版本。 备份个人数据 以防万一&#xff0c;把系统中的重要数据自己备份一下~ 安装配置SSH访问…

商业化大前端在性能优化领域的探索与实践

导读&#xff1a;在业务飞速发展的过程中&#xff0c;用户体验是必不可少的一个环节&#xff0c;而页面性能是直接影响用户体验的重要因素。当页面加载时间过长、交互操作不流畅时&#xff0c;意味着业务可能会出现转化率降低、用户流失等业务问题。在过去一年&#xff0c;为了…

Envoy 进阶指南(下):深入探究Envoy服务和架构

接上篇&#xff1a;《Envoy 进阶指南&#xff08;上&#xff09;&#xff1a;从入门到核心功能全掌握》 链接 文章目录 3.深入探究Envoy3.1 Envoy服务发现机制3.1.1文件订阅3.1.2 gRPC 流式订阅3.1.3 REST-JSON 轮询订阅 3.2监听器&#xff08;Listener&#xff09;3.3.架构3.3…

将PDF流使用 canvas 绘制展示在页面上(一)

将PDF流展示在页面上 使用 pdfjs-dist 库来渲染 PDF 页面到 canvas 上进行绘制展示 安装 pdfjs-dist 依赖 npm install pdfjs-dist 或者 yarn add pdfjs-dist创建一个组件来处理 PDF 流的加载和渲染 该组件中是一个包含 PDF 文件的 Base64。 将 pdf 流传入该组件中使用 /** fo…

PCIE概述

PCIE概述 文章目录 PCIE概述前言一、应用场景二、PCIE理论2.1 硬件2.2 拓扑结构&#xff1a;处理器和设备之间的关系2.3 速率2.4 层次接口2.5 四种请求类型2.5.1 bar空间2.5.2 memory2.5.3 IO2.5.4 configuration2.5.5 message 前言 参考链接&#xff1a; pcie总线知识点解析 …

序列模型的使用示例

序列模型的使用示例 1 RNN原理1.1 序列模型的输入输出1.2 循环神经网络&#xff08;RNN&#xff09;1.3 RNN的公式表示2 数据的尺寸 3 PyTorch中查看RNN的参数4 PyTorch中实现RNN&#xff08;1&#xff09;RNN实例化&#xff08;2&#xff09;forward函数&#xff08;3&#xf…

Elasticsearch8.17.0在mac上的安装

1、下载并安装 下载8.17版本es(目前最新版本)&#xff1a;Download Elasticsearch | Elastic 也可以通过历史版本列表页下载&#xff1a;Past Releases of Elastic Stack Software | Elastic 当然也可以指定具体版本号进行下载&#xff1a;Elasticsearch 8.17.0 | Elastic …

【自动控制原理】学习地图

分值分布 选择+填空+判断:50分 大题:50分 概念 控制系统的数学模型 在控制系统的分析和设计中,首先要建立系统的数学模型。控制系统的数学模型是描述系统内部物理量(或变量)之间关系的数学表达式。 在静态条件下(即变量各阶导数为零),描述变量之间关系的代数方程叫静态…

Synchronous Serial Port 协议详解

1、简介 Synchronous Serial Port (SSP) &#xff0c;基于下图文档的设计标准 1.1、包含3种数据帧格式&#xff1a; a Motorola SPI-compatible interface&#xff08;以下简称SPI&#xff09;a Texas Instruments synchronous serial interface&#xff08;简写SSI&#xff…

前端OpenAPI根据后端Swagger自动生成前端接口报错

测试之后发现是因为Map<Long,List<CommentVO>>的返回值类型的锅&#xff0c;改成Page<List<CommentVO>>即可解决。 前端使用的umiMAX的openapi&#xff0c;报错如下&#xff1a; originalRef: BaseResponseboolean\n "401&q…

在线预约陪诊小程序

一、前言 随着社会老龄化加剧以及人们健康意识的提高&#xff0c;就医过程中的陪伴需求日益增长。许多患者在面对复杂的医院环境、繁琐的就医流程时&#xff0c;需要有人协助挂号、候诊、取药等&#xff0c;而家属可能因工作繁忙无法全程陪同。同时&#xff0c;异地就医的患者更…

信号滤波分析-低通分析(Matlab)

Matlab低通滤波 信号滤波分析-低通分析&#xff08;Matlab&#xff09; 【标价是仅源码的价格】 【有课程设计答辩PPT和设计文档报告】 需要或感兴趣可以随时联系博主哦&#xff0c;常在线秒回&#xff01; 低通滤波分析方案的设计包括&#xff1a; 1.信号生成原理 2.低通滤波…

全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之复合运算符

&#xff08;一&#xff09;、复合运算符 在C中&#xff0c;可以通过“赋值语句”来修改变量的值。赋值语句的格式&#xff1a; 变量名 值或者表达式&#xff1b;其中""称为"赋值运算符"。 除此之外&#xff0c;在赋值运算符当中&#xff0c;C有复合赋…