You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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/AuthenticationProvider Bean覆盖自定义实现
  • 发起登录请求时控制台是否打印loadUserByUsername: xxx日志,若未打印说明自定义认证组件未被调用,需检查安全过滤链加载顺序

内容的提问来源于stack exchange,提问作者Kleber Mota

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 15:21:18