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

Spring Boot 3迁移SpringSecurityConfig登录失败问题排查

Spring Boot Security配置迁移后登录失败排查

问题现象

将SecurityConfig从旧版Spring Boot迁移到新版本后,登录功能失效。页面输入明文密码,数据库存储的是encode()生成的加密密码,但无法完成登录。日志输出如下:

Hibernate: select u1_0.id,u1_0.name,u1_0.password,u1_0.username from users u1_0 where u1_0.username=?
Hibernate: select a1_0.user_id,a1_0.id,a1_0.authority from authority a1_0 where a1_0.user_id=?

怀疑是id参数传递异常,但无法确定根源。


提供的代码文件

旧版配置文件

@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
  
  @Autowired
  private UserDetailsService userDetailsService;

  @Bean
  public PasswordEncoder getPasswordEncoder() {
    return new BCryptPasswordEncoder();
  }
  
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
      .userDetailsService(userDetailsService)
      .passwordEncoder(getPasswordEncoder());
        
//    auth.inMemoryAuthentication()
//        .passwordEncoder(getPasswordEncoder())
//        .withUser("trevor@craftycodr.com")
//        .password(getPasswordEncoder().encode("asdfasdf"))
//        .roles("USER");
  }
  
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
          .antMatchers("/").permitAll()
          .anyRequest().hasRole("USER").and()
        .formLogin()
          .loginPage("/login")
          .defaultSuccessUrl("/dashboard")
          .permitAll()
          .and()
        .logout()
          .logoutUrl("/logout")
          .permitAll();
  }
}

新版配置文件

@Configuration
@EnableWebSecurity
public class WebSecurityConfig
{
    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public UserDetailsService userDetailsService()
    {
        return new UserDetailsServiceImpl();
    }

    @Bean
    public PasswordEncoder passwordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception
    {
        return
            http.authorizeHttpRequests()
                .requestMatchers("/").permitAll()
                .anyRequest().hasRole("USER")
                .and()
                .formLogin()
                    .loginPage("/login")
                    .defaultSuccessUrl("/dashboard")
                    .permitAll()
                    .and()
                .logout()
                    .logoutUrl("/logout").permitAll()
                .and().build();
    }

        @Bean
        public AuthenticationProvider authenticationProvider()
        {
            DaoAuthenticationProvider authenticationProvider=new DaoAuthenticationProvider();
            authenticationProvider.setUserDetailsService(userDetailsService());
            authenticationProvider.setPasswordEncoder(passwordEncoder());
            return authenticationProvider;
        }
}

UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
    @Autowired
    private UserRepository userRepo;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {
        User user = userRepo.findByUsername(username);

        if(user == null)
        {
            throw new UsernameNotFoundException("Invalid username and password");
        }
        return new CustomSecurityUser(user);
    }
}

CustomSecurityUser

public class CustomSecurityUser extends User implements UserDetails
{
    private static final long serialVersionUID = 464958176L;
    
    public CustomSecurityUser()
    {}

    public CustomSecurityUser(User user)
    {
        this.setAuthorities(user.getAuthorities());
        this.setId(user.getId());
        this.setName(user.getName());
        this.setPassword(user.getPassword());
        this.setUsername(user.getUsername());
    }

    @Override
    public Set<Authority> getAuthorities()
    {
        return super.getAuthorities();
    }

    @Override
    public String getPassword()
    {
        return super.getPassword();
    }

    @Override
    public String getUsername()
    {
        return super.getUsername();
    }

    @Override
    public boolean isAccountNonExpired()
    {
        return true;
    }

    @Override
    public boolean isAccountNonLocked()
    {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired()
    {
        return true;
    }

    @Override
    public boolean isEnabled()
    {
        return true;
    }
}

UserRepository

public interface UserRepository extends JpaRepository<User, Long>
{
    User findByUsername(String username);
    
}

Authority实体

package com.freshvotes.security;
import org.springframework.security.core.GrantedAuthority;
import com.freshvotes.domain.User;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;

@Entity
public class Authority implements GrantedAuthority
{
    private static final long serialVersionUID = -6420181486L;
    private Long id;
    private String authority;
    private User user;
    @Override
    public String getAuthority()
    {
        return this.authority;
    }
    public void setAuthority(String authority)
    {
        this.authority = authority;
    }

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    public Long getId()
    {
        return this.id;
    }

    public void setId(Long id)
    {
        this.id = id;
    }

    @ManyToOne()
    public User getUser()
    {
        return this.user;
    }

    public void setUser(User user)
    {
        this.user = user;
    }

}

User实体

@Entity
@Table(name = "users")
public class User
{
    private Long id;
    private String username;
    private String password;
    private String name;
    private Set<Authority> authorities = new HashSet<>();

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    public Long getId()
    {
        return id;
    }
    public void setId(Long id)
    {
        this.id = id;
    }

    public String getUsername()
    {
        return username;
    }
    public void setUsername(String username)
    {
        this.username = username;
    }

    public String getPassword()
    {
        return password;
    }
    public void setPassword(String password)
    {
        this.password = password;
    }

    public String getName()
    {
        return name;
    }
    public void setName(String name)
    {
        this.name = name;
    }

    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.EAGER, mappedBy="user")
    public Set<Authority> getAuthorities()
    {
        return this.authorities;
    }
    public void setAuthorities(Set<Authority> authorities)
    {
        this.authorities = authorities;
    }
    

}

问题根源及修复方案

1. UserDetailsService实例冲突

新版配置中同时存在@Autowired注入和手动创建的UserDetailsService Bean,导致Spring容器中存在两个实例,认证逻辑混乱。

修复:删除手动创建的userDetailsService() Bean,直接使用注入的实例:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig
{
    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception
    {
        return
            http.authorizeHttpRequests()
                .requestMatchers("/").permitAll()
                .anyRequest().hasRole("USER")
                .and()
                .formLogin()
                    .loginPage("/login")
                    .defaultSuccessUrl("/dashboard")
                    .permitAll()
                    .and()
                .logout()
                    .logoutUrl("/logout").permitAll()
                .and().build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider()
    {
        DaoAuthenticationProvider authenticationProvider=new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }
}

2. 角色权限前缀不匹配

旧版hasRole("USER")会自动添加ROLE_前缀,若数据库中Authority表的authority字段存储的是USER而非ROLE_USER,会导致权限匹配失败。

修复:

  • 方案一:将数据库中authority字段值改为ROLE_USER;
  • 方案二:配置中改用hasAuthority("USER")替代hasRole("USER"):
.anyRequest().hasAuthority("USER")

3. CustomSecurityUser继承风险

CustomSecurityUser继承自定义User实体可能引发字段覆盖或方法冲突,建议改用组合模式优化:

public class CustomSecurityUser implements UserDetails {
    private static final long serialVersionUID = 464958176L;
    private final User user;

    public CustomSecurityUser(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getAuthorities();
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

4. 验证密码加密逻辑

确保数据库中的密码是通过BCryptPasswordEncoder.encode()生成的,可通过以下代码验证:

PasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode("你的明文密码"));

将输出的加密字符串与数据库存储值对比,确认是否一致。


内容的提问来源于stack exchange,提问作者Shekel Sniffer

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 16:10:55