Spring Boot如何重写UserDetailsService与PasswordEncoder解决登录异常
问题根因与修复方案
当前配置不生效、登录失败由4类核心问题导致,按以下步骤修复即可正常运行:
1 基础配置拼写错误
application.properties存在拼写错误,会导致JPA配置失效,引发数据库查询异常:
错误配置项为sprinf.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect,需将sprinf修正为spring。
2 安全配置逻辑缺失
直接定义UserDetailsService和PasswordEncoder Bean不生效,是因为Spring Security默认自动配置在没有显式绑定认证组件到安全过滤链时,会自动使用默认内存用户配置。你在更新2中编写SecurityFilterChain+自定义DaoAuthenticationProvider的方向是正确的,但缺失了表单登录的核心配置:没有指定自定义登录页、登录请求处理路径,Spring Security会默认走内置登录逻辑,和你编写的/login页面无法匹配。
另外此前将/login//register等路径放在WebSecurityCustomizer.ignoring()中是错误配置:ignoring()会让路径完全跳过安全过滤器链,登录请求无法被认证处理器拦截,仅静态资源可配置在此处。
3 密码编码器与Bean调用问题
自定义MD5密码编码器存在两个问题:
- 未处理空值、编码异常场景,极端情况返回null会直接导致密码比对失败
- MD5为无加盐的不安全哈希算法,生产环境不建议使用,优先选择BCrypt算法
同时在注册接口中直接调用passwordEncoder().encode()会绕过Spring Bean代理,可能出现编码器实例不一致的问题,应通过依赖注入的方式使用Bean。
4 用户查询逻辑的规范问题
loadUserByUsername方法直接返回Usuario实体类、调用get(0)取数据存在风险:
- 若查询结果为空会抛出索引越界异常,Spring Security无法识别为「用户不存在」场景,必须抛出标准
UsernameNotFoundException - 需确认
Usuario实现UserDetails接口时,四个账号状态方法(账号未过期、未锁定、凭证未过期、可用)均返回true,且权限集合正确封装为GrantedAuthority类型,否则会直接判定账号不可用导致登录失败。
修正后可直接运行的代码
修正后的application.properties
security.basic.enabled=false spring.datasource.driver-class-name=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://localhost:5432/appdata spring.datasource.username=kleber spring.datasource.password=123456 spring.datasource.continue-on-error=true # 修复拼写错误 spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect spring.jpa.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update spring.jpa.generate-ddl=true spring.thymeleaf.prefix=classpath:/templates/ spring.thymeleaf.suffix=.html spring.thymeleaf.mode=HTML5 spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.content-type=text/html spring.thymeleaf.cache=false spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=10MB spring.servlet.multipart.file-size-threshold=10MB server.tomcat.max-http-post-size=10MB
修正后的启动类与安全配置
@SpringBootApplication @Controller public class App extends SpringBootServletInitializer { @Autowired UsuarioDao usuarioDao; @Autowired CredencialDao credencialDao; // 注入密码编码器,避免直接调用方法绕过Spring代理 @Autowired private PasswordEncoder passwordEncoder; public static void main(String[] args) { SpringApplication.run(App.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(App.class); } @Bean public SpringSecurityDialect springSecurityDialect() { return new SpringSecurityDialect(); } // 仅静态资源配置为忽略安全拦截 @Bean public WebSecurityCustomizer ignoringCustomizer() { return (web) -> web .ignoring() .antMatchers("/css/**", "/js/**", "/img/**"); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .cors().disable() .authorizeRequests() .antMatchers("/", "/login", "/logout", "/register", "/error").permitAll() .anyRequest().authenticated() .and() // 补充表单登录配置 .formLogin() .loginPage("/login") .loginProcessingUrl("/login") // 与表单提交地址保持一致 .defaultSuccessUrl("/", true) .failureUrl("/login?error=true") .permitAll() .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login?logout=true") .permitAll() .and() .authenticationProvider(authProvider()); return http.build(); } @Bean public DaoAuthenticationProvider authProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setPasswordEncoder(passwordEncoder); provider.setUserDetailsService(userDetailsService()); provider.setHideUserNotFoundExceptions(false); return provider; } @Bean public UserDetailsService userDetailsService() { return username -> { List<Usuario> usuarios = usuarioDao.findBy("username", username); if (usuarios.isEmpty()) { // 必须抛出标准异常,禁止返回null或直接调用get(0) throw new UsernameNotFoundException("用户不存在: " + username); } Usuario usuario = usuarios.get(0); // 封装权限集合,注意权限前缀ROLE_需和代码中校验规则匹配 List<GrantedAuthority> authorities = usuario.getCredenciais().stream() .map(cred -> new SimpleGrantedAuthority("ROLE_" + cred.getNome())) .collect(Collectors.toList()); // 直接返回Spring Security标准User对象,避免实体类实现接口的疏漏 return new User( usuario.getUsername(), usuario.getPassword(), true, true, true, true, authorities ); }; } // 推荐使用BCryptPasswordEncoder替换自定义MD5实现,安全性更高 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @RequestMapping(value = "/") public String index(Model model) { return "index"; } @RequestMapping(value = "/login") public String login(Model model) { return "login"; } @RequestMapping(value = "/register", method=RequestMethod.GET) public String register(Model model) { model.addAttribute("obj", new Usuario()); return "register"; } @RequestMapping(value = "/register", method=RequestMethod.POST) public String register(@ModelAttribute Usuario usuario) { try { // 使用注入的密码编码器,禁止直接调用Bean方法 usuario.setPassword(passwordEncoder.encode(usuario.getPassword())); usuario.setCredenciais(new ArrayList<>()); usuario.getCredenciais().add(credencialDao.findBy("nome", "USER").get(0)); usuarioDao.insert(usuario); return "redirect:/login"; } catch (Exception e) { e.printStackTrace(); return "register"; } } }
额外排查点
如果修正后仍无法登录,按顺序校验:
- 登录表单的用户名、密码参数名是否为默认的
username/password,如果自定义了参数名需要在formLogin()中通过usernameParameter()/passwordParameter()指定 - 数据库存储的密码为加密后的密文,禁止存储明文
- 启动日志无Bean冲突报错,确认没有其他
UserDetailsService/AuthenticationProviderBean覆盖自定义实现 - 发起登录请求时控制台是否打印
loadUserByUsername: xxx日志,若未打印说明自定义认证组件未被调用,需检查安全过滤链加载顺序
内容的提问来源于stack exchange,提问作者Kleber Mota
相关产品推荐
相关产品推荐

