认证:内存用户与 UserDetailsService

上一章留下一个问题:用户名、密码从哪来?Spring Security 把"找用户"抽象成 UserDetailsService,把"校验密码"交给 PasswordEncoder + DaoAuthenticationProvider。本章讲从内置用户到自定义查库的完整链路,示例基于 Security 6 / Boot 3。

方式一:配置文件内置用户

最省事的演示方式,适合开发期:

spring:
  security:
    user:
      name: admin
      password: '{bcrypt}$2a$10$...'   # 存 BCrypt 密文(生成工具见下);明文仅限本地
      roles: ADMIN,USER

方式二:内存 UserDetailsService

需要多个用户或启动时动态构造时:

@Configuration
public class UserConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();       // 单向哈希 + 随机盐
    }

    @Bean
    public UserDetailsService users(PasswordEncoder encoder) {
        UserDetails admin = User.withUsername("admin")
                .password(encoder.encode("123456"))   // 输出:$2a$10$...(盐随机,两次结果不同)
                .roles("ADMIN", "USER")               // roles() 自动补 ROLE_ 前缀
                .build();
        return new InMemoryUserDetailsManager(admin);
    }
}

方式三:自定义 UserDetailsService 查库(生产主流)

只须实现接口:Security 调用 loadUserByUsername 拿到用户,DaoAuthenticationProvider 再用 PasswordEncoder 比对表单密码与库中密文。

@Service
public class DbUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;   // 构造注入省略

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
        return User.withUsername(user.getUsername())
                .password(user.getPassword())       // 数据库里必须存 BCrypt 密文
                .roles(user.getRole())              // 如 ADMIN/USER,自动转 ROLE_ADMIN
                .build();
    }
}

校验流程一句话:表单提交 → UsernamePasswordAuthenticationFilter 取 username/password → DaoAuthenticationProvider 调 loadUserByUsername → passwordEncoder.matches(明文, 密文) → 成功则构造带权限的 Authentication 放进 SecurityContext;用户不存在抛 UsernameNotFoundException,最终表现为 401/403。

密码存储规范

  • 明文密码是事故:务必 BCrypt(内置强度 10)或更高成本算法;
  • Boot 默认的 PasswordEncoder 是 DelegatingPasswordEncoder,密文带 {bcrypt} 前缀,将来换算法不影响存量数据;
  • 注册、改密接口里用同一个 encoder.encode() 加密后再入库。

认证后的使用

// 控制器参数注入
@GetMapping("/me")
public String me(@AuthenticationPrincipal UserDetails user) {
    return user.getUsername();     // 输出:当前登录用户名
}

小结:认证链路的用户来源只需三选一——属性、内存、或自定义 UserDetailsService;配合 BCryptPasswordEncoder,Security 即可完成"查用户 + 验密文"的标准认证。

笔记加载中…