1. 为什么需要自定义数据库用户认证
在企业级应用开发中,用户认证是系统安全的第一道防线。Spring Security作为Java生态中最成熟的安全框架,默认提供了基于内存的用户认证方案,但这在实际生产环境中远远不够。我们需要将用户信息存储在关系型数据库中,并与业务数据保持同步。
MyBatis-Plus作为MyBatis的增强工具,提供了强大的单表CRUD操作能力。当它与Spring Security结合时,可以构建出既安全又高效的认证体系。这种组合特别适合以下场景:
- 用户量超过1000的中大型系统
- 需要频繁变更用户权限的业务场景
- 要求细粒度权限控制的系统(如SaaS平台)
- 需要与现有用户表集成的遗留系统改造
提示:在Spring Security 5.7+版本中,官方推荐使用新的Lambda DSL配置方式,相比传统的继承WebSecurityConfigurerAdapter方式更加灵活。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与基础配置
2.1 依赖管理
首先确保你的pom.xml中包含必要的依赖:
xml复制<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
2.2 数据库表设计
用户认证至少需要两张核心表:
sql复制CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '密码',
`enabled` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否启用',
`account_non_expired` tinyint(1) NOT NULL DEFAULT '1' COMMENT '账号是否未过期',
`account_non_locked` tinyint(1) NOT NULL DEFAULT '1' COMMENT '账号是否未锁定',
`credentials_non_expired` tinyint(1) NOT NULL DEFAULT '1' COMMENT '凭证是否未过期',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '角色名称',
`code` varchar(50) NOT NULL COMMENT '角色编码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `sys_user_role` (
`user_id` bigint NOT NULL,
`role_id` bigint NOT NULL,
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
注意:密码字段长度建议设为100,为BCrypt加密后的结果预留足够空间。enabled和四个non_expired字段是Spring Security用户状态检查的关键字段。
3. 核心实现逻辑
3.1 用户实体与Mapper
使用MyBatis-Plus的注解简化实体类定义:
java复制@Data
@TableName("sys_user")
public class User implements UserDetails {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
@TableField(exist = false)
private List<Role> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getCode()))
.collect(Collectors.toList());
}
// 其他UserDetails接口方法实现...
}
@Data
@TableName("sys_role")
public class Role {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private String code;
}
对应的Mapper接口:
java复制public interface UserMapper extends BaseMapper<User> {
@Select("SELECT r.* FROM sys_role r " +
"JOIN sys_user_role ur ON r.id = ur.role_id " +
"WHERE ur.user_id = #{userId}")
List<Role> selectRolesByUserId(Long userId);
}
3.2 自定义UserDetailsService
实现从数据库加载用户的核心逻辑:
java复制@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户基础信息
User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
.eq(User::getUsername, username));
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 查询用户角色
List<Role> roles = userMapper.selectRolesByUserId(user.getId());
user.setRoles(roles);
return user;
}
}
3.3 安全配置类
使用最新的Lambda DSL配置方式:
java复制@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.userDetailsService(userDetailsService)
.formLogin(form -> form
.loginProcessingUrl("/api/login")
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/api/logout")
.logoutSuccessHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":200,\"message\":\"登出成功\"}");
})
)
.csrf().disable();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
4. 进阶优化与实战技巧
4.1 密码加密策略
Spring Security推荐使用BCryptPasswordEncoder,它自动处理salt的生成和存储:
java复制@SpringBootTest
public class PasswordEncoderTests {
@Autowired
private PasswordEncoder passwordEncoder;
@Test
void testEncode() {
String rawPassword = "123456";
String encodedPassword = passwordEncoder.encode(rawPassword);
System.out.println("加密后密码:" + encodedPassword);
// 验证密码
assertTrue(passwordEncoder.matches(rawPassword, encodedPassword));
}
}
重要提示:切勿使用MD5或SHA-1等弱加密算法。BCrypt的强度因子建议设置为10-12,在安全性和性能间取得平衡。
4.2 权限缓存优化
频繁查询数据库会影响性能,可以引入Redis缓存:
java复制@Service
@RequiredArgsConstructor
public class CachedUserDetailsService implements UserDetailsService {
private final UserDetailsServiceImpl delegate;
private final RedisTemplate<String, Object> redisTemplate;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String key = "user:details:" + username;
UserDetails userDetails = (UserDetails) redisTemplate.opsForValue().get(key);
if (userDetails == null) {
userDetails = delegate.loadUserByUsername(username);
redisTemplate.opsForValue().set(key, userDetails, 30, TimeUnit.MINUTES);
}
return userDetails;
}
}
4.3 登录失败处理
自定义认证失败处理器:
java复制@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
String message;
if (exception instanceof BadCredentialsException) {
message = "用户名或密码错误";
} else if (exception instanceof DisabledException) {
message = "账户已被禁用";
} else {
message = "登录失败";
}
response.getWriter().write("{\"code\":401,\"message\":\"" + message + "\"}");
}
}
然后在安全配置中注册:
java复制.httpBasic().disable()
.formLogin(form -> form
.loginProcessingUrl("/api/login")
.successHandler(loginSuccessHandler)
.failureHandler(authenticationFailureHandler)
.permitAll()
)
5. 常见问题排查
5.1 角色前缀问题
Spring Security默认要求角色名称以"ROLE_"开头。如果数据库中存储的是"ADMIN",需要在代码中处理:
java复制// 正确方式
new SimpleGrantedAuthority("ROLE_" + role.getCode());
// 错误方式 - 会导致权限校验失败
new SimpleGrantedAuthority(role.getCode());
5.2 事务管理问题
UserDetailsService中如果涉及多个查询,需要添加事务注解:
java复制@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) {
// ...
}
5.3 循环依赖问题
如果User实体中包含角色集合,而角色又需要查询用户信息,会导致JSON序列化时无限循环。解决方案:
java复制@Data
@TableName("sys_user")
public class User implements UserDetails {
// ...
@TableField(exist = false)
@JsonIgnoreProperties("users")
private List<Role> roles;
}
@Data
@TableName("sys_role")
public class Role {
// ...
@TableField(exist = false)
@JsonIgnoreProperties("roles")
private List<User> users;
}
5.4 MyBatis-Plus查询优化
对于N+1查询问题,可以使用MyBatis-Plus的@TableField注解配合自定义SQL:
java复制@Data
@TableName("sys_user")
public class User implements UserDetails {
// ...
@TableField(exist = false)
@TableField(select = false)
private List<Role> roles;
}
然后在Mapper中定义联合查询:
java复制@Select("SELECT u.*, r.id as role_id, r.name as role_name, r.code as role_code " +
"FROM sys_user u " +
"LEFT JOIN sys_user_role ur ON u.id = ur.user_id " +
"LEFT JOIN sys_role r ON ur.role_id = r.id " +
"WHERE u.username = #{username}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "roles", column = "role_id",
many = @Many(select = "com.example.mapper.RoleMapper.selectById"))
})
User selectUserWithRoles(String username);
在实际项目中,我发现当用户角色数量超过20个时,这种联合查询方式比分开查询效率更高。但要注意结果映射的复杂性会增加。
