基于数据库的用户认证
在前面我们讲了 Spring Security
基于内存的用户认证,但是实际开发中肯定不能做到基于内存隔这就验证,所以说生产开发中,更多使用的还是基于数据库的动态用户认证
Spring Security 基于数据库的用户验证是实际项目中最常用的认证方式,相比基于内存的验证,它能实现用户数据的持久化存储和动态管理。
实际配置理解基于数据库的用户认证
基于数据库的验证与基于内存的验证核心流程一致,都是通过 Spring
Security
的UserDetailsService
接口加载用户信息,但数据来源从硬编码的内存数据改为数据库。
我们设计数据库表,通常需要两张核心表(用户表和角色表),采用多对多关系:
1 | -- 用户表 |
创建子模块,由于我们需要在数据库进行操作,所以我们需要引入一些新的依赖
1 | <!-- Spring Security --> |
配置配置文件,运行项目确保项目能正常链接数据库且启动成功
1 | server.port=8081 |
接下来就是定义用户实体和角色实体了
用户实体
1 |
|
角色实体
1 |
|
数据访问层,创建 Repository 接口操作数据库
1 |
|
1 |
|
实现
UserDetailsService
,自定义用户服务类,实现从数据库加载用户信息:
1 | /** |
配置 Spring Security,创建配置类,指定认证方式和授权规则:
1 |
|
最后编写控制器,控制器由于太多了也没什么特殊的东西就不展示了
我们启动项目,插入一些初始化的角色信息,进行测试,没写前端页面,不展示了
也就是说,大部分内容都是在 Spring Security 的配置类中,配置密码编码器和安全规则,可以实现不同权限对不同页面的控制情况,而且密码在存储到数据库的时候也是加了密的
1 | // 授权配置 |
然后,我们需要有一个地方,实现UserDetailsService
接口,重要的还是loadUserByUsername
方法,之后服务的CURD都是正常写的:
1 |
|
在 Spring Security
实现基于数据库的用户认证时,及大多数情况下需要自定义UserDetailsService
,因为Spring
Security 的认证流程核心是
“根据用户名获取用户信息”,而UserDetailsService
就是这个流程的标准化接口。而和之前的内存不一样,Spring
Security
不会直接操作数据库,它通过UserDetailsService
接口定义了
“获取用户信息” 的规范,具体的数据库查询逻辑需要开发者自己实现。
主要就是重写loadUserByUsername
方法,它的作用如下
- 接收前端传入的用户名(登录时的用户名)
- 从数据库查询该用户的完整信息(用户名、加密后的密码、角色、权限等)
- 将查询结果封装为
UserDetails
对象返回给 Spring Security - 若用户不存在,抛出
UsernameNotFoundException
那么如何编写基于数据库的用户验证,基于上述我们就已经比较清晰了
首先创建用户实体,对应数据库中的用户表
然后实现
UserDetailsService
接口,编写自定义实现类,封装数据库查询逻辑1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CustomUserDetailsService implements UserDetailsService {
private UserRepository userRepository; // 数据库访问接口
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 从数据库查询用户
SysUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
// 2. 转换角色为Spring Security需要的格式(ROLE_前缀)
Collection<? extends GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
// 3. 封装为UserDetails对象返回
return User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 必须是加密后的密码
.enabled(user.isEnabled())
.authorities(authorities)
.build();
}
}然后编写 Repository 接口(数据库访问)
配置 Spring Security(核心配置类)
一个示例如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 启用方法级别的权限控制
public class SecurityConfig {
// 注入自定义的用户服务
private CustomUserDetailsService userDetailsService;
// 1. 配置密码加密器
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 推荐使用BCrypt加密
}
// 2. 配置认证管理器(处理认证逻辑)
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
// 3. 配置认证提供者(关联用户服务和密码加密器)
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService); // 关联自定义用户服务
provider.setPasswordEncoder(passwordEncoder()); // 关联密码加密器
return provider;
}
// 4. 配置安全过滤链(定义授权规则和表单登录)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置认证提供者
.authenticationProvider(authenticationProvider())
// 关闭CSRF(开发环境可关闭,生产环境需开启)
.csrf(csrf -> csrf.disable())
// 配置授权规则(从小到大的权限范围)
.authorizeHttpRequests(auth -> auth
// 公开资源:登录页、静态资源、注册接口等
.requestMatchers("/login", "/register", "/css/**", "/js/**").permitAll()
// 管理员权限:仅ROLE_ADMIN可访问
.requestMatchers("/admin/**").hasRole("ADMIN")
// 用户权限:ROLE_USER或ROLE_ADMIN可访问
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// 其他所有请求必须认证
.anyRequest().authenticated()
)
// 配置表单登录
.formLogin(form -> form
.loginPage("/login") // 自定义登录页路径
.usernameParameter("username") // 表单用户名参数名
.passwordParameter("password") // 表单密码参数名
.defaultSuccessUrl("/home", true) // 登录成功跳转页
.failureUrl("/login?error") // 登录失败跳转页
.permitAll() // 允许匿名访问登录页
)
// 配置退出登录
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout") // 退出成功跳转页
.invalidateHttpSession(true) // 清除会话
.deleteCookies("JSESSIONID") // 删除会话Cookie
.permitAll()
);
return http.build();
}
}
基于数据库的用户认证流程
可以估计,基于数据库的用户认证流程和基于内存认证差不多

上述是一个比较宏观的,可以参考的认证流程,我们接下来通过代码分析基于数据库的用户认证流程
还记得我们之前说,InMemoryUserDetailsManager
是UserDetailsManager
接口的一个实现类,那么UserDetailsManage
还有一个实现类就是JdbcUserDetailsManager
其实本身UserDetailsManager
接口就有关于用户的 CURD
的操作,所以作为它的实现类,JdbcUserDetailsManager
本质上是Spring
Security 提供的开箱即用的
JDBC(数据库)用户认证与管理实现类,本质上也封装了基于数据库的用户查询、CRUD
操作及用户组权限管理逻辑
它是基于 Spring JDBCTemple
那一套编写的,对这个原生逻辑感兴趣的可以去细看,一般情况下,我们是要重写其中的loadUserByUsername
方法的
1 | public interface UserDetailsManager extends UserDetailsService { |
而UserDetailsManager
的父类UserDetailsService
,就有着这个最重要的方法
1 | public interface UserDetailsService { |
所以,我创建了 CustomUserDetailsService
,它实现了
UserDetailsService
接口,当然你也可以照着InMemoryUserDetailsManager
的那一套写,也实现UserDetailsPasswordService
,这里面就一个更新密码的方法,最重要的是,在
CustomUserDetailsService
类中重写loadUserByUsername
方法就可以,它负责从数据库里拿出来
User对象就行了。
1 |
|
然后我们其实也算是创建了 User 对象,只不过在数据库里,是之前的注册业务创建的,我们只需要到时候拿出来就行了
然后,继续,我们拿出来了用户信息就要进行认证,和在内存中进行认证是一样的,在UsernamePasswordAuthenticationFilter
过滤器中的attemptAuthentication
方法将用户拿出来,进行密码的认证,密码的认证也是把从数据库拿出来的用户信息封装成令牌,和输入的进行比对,具体的过程,和内存认证是一模一样的
简单讲解JdbcUserDetailsManager
再多说几句JdbcUserDetailsManager
,一上来我们可以看到它设定了一大堆的数据库操作的语句和Spring
Temple相关的一大堆对象,我们通过这个了解
Spring Security 默认约定了 5 张核心表(用户、权限、用户组、组成员、组权限),类中提供了操作这些表的默认 SQL:
常量名 | 默认 SQL 作用 | 对应表 |
---|---|---|
DEF_CREATE_USER_SQL |
插入用户(用户名、加密密码、是否可用) | users |
DEF_INSERT_AUTHORITY_SQL |
插入用户权限(用户名、权限标识) | authorities |
DEF_CHANGE_PASSWORD_SQL |
更新用户密码 | users |
DEF_INSERT_GROUP_SQL |
创建用户组(组名) | groups |
DEF_INSERT_GROUP_MEMBER_SQL |
关联用户与组(组 ID、用户名) | group_members |
唉,约定大于配置这招太狠了
类中的属性也是喜闻乐见
DataSource
:数据库连接源(通过构造函数或setDataSource()
注入,是 JDBC 操作的基础);SecurityContextHolderStrategy
:安全上下文策略,用于操作当前用户的认证状态(如修改密码后更新会话);AuthenticationManager
:认证管理器,在 “修改密码” 时验证旧密码的合法性(可选,未注入则不验证旧密码);UserCache
:用户缓存(默认NullUserCache
,即不缓存;可替换为EhCacheUserCache
等提升性能);enableAuthorities
:是否启用权限管理(默认true
,即操作用户时自动同步权限表)。
JdbcUserDetailsManager
的价值在于封装了三类高频操作,我们可以参考进行实现
用户认证(实现
UserDetailsService
)当用户登录时,Spring Security 会调用
loadUserByUsername(String username)
方法(继承自JdbcDaoImpl
),内部逻辑:- 执行默认
SQL(
SELECT username, password, enabled FROM users WHERE username = ?
)查询用户基本信息; - 执行默认
SQL(
SELECT username, authority FROM authorities WHERE username = ?
)查询用户权限; - 通过
mapToUser()
方法将 ResultSet 映射为User
(UserDetails
实现类),包含用户名、加密密码、是否可用、权限集合; - 返回
User
对象给DaoAuthenticationProvider
,用于后续密码比对。
进入到
JdbcDaoImpl
中看看loadUserByUsername(String username)
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57/**
* 根据用户名查询用户详情(包含用户名、密码、权限等)
* @param username 登录用户名
* @return 封装了用户信息的 UserDetails 对象
* @throws UsernameNotFoundException 当用户不存在或无权限时抛出
*/
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 从数据库查询用户基本信息(用户名、密码、是否可用等)
List<UserDetails> users = this.loadUsersByUsername(username);
// 2. 如果查询结果为空,抛出"用户不存在"异常
if (users.size() == 0) {
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"
)
);
}
// 3. 取第一个用户(用户名唯一,理论上只有一条记录)
UserDetails user = (UserDetails)users.get(0);
// 4. 收集用户的所有权限(包括用户自身权限和组权限)
Set<GrantedAuthority> dbAuthsSet = new HashSet<>();
// 4.1 如果启用了用户权限(默认启用),加载用户自身的权限
if (this.enableAuthorities) {
dbAuthsSet.addAll(this.loadUserAuthorities(user.getUsername()));
}
// 4.2 如果启用了组权限(默认禁用),加载用户所属组的权限
if (this.enableGroups) {
dbAuthsSet.addAll(this.loadGroupAuthorities(user.getUsername()));
}
// 5. 将权限集合转为列表,并添加自定义权限(如需扩展)
List<GrantedAuthority> dbAuths = new ArrayList<>(dbAuthsSet);
this.addCustomAuthorities(user.getUsername(), dbAuths); // 空实现,供子类扩展
// 6. 如果用户没有任何权限,抛出异常(避免无权限用户登录)
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"
)
);
}
// 7. 构建并返回完整的 UserDetails 对象(包含用户名、密码、权限等)
return this.createUserDetails(username, user, dbAuths);
}可以继续看到其中的
loadUsersByUsername
方法,约定的这个表是特别固定的,所以说这个东西,大部分不能拿过来直接用1
2
3
4
5
6
7
8
9protected List<UserDetails> loadUsersByUsername(String username) {
RowMapper<UserDetails> mapper = (rs, rowNum) -> {
String username1 = rs.getString(1);
String password = rs.getString(2);
boolean enabled = rs.getBoolean(3);
return new User(username1, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES);
};
return this.getJdbcTemplate().query(this.usersByUsernameQuery, mapper, new Object[]{username});
}- 执行默认
SQL(
用户组与组权限管理(实现 GroupManager)
对于复杂系统,“用户组” 是批量管理权限的常用方式(如 “管理员组” 包含
ROLE_ADMIN
/ROLE_OPERATE
权限)。JdbcUserDetailsManager
封装了组管理的全流程,依赖 3 张表:groups
:存储组信息(id
主键、group_name
组名);group_members
:用户与组的关联(group_id
、username
);group_authorities
:组与权限的关联(group_id
、authority
)。
方法名 作用 核心逻辑 createGroup(String groupName, List<GrantedAuthority> authorities)
创建用户组 1. 插入 groups
表创建组;2. 查询组 ID;3. 插入group_authorities
表绑定组权限addUserToGroup(String username, String groupName)
新增用户到组 1. 查询组 ID;2. 插入 group_members
表关联用户与组;3. 清除用户缓存(更新权限)removeUserFromGroup(String username, String groupName)
移除组内用户 1. 查询组 ID;2. 删除 group_members
表的关联记录;3. 清除用户缓存deleteGroup(String groupName)
删除用户组 1. 查询组 ID;2. 级联删除 group_members
(组成员)、group_authorities
(组权限);3. 删除groups
表的组记录findGroupAuthorities(String groupName)
查询组权限 执行 SQL 查询 group_authorities
表,返回组的所有权限用户的 CURD 操作
太固定了,而且很正常的Temple操作,没什么特别的
再次详细讲解如何实现UserDetailsService
接口
首先实现这两个接口,然后把空方法写出来
1 | package hbnu.project.databasesecurity.security; |
然后我们就需要把持久层整合进来了
1 | // 注入用户数据访问接口 |
编写 loadUserByUsername
1 | /** |
之后,实现那些CURD,不写了,直接引用 UserRepository 中的方法就可以了
对了,频繁认证会导致数据库压力,可添加缓存减少查询:
1 | // 基于用户名缓存 |
最后,就可以使用了,SecurityConfig中,注入自定义的UserDetailsService
1 | // 注入自定义的UserDetailsService |
然后在认证提供者中,把自己定义的内容注入进去
1 | // 配置认证提供者(关联UserDetailsService和密码加密器) |
添加用户功能的实现
我们编写CustomUserDetailsService
中的
loadUserByUsername
方法,它继承了UserDetailsService
,结合我们上面的
User 相关的 Repository,来实现加载用户的功能
1 |
|
然后丰富一下我们的UserService
类,把注册新用户的功能加进
1 | package hbnu.project.databasesecurity.service; |
控制器中编写对应的接口
1 | /** |
我们接下来进行注册的测试,可以发现POST请求是成功发送了



哦耶