基于数据库的用户认证

在前面我们讲了 Spring Security 基于内存的用户认证,但是实际开发中肯定不能做到基于内存隔这就验证,所以说生产开发中,更多使用的还是基于数据库的动态用户认证

Spring Security 基于数据库的用户验证是实际项目中最常用的认证方式,相比基于内存的验证,它能实现用户数据的持久化存储和动态管理。

实际配置理解基于数据库的用户认证

基于数据库的验证与基于内存的验证核心流程一致,都是通过 Spring Security 的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
-- 用户表
CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL,
`password` varchar(100) NOT NULL,
`enabled` tinyint NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
);

-- 角色表
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`role_name` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
);

-- 用户角色关联表
CREATE TABLE `sys_user_role` (
`user_id` bigint NOT NULL,
`role_id` bigint NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`),
FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`)
);

创建子模块,由于我们需要在数据库进行操作,所以我们需要引入一些新的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- 数据库访问 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

配置配置文件,运行项目确保项目能正常链接数据库且启动成功

1
2
3
4
5
6
7
server.port=8081
server.servlet.context-path=/db-security

spring.datasource.url=jdbc:mysql://localhost:3306/security-demo?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=不给你看
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

接下来就是定义用户实体和角色实体了

用户实体

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
@Entity
@Table(name = "sys_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false, length = 50)
private String username;

@Column(nullable = false, length = 100)
private String password;

@Column(nullable = false)
private boolean enabled = true;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "sys_user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
}

角色实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Entity
@Table(name = "sys_role")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "role_name", nullable = false, length = 50)
private String name;
}

数据访问层,创建 Repository 接口操作数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {

/**
* 根据角色名查找角色
*/
Optional<Role> findByName(String name);

/**
* 检查角色名是否存在
*/
boolean existsByName(String name);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

/**
* 根据用户名查找用户,并预加载角色信息
*/
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username")
Optional<User> findByUsernameWithRoles(@Param("username") String username);

/**
* 根据用户名查找用户
*/
Optional<User> findByUsername(String username);

/**
* 检查用户名是否存在
*/
boolean existsByUsername(String username);
}

实现 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
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
/**
* 用户服务类
*/
@Service
@Transactional
public class UserService {

@Autowired
private UserRepository userRepository;

@Autowired
private RoleRepository roleRepository;

@Autowired
private PasswordEncoder passwordEncoder;

/**
* 创建用户
*/
public User createUser(String username, String password, String... roleNames) {
// 检查用户名是否已存在
if (userRepository.existsByUsername(username)) {
throw new RuntimeException("用户名已存在: " + username);
}

User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true);

// 设置角色
Set<Role> roles = new HashSet<>();
for (String roleName : roleNames) {
Role role = roleRepository.findByName(roleName)
.orElseThrow(() -> new RuntimeException("角色不存在: " + roleName));
roles.add(role);
}
user.setRoles(roles);

return userRepository.save(user);
}

/**
* 根据用户名查找用户
*/
@Transactional(readOnly = true)
public Optional<User> findByUsername(String username) {
return userRepository.findByUsernameWithRoles(username);
}

/**
* 删除用户
*/
public void deleteUser(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在: " + username));
userRepository.delete(user);
}
}

配置 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
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Autowired
private UserDetailsService userDetailsService;

// 配置密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

// 配置认证管理器
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}

// 配置安全规则
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login").permitAll() // 登录页允许匿名访问
.anyRequest().authenticated() // 其他请求需要认证
)
.formLogin(form -> form
.defaultSuccessUrl("/home", true) // 登录成功跳转页
)
.logout(logout -> logout
.logoutSuccessUrl("/login") // 登出成功跳转页
);

return http.build();
}
}

最后编写控制器,控制器由于太多了也没什么特殊的东西就不展示了

我们启动项目,插入一些初始化的角色信息,进行测试,没写前端页面,不展示了

也就是说,大部分内容都是在 Spring Security 的配置类中,配置密码编码器和安全规则,可以实现不同权限对不同页面的控制情况,而且密码在存储到数据库的时候也是加了密的

1
2
3
4
5
6
7
8
9
10
11
12
13
// 授权配置
.authorizeHttpRequests(authz -> authz
// 静态资源允许访问
.requestMatchers("/css/**", "/js/**", "/images/**", "/favicon.ico").permitAll()
// 公共页面允许访问
.requestMatchers("/", "/home", "/login", "/register").permitAll()
// 管理员页面需要ADMIN角色
.requestMatchers("/admin/**").hasRole("ADMIN")
// 用户页面需要USER或ADMIN角色
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
// 其他请求需要认证
.anyRequest().authenticated()
)

然后,我们需要有一个地方,实现UserDetailsService接口,重要的还是loadUserByUsername方法,之后服务的CURD都是正常写的:

1
2
3
4
5
6
@Override
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsernameWithRoles(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
return new CustomUserDetails(user);
}

在 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
    @Service
    public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository; // 数据库访问接口

    @Override
    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
    @Configuration
    @EnableWebSecurity
    @EnableMethodSecurity // 启用方法级别的权限控制
    public class SecurityConfig {

    // 注入自定义的用户服务
    @Autowired
    private CustomUserDetailsService userDetailsService;

    // 1. 配置密码加密器
    @Bean
    public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(); // 推荐使用BCrypt加密
    }

    // 2. 配置认证管理器(处理认证逻辑)
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
    }

    // 3. 配置认证提供者(关联用户服务和密码加密器)
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userDetailsService); // 关联自定义用户服务
    provider.setPasswordEncoder(passwordEncoder()); // 关联密码加密器
    return provider;
    }

    // 4. 配置安全过滤链(定义授权规则和表单登录)
    @Bean
    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();
    }
    }

基于数据库的用户认证流程

可以估计,基于数据库的用户认证流程和基于内存认证差不多

image-20250921193600248

上述是一个比较宏观的,可以参考的认证流程,我们接下来通过代码分析基于数据库的用户认证流程

还记得我们之前说,InMemoryUserDetailsManagerUserDetailsManager接口的一个实现类,那么UserDetailsManage还有一个实现类就是JdbcUserDetailsManager

其实本身UserDetailsManager接口就有关于用户的 CURD 的操作,所以作为它的实现类,JdbcUserDetailsManager本质上是Spring Security 提供的开箱即用的 JDBC(数据库)用户认证与管理实现类,本质上也封装了基于数据库的用户查询、CRUD 操作及用户组权限管理逻辑

它是基于 Spring JDBCTemple 那一套编写的,对这个原生逻辑感兴趣的可以去细看,一般情况下,我们是要重写其中的loadUserByUsername方法的

1
2
3
4
5
6
7
8
9
10
11
public interface UserDetailsManager extends UserDetailsService {
void createUser(UserDetails user);

void updateUser(UserDetails user);

void deleteUser(String username);

void changePassword(String oldPassword, String newPassword);

boolean userExists(String username);
}

UserDetailsManager的父类UserDetailsService,就有着这个最重要的方法

1
2
3
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

所以,我创建了 CustomUserDetailsService,它实现了 UserDetailsService接口,当然你也可以照着InMemoryUserDetailsManager的那一套写,也实现UserDetailsPasswordService,这里面就一个更新密码的方法,最重要的是,在 CustomUserDetailsService 类中重写loadUserByUsername方法就可以,它负责从数据库里拿出来 User对象就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
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();
}

然后我们其实也算是创建了 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),内部逻辑:

    1. 执行默认 SQL(SELECT username, password, enabled FROM users WHERE username = ?)查询用户基本信息;
    2. 执行默认 SQL(SELECT username, authority FROM authorities WHERE username = ?)查询用户权限;
    3. 通过 mapToUser() 方法将 ResultSet 映射为 UserUserDetails 实现类),包含用户名、加密密码、是否可用、权限集合;
    4. 返回 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
    9
    protected 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});
    }
  • 用户组与组权限管理(实现 GroupManager)

    对于复杂系统,“用户组” 是批量管理权限的常用方式(如 “管理员组” 包含 ROLE_ADMIN/ROLE_OPERATE 权限)。JdbcUserDetailsManager 封装了组管理的全流程,依赖 3 张表:

    • groups:存储组信息(id 主键、group_name 组名);
    • group_members:用户与组的关联(group_idusername);
    • group_authorities:组与权限的关联(group_idauthority)。
    方法名 作用 核心逻辑
    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
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
package hbnu.project.databasesecurity.security;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.provisioning.UserDetailsManager;

@Service
public class CustomUserDetailsService implements UserDetailsManager, UserDetailsPasswordService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}

@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}

@Override
public void createUser(UserDetails user) {

}

@Override
public void updateUser(UserDetails user) {

}

@Override
public void deleteUser(String username) {

}

@Override
public void changePassword(String oldPassword, String newPassword) {

}

@Override
public boolean userExists(String username) {
return false;
}
}

然后我们就需要把持久层整合进来了

1
2
// 注入用户数据访问接口
private final UserRepository userRepository;

编写 loadUserByUsername

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
/**
* 根据用户名加载用户信息(Spring Security认证时自动调用)
* @param username 登录时输入的用户名
* @return 封装了用户信息的UserDetails对象
* @throws UsernameNotFoundException 用户不存在时抛出
*/
@Override
@Transactional // 确保关联的角色信息能被正确加载(解决懒加载问题)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 从数据库查询用户(若不存在则抛出异常)
SysUser sysUser = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在:" + username));

// 2. 将数据库角色转换为Spring Security的权限对象(GrantedAuthority)
Collection<? extends GrantedAuthority> authorities = sysUser.getRoles().stream()
// 角色名需以"ROLE_"为前缀(Spring Security的约定)
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());

// 3. 封装为UserDetails对象返回(使用Spring提供的User实现类)
return User.builder()
.username(sysUser.getUsername()) // 用户名
.password(sysUser.getPassword()) // 加密后的密码
.enabled(sysUser.isEnabled()) // 账户是否可用
.accountNonLocked(sysUser.isAccountNonLocked()) // 账户是否未锁定
.accountNonExpired(sysUser.isAccountNonExpired()) // 账户是否未过期
.credentialsNonExpired(sysUser.isCredentialsNonExpired()) // 凭证是否未过期
.authorities(authorities) // 权限集合
.build();
}

之后,实现那些CURD,不写了,直接引用 UserRepository 中的方法就可以了

对了,频繁认证会导致数据库压力,可添加缓存减少查询:

1
2
3
4
5
@Cacheable(value = "userCache", key = "#username") // 基于用户名缓存
@Override
public UserDetails loadUserByUsername(String username) {
// 原查询逻辑...
}

最后,就可以使用了,SecurityConfig中,注入自定义的UserDetailsService

1
2
3
4
5
// 注入自定义的UserDetailsService
private final CustomUserDetailsService userDetailsService;

// 注入密码加密器
private final PasswordEncoder passwordEncoder;

然后在认证提供者中,把自己定义的内容注入进去

1
2
3
4
5
6
7
8
// 配置认证提供者(关联UserDetailsService和密码加密器)
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService); // 关联自定义实现类
provider.setPasswordEncoder(passwordEncoder); // 关联密码加密器
return provider;
}

添加用户功能的实现

我们编写CustomUserDetailsService 中的 loadUserByUsername方法,它继承了UserDetailsService,结合我们上面的 User 相关的 Repository,来实现加载用户的功能

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户名不存在: " + username));

return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.disabled(!user.isEnabled())
.authorities(mapRolesToAuthorities(user.getRoles()))
.build();
}

然后丰富一下我们的UserService类,把注册新用户的功能加进

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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package hbnu.project.databasesecurity.service;

import hbnu.project.databasesecurity.dto.UserRegistrationDto;
import hbnu.project.databasesecurity.entity.Role;
import hbnu.project.databasesecurity.entity.User;
import hbnu.project.databasesecurity.repository.RoleRepository;
import hbnu.project.databasesecurity.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

/**
* 用户业务服务
*/
@Service
@RequiredArgsConstructor
public class UserService {

private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;

/**
* 注册新用户
* @param registrationDto 注册信息
* @return 注册结果
*/
@Transactional
public String registerUser(UserRegistrationDto registrationDto) {
// 验证用户名是否已存在
if (userRepository.existsByUsername(registrationDto.getUsername())) {
return "用户名已存在";
}

// 验证密码是否匹配
if (!registrationDto.isPasswordMatching()) {
return "两次输入的密码不一致";
}

// 创建新用户
User user = new User();
user.setUsername(registrationDto.getUsername());
user.setPassword(passwordEncoder.encode(registrationDto.getPassword()));
user.setEnabled(true);

// 设置默认角色为USER
Set<Role> roles = new HashSet<>();
Optional<Role> userRole = roleRepository.findByName("USER");
if (userRole.isPresent()) {
roles.add(userRole.get());
} else {
// 如果USER角色不存在,创建一个
Role newUserRole = new Role();
newUserRole.setName("USER");
roleRepository.save(newUserRole);
roles.add(newUserRole);
}
user.setRoles(roles);

// 保存用户
userRepository.save(user);
return "注册成功";
}

/**
* 根据用户名查找用户
* @param username 用户名
* @return 用户实体
*/
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}

/**
* 检查用户名是否存在
* @param username 用户名
* @return 是否存在
*/
public boolean existsByUsername(String username) {
return userRepository.existsByUsername(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
/**
* 处理用户注册
*/
@PostMapping("/register")
public String registerUser(@ModelAttribute("user") UserRegistrationDto registrationDto,
RedirectAttributes redirectAttributes) {

// 基本验证
if (registrationDto.getUsername() == null || registrationDto.getUsername().trim().isEmpty()) {
redirectAttributes.addFlashAttribute("error", "用户名不能为空");
return "redirect:/register";
}

if (registrationDto.getPassword() == null || registrationDto.getPassword().trim().isEmpty()) {
redirectAttributes.addFlashAttribute("error", "密码不能为空");
return "redirect:/register";
}

if (registrationDto.getPassword().length() < 4) {
redirectAttributes.addFlashAttribute("error", "密码长度至少4位");
return "redirect:/register";
}

// 注册用户
String result = userService.registerUser(registrationDto);

if ("注册成功".equals(result)) {
redirectAttributes.addFlashAttribute("success", "注册成功,请登录");
return "redirect:/login";
} else {
redirectAttributes.addFlashAttribute("error", result);
return "redirect:/register";
}
}

我们接下来进行注册的测试,可以发现POST请求是成功发送了

image-20250925151840752
image-20250925151529065
image-20250925152613275

哦耶