Spring Security 6.0系列【2】认证篇之使用数据库存储用户

有道无术,术尚可求,有术无道,止于术。

本系列Spring Boot 版本 3.0.4

本系列Spring Security 版本 6.0.2

源码地址:https://gitee.com/pearl-organization/study-spring-security-demo

文章目录

    • 前言
    • 1. 环境搭建
      • 1.1 创建用户表
      • 1.2 集成 Mybatis Plus
      • 1.3 生成代码
      • 1.4 测试
    • 2. 用户登录
      • 2.1 UserDetailsService 接口
      • 2.2 UserDetails 接口
      • 2.3 接口实现
      • 2.4 添加配置类
      • 2.5 测试

前言

用户进行认证,最常见的认证方式就是用户名+密码,认证服务需要根据用户名从存储中查询用户信息,然后判断输入的密码和存储中的密码是否匹配。

对用户名、密码存储,Spring Security支持多种存储机制:

  • 内存

  • JDBC关系型数据库

  • 使用 UserDetailsService的自定义数据存储

  • 使用LDAP认证的LDAP存储

本篇文档主要学习使用数据库存储用户信息。

1. 环境搭建

1.1 创建用户表

创建数据库并执行源码地址中的SQL脚本:
在这里插入图片描述

1.2 集成 Mybatis Plus

MyBatis-Plus官网

引入Mybatis PlusMysql驱动、开发工具包:

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.21</version>
        </dependency>

配置数据源:

spring:
# DataSource Config
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.0:3306/study?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true
    username: root
    password: root

启动类上添加@MapperScan扫描:

@MapperScan("com.pearl.security.auth.mapper")

1.3 生成代码

使用Mybatis Plus代码生成器生成各层代码。

首先引入代码生成器和模板引擎:

        <!--代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>

添加生成工具类,修改一些数据库地址、包名等参数:

public class AutoGeneratorUtils {

    public static void main(String[] args) {
        String encode = new BCryptPasswordEncoder().encode("123456");
        System.out.println(encode);
        FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3306/study", "root", "123456")
                .globalConfig(builder -> {
                    builder.author("pearl") // 设置作者
                            .fileOverride() // 覆盖已生成文件
                            .outputDir("D://"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent("com.pearl.security") // 设置父包名
                            .moduleName("auth") // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.xml, "D://")); // 设置mapperXml生成路径
                })
                .strategyConfig(builder -> {
                    builder.addInclude("user") // 设置需要生成的表名
                            .addTablePrefix("t_", "c_"); // 设置过滤表前缀
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }
}

运行并将生成的代码复制到项目中:
在这里插入图片描述

1.4 测试

在测试类中添加测试代码,查验环境是否搭建成功:

@SpringBootTest
class StudySpringSecurityAuthDemoApplicationTests {

    @Autowired
    IUserService userService;

    @Test
    @DisplayName("根据用户名查询用户")
    void testMp() {
        User admin = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getUserName, "admin"));
        System.out.println(admin);
    }
}

2. 用户登录

2.1 UserDetailsService 接口

在这里插入图片描述
首先我们需要从数据库中获取用户,Spring Security提供了UserDetailsService接口查询用户数据。

该接口中,只声明了一个根据用户名加载用户信息的方法:

public interface UserDetailsService {

	/**
	 * @param username 用户的用户名
	 * @return 返回用户信息
	 * @throws UsernameNotFoundException 找不到当前用户异常
	 */
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}

Spring Security默认提供了几个实现类:
在这里插入图片描述
从类名称已经比较好理解,支持内存、数据库查询用户。首先我们看下JdbcDaoImpl是如何查询用户的,是不是满足我们的业务要求。

查看其loadUserByUsername方法执行逻辑:

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    	// select username,password,enabled from users where username = ?
    	// 1. JdbcTemplate 执行SQL
        List<UserDetails> users = this.loadUsersByUsername(username);
        if (users.size() == 0) {
        	// 2. 没有查询到,抛出 UsernameNotFoundException
            this.logger.debug("Query returned no results for user '" + username + "'");
            throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.notFound", new Object[]{username}, "Username {0} not found"));
        } else {
        	// 3. 查询多条,取第一条数据
            UserDetails user = (UserDetails)users.get(0);
            Set<GrantedAuthority> dbAuthsSet = new HashSet(); // 存放用户授予的权限
            // 4. 开启了查询权限,执行SQL:select username,authority from authorities where username = ?
            // 将查询到的结果放入集合中
            if (this.enableAuthorities) {
                dbAuthsSet.addAll(this.loadUserAuthorities(user.getUsername()));
            }
			// 5. 开启了权限分组,=》select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id
            if (this.enableGroups) {
                dbAuthsSet.addAll(this.loadGroupAuthorities(user.getUsername()));
            }
			// Set=》List
            List<GrantedAuthority> dbAuths = new ArrayList(dbAuthsSet);
            this.addCustomAuthorities(user.getUsername(), dbAuths);
            // 6. 当前用户没有任何权限,也会抛出 UsernameNotFoundException
            if (dbAuths.size() == 0) {
                this.logger.debug("User '" + username + "' has no authorities and will be treated as 'not found'");
                throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.noAuthority", new Object[]{username}, "User {0} has no GrantedAuthority"));
            } else {
            	// 7. 创建UserDetails 类型的用户对象并返回 
                return this.createUserDetails(username, user, dbAuths);
            }
        }
    }

通过以上分析可知,JdbcDaoImpl中的SQL都是固定的,而且为了更好的扩展,我们可以仿照其逻辑自定义实现UserDetailsService接口。

2.2 UserDetails 接口

UserDetailsService接口需要返回一个UserDetails 类型的对象,从名称上也很好理解,就是一个封装了用户信息的类。我们需要将我们查询出来的用户对象,转为Spring Security中支持的用户对象,以便框架进行校验、存储。

UserDetails 接口源码如下:

public interface UserDetails extends Serializable {
	// 授权信息集合
    Collection<? extends GrantedAuthority> getAuthorities();
	// 获取密码
    String getPassword();
	// 获取用户名
    String getUsername();
	// 用户的帐户是否未过期。即未过期则返回true
    boolean isAccountNonExpired();
	// 用户是否未锁定。无法对锁定的用户进行身份验证,如果用户未被锁定,则返回true
    boolean isAccountNonLocked();
	// 用户的凭据(密码)是否未过期,即未过期则返回true
    boolean isCredentialsNonExpired();
	// 用户是启用还是禁用,如果启用了用户则返回true
    boolean isEnabled();
}

Spring Security默认提供了一个实现类User
在这里插入图片描述
目前来说,框架提供的User类,已经够用,但是本着可能需要扩展的情况,我们也需要自定义实现UserDetails 接口。

2.3 接口实现

首先实现UserDetails接口,代码如下:

@Data
public class PearlUserDetails implements UserDetails {
    private String password;
    private final String username;
    private final String phone; // 扩展字段,手机号放入用户信息中
    private final Set<GrantedAuthority> authorities;
    private final boolean accountNonExpired;
    private final boolean accountNonLocked;
    private final boolean credentialsNonExpired;
    private final boolean enabled;

    public PearlUserDetails( String username,String password, String phone, List<GrantedAuthority> authorities, boolean accountNonExpired, boolean accountNonLocked, boolean credentialsNonExpired, boolean enabled) {
        this.password = password;
        this.phone = phone;
        this.username = username;
        this.accountNonExpired = accountNonExpired;
        this.accountNonLocked = accountNonLocked;
        this.credentialsNonExpired = credentialsNonExpired;
        this.enabled = enabled;
        this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities)); // 非空判断+排序
    }

    private static SortedSet<GrantedAuthority> sortAuthorities(Collection<? extends GrantedAuthority> authorities) {
        Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
        SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet(new PearlUserDetails.AuthorityComparator());
        for (GrantedAuthority grantedAuthority : authorities) {
            Assert.notNull(grantedAuthority, "GrantedAuthority list cannot contain any null elements");
            sortedAuthorities.add(grantedAuthority);
        }
        return sortedAuthorities;
    }

    private static class AuthorityComparator implements Comparator<GrantedAuthority>, Serializable {
        private static final long serialVersionUID = 600L;
        public int compare(GrantedAuthority g1, GrantedAuthority g2) {
            if (g2.getAuthority() == null) {
                return -1;
            } else {
                return g1.getAuthority() == null ? 1 : g1.getAuthority().compareTo(g2.getAuthority());
            }
        }
    }
}

然后实现UserDetailsService接口,代码如下:

@Slf4j
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final IUserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 数据库查询用户
        User user = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getUserName, username));
        if (ObjectUtil.isNull(user)) {
            log.error("Query returned no results for user '" + username + "'");
            throw new UsernameNotFoundException(StrUtil.format("Username {} not found", username));
        } else {
            // 2. 设置权限集合,后续需要数据库查询(授权篇讲解)
            List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
            // 3. 返回UserDetails类型用户
            return new PearlUserDetails(username, user.getPassword(), user.getPhone(), authorityList,
                    true, true, true, true); // 账号状态这里都直接设置为启用,实际业务可以存在数据库中
        }
    }
}

2.4 添加配置类

Spring Security 6.0和之前的配置有些区别,后续会详细解读。

添加配置类,注入一个密码编码器:

@Configuration
// 开启 Spring Security,debug:是否开启Debug模式
@EnableWebSecurity(debug = false)
public class PearlWebSecurityConfig {

    /**
     * 密码器
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

2.5 测试

在测试类中,插入一条用户数据:

    @Test
    @DisplayName("插入一条用户数据")
    void insertUserTest() {
        User user = new User();
        user.setUserName("admin");
        user.setPassword(new BCryptPasswordEncoder().encode("123456"));
        user.setLoginName("管理员");
        user.setPhone("13688888888");
        userService.save(user);
    }

访问首页,使用数据库中的用户、密码登录,集成完毕。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Oracle用户密码过期,修改永不过期

修改密码有效过期时间&#xff0c;可以通过以下四步设置&#xff0c;如果再第一步发现本身的密码过期时间为无限期的&#xff0c;那就请各位小伙伴绕过&#xff0c;如果发现不是无期限的&#xff0c;那么必须设置第四步&#xff0c;才会生效。 目录 第一步&#xff1a;查询密码…

实验三 数据更新及视图

实验三 数据更新及视图 1.实验目的 1.加深对数据库相关性质的理解&#xff1b; 2.各种约束性理解&#xff1b; 3.学会数据库中数据的更新的方法&#xff1b; 4.学会视图的创建与查询。 2.实验内容 对已建好的各表输入适当的数据并练习数据的插入、删除和修改&#xff0c;注意…

瑞吉外卖项目Day2———完善登录问题、员工功能

创建过滤器类(filter) package com.study.filter;import com.alibaba.fastjson.JSON; import com.study.common.R; import lombok.extern.slf4j.Slf4j; import org.springframework.util.AntPathMatcher;import javax.servlet.*; import javax.servlet.annotation.WebFilter; …

华为OD机试题,用 Java 解【统计匹配的二元组个数】问题 | 含解题说明

华为Od必看系列 华为OD机试 全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典本篇题目:统计匹配的二元组个数 题目 给…

串口,IIC,SPI,USB等总线叙述

串口&#xff0c;IIC&#xff0c;SPI&#xff0c;USB等总线叙述 文章目录串口&#xff0c;IIC&#xff0c;SPI&#xff0c;USB等总线叙述1 串口2.I2C3.SPI4.USB控制&#xff08;Control&#xff09;传输方式同步&#xff08;Isochronous&#xff09;传输方式中断&#xff08;In…

在等GPT-5多模态?试试Genmo!Adobe AI首轮内测报告;ChatGPT三条使用哲学与实践;论文追更与阅读神器 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f916; 『微软 New Bing 引入广告』日活跃用户超1亿&#xff0c;探索广告投放的未来 微软正在尝试在 New Bing 的聊天回复中投放广告。虽然广…

【11】Activity的生命周期

其实Android是使用任务(task)来管理Activity的&#xff0c;一个任务就是一组存放在栈里的Activity 的集合&#xff0c;这个栈也被称作返回栈(back stack)。栈是一种后进先出的数据结构&#xff0c;在默认情况 下&#xff0c;每当我们启动了一个新的Activity&#xff0c;它就会在…

【机器学习】03-转换器和预估器、K-近邻算法、朴素贝叶斯算法、决策树等算法知识

分类算法 一、sklearn转换器和预估器 1 转换器 - 特征工程的父类 fit_transform()fit() 计算 每一列的平均值、标准差transform() (x - mean) / std进行最终的转换 2 估计器(sklearn机器学习算法的实现) 估计器(estimator)–一类实现算法的API 实例化一个estimatorestimat…

剑指offer JZ23 链表中环的入口结点

Java JZ23 链表中环的入口结点 文章目录Java JZ23 链表中环的入口结点一、题目描述二、hash法&#xff0c;记录第一次重复的结点三、快慢指针法使用hash法和快慢指针法解决剑指offer 第JZ23 链表中环的入口结点的问题。 一、题目描述 给一个长度为n链表&#xff0c;若其中包含环…

【新】(2023Q2模拟题JAVA)华为OD机试 - 寻找链表的中间结点

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为od机试,独家整理 已参加机试人员的实战技巧本篇题解:寻找链表的中间结点 题目 给…

利用自动化平台可以做的那亿点事 |得物技术

前言 相信大家对接口自动化已经不陌生了&#xff0c;这是几乎我们每个迭代都会投入的事情&#xff0c;但耗费了这么多精力去编写和维护&#xff0c;实际的收益如何呢&#xff1f;如果收益不好&#xff0c;是不是说明我们自动化 case 的实现方式、使用方式还有改进的地方呢&…

第09章_子查询

第09章_子查询 &#x1f3e0;个人主页&#xff1a;shark-Gao &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是shark-Gao&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f389;目前状况&#xff1a;23届毕业生&#xff0c;目前在某公…

【ABAP】ME55双击跳转MD04增强

最近收到了一个需求&#xff0c;大致的要求是在标准报表ME55的ALV短文本列双击后跳转到MD04的详情。刚开始没有找到增强点想用间接的办法实现&#xff0c;在ME55上增加一列&#xff0c;展示想看到的内容&#xff0c;最后由于需要展示的内容太多&#xff0c;该方案被舍弃。 经过…

深度学习实战19(进阶版)-SpeakGPT的本地实现部署测试,基于ChatGPT在自己的平台实现SpeakGPT功能

大家好&#xff0c;我是微学AI&#xff0c;今天给大家带来SpeakGPT的本地实现&#xff0c;在自己的网页部署&#xff0c;可随时随地通过语音进行问答&#xff0c;本项目项目是基于ChatGPT的语音版&#xff0c;我称之为SpeakGPT。 ChatGPT最近大火&#xff0c;其实在去年12月份…

SpringBoot @Transactional事务详解

事务用处及作用 事务主要是保证数据统一、一致的一种操作。 详细的一些专用术语在此这里不会说太多&#xff0c;如需了解自行百度了&#xff08;还不是枯燥乏味&#xff09;&#xff0c;大致就是这意思。 事务用处 比如坤坤&#xff0c;坤坤拿着100元去买鸡&#xff0c;一个…

JAVA ---程序流程

&#xff08;一&#xff09;引言 在生活中&#xff0c;我们经常会发现在医院或者官方机构办事是要走流程的&#xff0c;同样的程序必须能操控自己的世界&#xff0c;在执行过程中作出判断与选择。在Java中&#xff0c;通过流程控制语句可实现程序执行流程的随意控制&#xff0…

C#中使用I/O文件流

流&#xff0c;即是二进制数值&#xff0c;文件和流 I/O&#xff08;输入/输出&#xff09;是指在存储媒介中传入或传出数据。 在 .NET 中&#xff0c;System.IO 命名空间包含允许以异步方式和同步方式对数据流和文件进行读取和写入操作的类型。 这些命名空间还包含对文件执行压…

Android开发 Intent

1. Intent 在组件之间传递信息&#xff0c;一般需要设置发送方&#xff0c;接收方和数据。 下图是Intent 的常用属性&#xff1a; 2. Intent分类 1&#xff09;显式Intent&#xff1a;精确匹配发送方和接收方 方法一&#xff1a; startActivity(new Intent(this,MainActiv…

USB抓包分析

1、USB传输协议基本概念 一个传输(控制、批量、中断、等时)&#xff1a;由多个事务transaction组成&#xff1b; 一个事务transaction (IN、OUT、SETUP)&#xff1a;由一多个包Packet组成。USB数据在主机与usb设备间被传输&#xff0c;之间的关联叫做管道pipe。一个USB设备可以…

图片转字符画

目录一、字符画二、制作方式一、字符画 字符画&#xff1a;用字符填充创作的人物或动物图片&#xff0c;就像下面这样&#xff1a; 二、制作方式 1.使用Ps的文字工具和蒙版工具来实现 可以看下YouTube上这个教程视频&#xff1a;Photoshop CS6 Tutorial: How to Make an Edi…