怎么让 Spring Security 从数据库加载用户?自定义 UserDetailsService 和 UserDetails 怎么做?
简化版
Spring Security 默认的用户是「内存里配置的」(或默认的 user/随机密码),实际项目要「从数据库加载用户」——扩展点就是 UserDetailsService 和 UserDetails。核心两步:① 实现 UserDetailsService——它只有一个方法 loadUserByUsername(String username),你在里面从数据库查用户(按用户名/手机号/邮箱),返回一个 UserDetails 对象;找不到就抛 UsernameNotFoundException;② 提供 UserDetails——它是 Spring Security 认识的「用户模型」,包含用户名、密码(加密后的)、权限列表、账号状态(是否启用/锁定/过期);你可以用 Spring 自带的 User 类,或让自己的实体实现 UserDetails 接口。认证流程:用户登录时,DaoAuthenticationProvider 调你的 UserDetailsService.loadUserByUsername 拿到 UserDetails(含数据库里存的加密密码),再用 PasswordEncoder 把「用户输入的密码」和「数据库里的加密密码」比对,一致就认证通过。关键:UserDetailsService 负责「用户从哪来(数据库加载)」,PasswordEncoder 负责「密码怎么比对」,两者配合完成数据库认证。
详细版
两个核心接口:
| 接口 | 作用 |
|---|---|
| UserDetailsService | 加载用户:loadUserByUsername(username) → UserDetails |
| UserDetails | 用户模型:用户名、密码、权限、账号状态 |
| PasswordEncoder | 密码编码/比对(存加密密码、比对时 matches) |
// ① 自定义 UserDetailsService(从数据库加载用户)
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) {
// 从数据库查用户
User user = userRepository.findByUsername(username);
if (user == null)
throw new UsernameNotFoundException("用户不存在: " + username);
// 转成 Spring Security 的 UserDetails
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 数据库里存的是加密密码
.authorities(getAuthorities(user)) // 用户的权限/角色
.disabled(!user.isEnabled()) // 账号状态
.build();
}
}
// ② PasswordEncoder(密码加密与比对)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // BCrypt 加密(注册时加密存库,登录时比对)
}
// ③ 让自己的实体实现 UserDetails(另一种方式)
public class MyUser implements UserDetails {
// 实现 getUsername/getPassword/getAuthorities/isEnabled 等
}
⚠️ 数据库认证有个必须理解的配合:
UserDetailsService(加载用户,拿到数据库里的加密密码)+PasswordEncoder(比对密码)——两者缺一不可,且密码「存的是加密后的」。流程是:注册时用PasswordEncoder.encode(明文密码)得到加密密码存数据库(绝不存明文);登录时UserDetailsService从数据库加载UserDetails(含加密密码),DaoAuthenticationProvider用PasswordEncoder.matches(用户输入的明文, UserDetails里的加密密码)比对。关键点:UserDetailsService只管「加载用户」,它不做密码比对——密码比对是DaoAuthenticationProvider用PasswordEncoder做的。所以你的loadUserByUsername里只需返回带加密密码的UserDetails,不要自己比对密码(自己比对是常见错误)。另外,密码加密方式(BCrypt 等)要和存储时一致——Spring Security 5 后默认要求密码带{bcrypt}等前缀标识算法,或统一用DelegatingPasswordEncoder。
完整版教学
一、UserDetailsService:加载用户
先理解 UserDetailsService——加载用户的核心:
UserDetailsService(只有一个方法):
UserDetails loadUserByUsername(String username)
→ 根据用户名(或其他标识)加载用户,返回 UserDetails
→ 找不到抛 UsernameNotFoundException
它在认证中的角色:
用户登录时,DaoAuthenticationProvider 需要"用户信息"
→ 调 UserDetailsService.loadUserByUsername(用户输入的用户名)
→ 拿到 UserDetails(含用户名、加密密码、权限、状态)
→ 用它来比对密码、判断状态
默认 vs 自定义:
默认:内存用户(InMemoryUserDetailsManager)或默认 user
自定义:从数据库加载(实现 UserDetailsService,查数据库)
自定义实现(从数据库):
@Service
public class MyUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByUsername(username); // 查数据库
if (user == null) throw new UsernameNotFoundException(...);
return 构造 UserDetails(user); // 转成 UserDetails 返回
}
}
关键:
loadUserByUsername 只管"加载用户"——查库、返回 UserDetails
★ 不做密码比对(那是 PasswordEncoder + Provider 的事)
→ 返回的 UserDetails 里带着"数据库里的加密密码"
自定义 UserDetailsService 会自动被 Spring Security 用(注册成 Bean 即可)
UserDetailsService 只有一个方法 loadUserByUsername(username)——加载用户返回 UserDetails(找不到抛 UsernameNotFoundException)。角色:登录时 DaoAuthenticationProvider 调它拿用户信息、用于比对密码判断状态。自定义(从数据库):实现接口、findByUsername 查数据库、构造 UserDetails 返回。关键:loadUserByUsername 只管加载用户(查库、返回带加密密码的 UserDetails),不做密码比对(那是 PasswordEncoder+Provider 的事)。注册成 Bean 就自动被用。理解「UserDetailsService.loadUserByUsername 加载用户返回 UserDetails(找不到抛异常)、登录时 Provider 调它拿用户信息;自定义从数据库查;★只管加载不做密码比对(返回带加密密码的 UserDetails)」,就掌握了 UserDetailsService。
二、UserDetails:用户模型
UserDetails 是 Spring Security 认识的「用户模型」:
UserDetails(用户模型,Spring Security 认的):
getUsername():用户名
getPassword():密码(★ 加密后的,数据库里存的)
getAuthorities():权限/角色列表
isEnabled():账号是否启用
isAccountNonExpired():账号是否未过期
isAccountNonLocked():账号是否未锁定
isCredentialsNonExpired():密码是否未过期
两种提供 UserDetails 的方式:
① 用 Spring 自带的 User 类(org.springframework.security.core.userdetails.User):
User.builder().username(...).password(...).authorities(...).build()
→ 简单,把你的实体信息填进去
② 让自己的实体实现 UserDetails 接口:
public class MyUser implements UserDetails {
// 实现所有方法,直接用自己的字段
}
→ 更灵活,UserDetails 就是你的用户对象(能带额外信息)
账号状态的意义:
isEnabled=false → 账号禁用,登录被拒
isAccountNonLocked=false → 账号锁定(如多次密码错误锁定)
→ 这些状态让 Spring Security 能处理"账号被禁/锁"的情况
→ 认证时会检查这些状态(都要 true 才能登录)
所以 UserDetails = 把你的用户信息,转成 Spring Security 认的格式
UserDetails 是 Spring Security 认的「用户模型」——getUsername/getPassword(加密后的)/getAuthorities(权限)/isEnabled/isAccountNonExpired/isAccountNonLocked/isCredentialsNonExpired(账号状态)。两种提供方式:① 用 Spring 自带的 User 类(User.builder()... 简单)、② 让自己的实体实现 UserDetails 接口(更灵活、能带额外信息)。账号状态(禁用/锁定/过期)让 Spring Security 处理「账号被禁/锁」(认证时检查、都 true 才能登录)。所以 UserDetails = 把你的用户信息转成 Spring Security 认的格式。理解「UserDetails 用户模型:getUsername/getPassword(加密)/getAuthorities/账号状态(isEnabled/NonLocked 等);两种提供方式:用 Spring User 类(简单)或自己实体实现接口(灵活);账号状态让 Security 处理禁用/锁定」,就掌握了 UserDetails。
三、密码:PasswordEncoder 的配合
密码认证靠 UserDetailsService + PasswordEncoder 配合:
密码认证的完整配合:
注册时(存密码):
用户注册 → PasswordEncoder.encode(明文密码) → 加密密码
→ 加密密码存数据库(★ 绝不存明文)
登录时(验密码):
① UserDetailsService.loadUserByUsername → UserDetails
(含数据库里的加密密码)
② DaoAuthenticationProvider 比对:
PasswordEncoder.matches(用户输入的明文, UserDetails里的加密密码)
→ 一致 → 认证通过
关键分工:
UserDetailsService:加载用户(返回带加密密码的 UserDetails)
PasswordEncoder:加密(存储时)+ 比对(登录时 matches)
DaoAuthenticationProvider:调用上面两者,完成认证
→ 你的 UserDetailsService "不比对密码",只返回用户
比对是 Provider 用 PasswordEncoder 做的
★ 常见错误:在 loadUserByUsername 里自己比对密码
→ 错!loadUserByUsername 拿不到"用户输入的密码"
(它只接收 username 参数,没有 password)
→ 密码比对是 Provider 的事,你只返回带加密密码的 UserDetails
PasswordEncoder 选择:
BCryptPasswordEncoder(推荐,加盐、慢哈希、抗暴力破解)
或 DelegatingPasswordEncoder(支持多种算法,密码带 {算法} 前缀)
密码认证靠 UserDetailsService + PasswordEncoder 配合:注册时 PasswordEncoder.encode(明文) 得加密密码存数据库(绝不存明文);登录时 UserDetailsService 加载 UserDetails(含加密密码)、DaoAuthenticationProvider 用 PasswordEncoder.matches(用户输入的明文, UserDetails 里的加密密码) 比对。关键分工:UserDetailsService 加载用户(不比对密码)、PasswordEncoder 加密+比对、DaoAuthenticationProvider 调两者。常见错误:在 loadUserByUsername 里自己比对密码(错!它只接收 username 没 password、密码比对是 Provider 的事)。PasswordEncoder 选 BCrypt(加盐慢哈希)。理解「密码认证配合:注册 encode 存加密密码(绝不存明文)、登录 UserDetailsService 加载+PasswordEncoder.matches 比对;分工:UserDetailsService 加载不比对、PasswordEncoder 加密比对、Provider 调两者;★错误:在 loadUserByUsername 自己比对(只有 username 没 password)」,就掌握了密码认证的配合。
四、权限的加载
用户的权限/角色怎么从数据库加载到 UserDetails:
UserDetails 的 getAuthorities() 返回用户的权限:
→ 要从数据库加载用户的角色/权限,转成 GrantedAuthority
常见的数据模型(RBAC):
用户表(users)
角色表(roles)
用户-角色关联表(user_roles)
角色-权限关联表(role_permissions,可选)
加载权限:
在 loadUserByUsername 里:
1. 查用户
2. 查用户的角色(user_roles 关联)
3. (可选)查角色的权限
4. 转成 List<GrantedAuthority>
角色 → new SimpleGrantedAuthority("ROLE_" + roleName) // 带前缀
权限 → new SimpleGrantedAuthority(permissionName)
5. 放进 UserDetails 的 authorities
角色前缀(重要):
角色要带 ROLE_ 前缀(配合 hasRole)
数据库存 "ADMIN"(干净)→ 加载时拼 "ROLE_ADMIN"
或用 User.builder().roles("ADMIN")(自动加前缀)
例:
authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(toList());
所以权限加载 = 从数据库查角色/权限 → 转 GrantedAuthority → 放 UserDetails
用户权限从数据库加载到 UserDetails.getAuthorities():常见 RBAC 数据模型(用户表、角色表、用户-角色关联表、角色-权限关联表)。加载:loadUserByUsername 里查用户 → 查角色 →(可选)查权限 → 转成 List<GrantedAuthority>(角色 new SimpleGrantedAuthority("ROLE_" + roleName) 带前缀、权限 new SimpleGrantedAuthority(permissionName))→ 放进 UserDetails。角色前缀:数据库存 ADMIN(干净)、加载时拼 ROLE_ADMIN(或 User.builder().roles("ADMIN") 自动加)。理解「权限加载:RBAC 数据模型(用户/角色/关联表)、loadUserByUsername 查角色权限转 GrantedAuthority(角色带 ROLE_ 前缀/权限不带)放进 UserDetails;数据库存 ADMIN 加载拼前缀或用 roles()」,就掌握了权限的加载。
五、多种登录标识
支持「用户名/手机号/邮箱」等多种登录标识:
loadUserByUsername 的参数其实是"登录标识":
不一定是"用户名"——可以是手机号、邮箱等
支持多种标识登录:
① 一个 loadUserByUsername 里判断标识类型:
public UserDetails loadUserByUsername(String identifier) {
User user;
if (isEmail(identifier)) user = findByEmail(identifier);
else if (isMobile(identifier)) user = findByMobile(identifier);
else user = findByUsername(identifier);
...
}
② 或数据库查询用 OR:
findByUsernameOrEmailOrMobile(identifier)
→ 用户可以用用户名 / 邮箱 / 手机号任一登录
配合前面的"自定义认证方式"(AuthenticationProvider):
手机验证码登录:UserDetailsService.loadUserByUsername(手机号)
→ UserDetailsService 复用,只是传的标识不同
所以 loadUserByUsername 的 "username" 是泛指"登录标识"
→ 灵活支持多种登录方式,只要能按标识查到用户
支持多种登录标识:loadUserByUsername 的参数其实是「登录标识」(不一定是用户名,可以是手机号/邮箱)。支持多种标识:① 在 loadUserByUsername 里判断标识类型(isEmail/isMobile 分别查)、② 数据库查询用 OR(findByUsernameOrEmailOrMobile)。配合自定义认证方式(AuthenticationProvider):手机验证码登录 loadUserByUsername(手机号)(UserDetailsService 复用、传标识不同)。所以 username 是泛指「登录标识」(只要能按标识查到用户)。理解「loadUserByUsername 参数是登录标识(不一定用户名可手机号/邮箱);支持多标识:判断类型分别查或 OR 查询;配合 AuthenticationProvider 复用(传标识不同);username 泛指登录标识」,就掌握了多种登录标识。
六、实践与总结
总结数据库认证的实践:
实践步骤:
① 实现 UserDetailsService(loadUserByUsername 从数据库查用户)
② 提供 UserDetails(用 Spring User 类 或 自己实体实现接口)
③ 配置 PasswordEncoder(BCrypt)
④ 注册时用 PasswordEncoder.encode 加密密码存库
⑤ 登录靠 UserDetailsService 加载 + PasswordEncoder 比对(自动)
注意点:
① loadUserByUsername 只加载用户,不比对密码(比对是 Provider 的事)
② 数据库存加密密码(BCrypt),绝不存明文
③ 角色带 ROLE_ 前缀(或用 roles() 自动加)
④ 账号状态(isEnabled/Locked)要正确返回(处理禁用/锁定)
⑤ 找不到用户抛 UsernameNotFoundException
⑥ UserDetailsService 注册成 Bean 自动被用
常见错误:
✗ 在 loadUserByUsername 里自己比对密码(拿不到用户输入的密码)
✗ 存明文密码
✗ 角色没带 ROLE_ 前缀(hasRole 匹配不上)
✗ 加密方式和存储不一致(比对失败)
核心总结:
UserDetailsService.loadUserByUsername:从数据库加载用户 → UserDetails
UserDetails:用户模型(用户名、加密密码、权限、状态)
PasswordEncoder:加密(存)+ 比对(登录)
UserDetailsService 加载 + PasswordEncoder 比对 = 数据库认证
数据库认证实践:① 实现 UserDetailsService(从数据库查用户)、② 提供 UserDetails(Spring User 类或自己实体实现)、③ 配置 PasswordEncoder(BCrypt)、④ 注册时 encode 加密存库、⑤ 登录靠 UserDetailsService 加载+PasswordEncoder 比对(自动)。注意:loadUserByUsername 只加载不比对、存加密密码绝不存明文、角色带 ROLE_ 前缀、账号状态正确返回、找不到抛 UsernameNotFoundException。常见错误:自己比对密码、存明文、角色没前缀、加密方式不一致。理解「实践:实现 UserDetailsService+提供 UserDetails+配置 PasswordEncoder+注册加密存库+登录自动比对;注意:只加载不比对/存加密/角色带前缀/状态正确/找不到抛异常」,就掌握了数据库认证的实践。
记忆钩子:「Spring Security 从数据库加载用户两步:①实现 UserDetailsService(loadUserByUsername(标识)从数据库查用户返回 UserDetails、找不到抛 UsernameNotFoundException、★只加载不比对密码)②提供 UserDetails(用户模型:getUsername/getPassword(加密后的)/getAuthorities/账号状态 isEnabled 等,用 Spring User 类或自己实体实现接口);密码认证配合:注册 PasswordEncoder.encode 加密存库(绝不存明文)、登录 UserDetailsService 加载+DaoAuthenticationProvider 用 PasswordEncoder.matches 比对;分工:UserDetailsService 加载/PasswordEncoder 加密比对/Provider 调两者;★错误:在 loadUserByUsername 自己比对密码(只有 username 没 password);角色带 ROLE_ 前缀、loadUserByUsername 参数是登录标识(可手机号/邮箱)」。
七、常见误区与追问
- 误区:在 loadUserByUsername 里比对密码。 错——loadUserByUsername 只接收 username 参数、拿不到用户输入的密码,它只管「加载用户」(返回带加密密码的 UserDetails);密码比对是 DaoAuthenticationProvider 用 PasswordEncoder.matches 做的(比对用户输入的明文和 UserDetails 里的加密密码);自己在里面比对是常见错误。
- 误区:数据库存明文密码。 绝不能——要用 PasswordEncoder.encode(明文) 得到加密密码(BCrypt 等)存数据库;登录时 PasswordEncoder.matches 比对;存明文一旦数据库泄露所有密码暴露;BCrypt 加盐、慢哈希、抗暴力破解。
- 误区:UserDetails 的 getPassword 返回明文密码。 返回的是数据库里存的加密密码——PasswordEncoder.matches 会把「用户输入的明文」和「UserDetails 的加密密码」比对;如果 getPassword 返回明文,加密方式对不上、比对会失败。
- 误区:角色不带 ROLE_ 前缀也能用 hasRole。 不行——hasRole(‘ADMIN’) 检查的是 ROLE_ADMIN 权限;如果 UserDetails 的 authorities 里存的是 “ADMIN”(没前缀),hasRole(‘ADMIN’) 匹配不上;加载权限时角色要拼上 ROLE_ 前缀(或用 User.builder().roles(“ADMIN”) 自动加)。
- 追问:怎么让 Spring Security 从数据库加载用户? 两步:① 实现 UserDetailsService,在 loadUserByUsername(username) 里从数据库查用户(按用户名/邮箱/手机号),找不到抛 UsernameNotFoundException,找到就构造并返回一个 UserDetails(含用户名、数据库里的加密密码、权限、账号状态);② 配置 PasswordEncoder(如 BCryptPasswordEncoder);注册时用它加密密码存库,登录时 DaoAuthenticationProvider 会自动调你的 UserDetailsService 加载用户、用 PasswordEncoder 比对密码;UserDetailsService 注册成 Bean 就会被自动使用。
- 追问:UserDetailsService 和 PasswordEncoder 各负责什么? UserDetailsService 负责「加载用户」——loadUserByUsername 从数据库查用户、返回带加密密码的 UserDetails;PasswordEncoder 负责「密码的加密和比对」——注册时 encode(明文) 加密存库、登录时 matches(明文, 加密密码) 比对;DaoAuthenticationProvider 把两者组合起来完成认证(调 UserDetailsService 拿用户、调 PasswordEncoder 比对密码);UserDetailsService 不做密码比对。
- 追问:怎么支持用户名、邮箱、手机号都能登录? loadUserByUsername 的参数其实是「登录标识」(不一定是用户名)——在里面判断标识的类型(是邮箱/手机号/用户名)分别查询,或用一个 OR 查询(findByUsernameOrEmailOrMobile);只要能按这个标识从数据库查到用户、返回 UserDetails 就行;这样用户用任一标识都能登录,UserDetailsService 复用。
八、加强记忆
让 Spring Security 从数据库加载用户,核心是自定义 UserDetailsService 和提供 UserDetails。① UserDetailsService——只有一个方法 loadUserByUsername(username),在里面从数据库查用户(参数其实是「登录标识」,可以是用户名/邮箱/手机号),找到就返回 UserDetails、找不到抛 UsernameNotFoundException;它只管加载用户,不做密码比对(返回带加密密码的 UserDetails)。② UserDetails——Spring Security 认的「用户模型」(getUsername/getPassword(加密后的)/getAuthorities(权限,角色带 ROLE_ 前缀)/账号状态 isEnabled/isAccountNonLocked 等),用 Spring 自带的 User 类或让自己实体实现接口。密码认证靠 UserDetailsService + PasswordEncoder 配合:注册时 PasswordEncoder.encode(明文) 加密存库(绝不存明文),登录时 UserDetailsService 加载 UserDetails(含加密密码)、DaoAuthenticationProvider 用 PasswordEncoder.matches(用户输入的明文, 加密密码) 比对。常见错误:在 loadUserByUsername 里自己比对密码(它只有 username 没 password、比对是 Provider 的事)。PasswordEncoder 用 BCrypt。一句话「从数据库加载用户:实现 UserDetailsService(loadUserByUsername 查库返回 UserDetails,只加载不比对密码)+提供 UserDetails(用户名/加密密码/权限/状态)+配置 PasswordEncoder(BCrypt);注册 encode 存加密密码(绝不明文)、登录 UserDetailsService 加载+PasswordEncoder.matches 比对;错误:自己比对密码/存明文/角色没 ROLE_ 前缀」。