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

本系列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>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></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 Configdatasource:type: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://127.0.0.0:3306/study?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=trueusername: rootpassword: 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 {@AutowiredIUserService 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 执行SQLList<UserDetails> users = this.loadUsersByUsername(username);if (users.size() == 0) {// 2. 没有查询到,抛出 UsernameNotFoundExceptionthis.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_idif (this.enableGroups) {dbAuthsSet.addAll(this.loadGroupAuthorities(user.getUsername()));}// Set=》ListList<GrantedAuthority> dbAuths = new ArrayList(dbAuthsSet);this.addCustomAuthorities(user.getUsername(), dbAuths);// 6. 当前用户没有任何权限,也会抛出 UsernameNotFoundExceptionif (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();// 用户的帐户是否未过期。即未过期则返回trueboolean isAccountNonExpired();// 用户是否未锁定。无法对锁定的用户进行身份验证,如果用户未被锁定,则返回trueboolean isAccountNonLocked();// 用户的凭据(密码)是否未过期,即未过期则返回trueboolean isCredentialsNonExpired();// 用户是启用还是禁用,如果启用了用户则返回trueboolean 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;@Overridepublic 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 {/*** 密码器*/@BeanPasswordEncoder 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);}

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

Spring Security 6.x 系列【2】认证篇之使用数据库存储用户相关推荐

  1. Spring Security 实战:使用 JWT 认证访问接口

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 1. 前言 欢迎阅读Spring Security 实战 ...

  2. Spring Security:基于内存的认证信息

    本文来说下Spring Boot+Spring Security:基于内存的认证信息 文章目录 概述 需求缘起 编码思路 基于内存的认证信息 创建一个配置类 密码加密 本文小结 概述 需求缘起 上面我 ...

  3. Spring Security 中使用Keycloak作为认证授权服务器

    Keycloak对流行的Java应用提供了适配器.在系列文章的上一篇我们演示了针对Spring Boot的安全保护,用的就是适配器的一种.Keycloak同样提供Spring Security的适配器 ...

  4. Spring Security实战--(五)认证和鉴权过程

    前面几篇直接讲了Demo,但是可能还是有点混乱,这里再将整个认证过程梳理一下,加深对前面的理解 一.标准的身份验证方案 对一个系统来说,标准的安全身份验证方案应该按如下步骤: 用户使用用户名和密码登录 ...

  5. Spring Security技术栈开发企业级认证与授权-笔记

    Spring Security 慕x网课程笔记:包括内容如下 MySQL Workbench 官方客户端:sql客户端security oauth2 social spring security sp ...

  6. Spring Security总结之如何让认证失败消息自定义在前端页面显示(一)

    采用springboot+thymeleaf 前端登录页面代码 <form action="/doLogin" method="post">用户名: ...

  7. 【Spring Security OAuth2笔记系列】- Spring Social第三方登录 - QQ登录下

    qq登录下 前面把所有的代码组件都弄好了.现在可以开启调试了 在这之前你需要有一个qq互联的应用:也就是为了拿到appid和appSecret:自己去qq互联创建一个应用即可 这里讲下本地怎么调试应用 ...

  8. Spring Security笔记:HTTP Basic 认证

    在第一节 Spring Security笔记:Hello World 的基础上,只要把Spring-Security.xml里改一个位置 1 <http auto-config="tr ...

  9. java实现 mysql 身份认证,java-从Filter中的数据库对用户进行身份验证是一种好习惯吗?...

    我正在为Android App创建Rest API(Spring Boot项目).从数据库对用户进行身份验证的理想方法应该是什么? 1.在控制器类中查询数据库 2.在过滤器类中查询数据库 3.使用Sp ...

最新文章

  1. 交换三个整数的值,并输出
  2. Java黑皮书课后题第6章:6.12(显示字符)使用下面的方法头,编写一个打印字符的方法。编写一个测试程序、打印从‘1‘到‘Z‘的字符,每行打印10个,字符之间使用一个空格字符隔开
  3. 准备树莓派下的模块开发环境
  4. 算法-找出最近点对问题
  5. 精通 RPM 之校验篇_检验篇_检测篇
  6. Linux下编译GDAL
  7. 怎么安装mysql8.0.20_Mysql 8.0.20安装教程
  8. Tachyou alluxio初识
  9. 一款基于jQuery底部带缩略图的焦点图
  10. QPressEvent实现双击ctrl快捷键
  11. 智迈职称计算机一练通破解版,智迈初级会计职称考试软件
  12. 上海电信光猫设置虚拟服务器,你们想要的上海电信光猫桥接+4K IPTV配置流程...
  13. 【Docker】安装-win7
  14. 中国身份证号码验证,支持15,18位
  15. 【python数据结构】多维数组
  16. winows服务器的ftp密码如何修改,windows怎么修改服务器ftp密码
  17. 几种软件打包工具的对比
  18. Redux和react-redux的区别是什么?
  19. 数据结构与算法——迷宫问题
  20. 基于asp.net校园二手物品交易平台-计算机毕业设计

热门文章

  1. 祖安服务器位置,光明日报:不能坐视粗鄙的祖安文化侵蚀校园
  2. 测试视频软件支持的格式,检测任何格式的视频编码信息(MediaInfo)
  3. 基于内存,redis,mysql的高速游戏数据服务器设计架构
  4. AI制作金属字感的立体文字效果
  5. 郑建新 计算机 山西大学商务学院官网,迎新第一天 | 这是送给2020级新商院人的独家记忆...
  6. RxSwift Subjects: ReplaySubject replaySubject BehaviorSub map
  7. 死锁如何定位,修复?及其预防?
  8. 安卓投屏大师_OPPO手机怎么把影像投屏到电视上?想不到那么简单,涨知识了...
  9. git命令之新建仓库
  10. LayUI table 刷新页面不重置页码