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

本系列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 Plus1.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 Plus、Mysql驱动、开发工具包:

com.baomidou

mybatis-plus-boot-starter

3.5.3.1

com.mysql

mysql-connector-j

runtime

org.projectlombok

lombok

cn.hutool

hutool-all

5.7.21

配置数据源:

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代码生成器生成各层代码。

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

com.baomidou

mybatis-plus-generator

3.5.2

org.freemarker

freemarker

2.3.31

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

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().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 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 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 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 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 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 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 sortAuthorities(Collection authorities) {

Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");

SortedSet 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, 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().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 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);

}

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

参考阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: