赞
踩
有道无术,术尚可求,有术无道,止于术。
本系列Spring Boot 版本 3.0.4
本系列Spring Security 版本 6.0.2
源码地址:https://gitee.com/pearl-organization/study-spring-security-demo
用户进行认证,最常见的认证方式就是用户名+密码,认证服务需要根据用户名从存储中查询用户信息,然后判断输入的密码和存储中的密码是否匹配。
对用户名、密码存储,Spring Security
支持多种存储机制:
内存
JDBC
关系型数据库
使用 UserDetailsService
的自定义数据存储
使用LDAP
认证的LDAP
存储
本篇文档主要学习使用数据库存储用户信息。
创建数据库并执行源码地址中的SQL
脚本:
引入Mybatis Plus
、Mysql
驱动、开发工具包:
<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 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")
使用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(); } }
运行并将生成的代码复制到项目中:
在测试类中添加测试代码,查验环境是否搭建成功:
@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);
}
}
首先我们需要从数据库中获取用户,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
接口。
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
接口。
首先实现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); // 账号状态这里都直接设置为启用,实际业务可以存在数据库中 } } }
Spring Security 6.0
和之前的配置有些区别,后续会详细解读。
添加配置类,注入一个密码编码器:
@Configuration
// 开启 Spring Security,debug:是否开启Debug模式
@EnableWebSecurity(debug = false)
public class PearlWebSecurityConfig {
/**
* 密码器
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
在测试类中,插入一条用户数据:
@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);
}
访问首页,使用数据库中的用户、密码登录,集成完毕。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。