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

Spring Security自定义手机号认证失败问题排查求助

问题

我通过自定义字段实现Spring Security认证,用Long类型的手机号(phoneNumber)替代默认String类型的username作为Principal,PIN字段符合UserDetails要求。输入正确数据提交认证请求后,Spring Security返回登录表单并抛出错误"Invalid phone number or PIN",怀疑Principal未被正确接收,但不确定原因。请问错误可能是什么?哪里操作有误?该如何修复?


相关代码

Spring Security配置文件

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
    private final AuthProviderImpl authProvider;

    @Autowired
    public SecurityConfiguration(@Lazy AuthProviderImpl authProvider) {
        this.authProvider = authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);
        authenticationManagerBuilder.authenticationProvider(authProvider);

        return authenticationManagerBuilder.build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .requestMatchers("/BankAccounts/new").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/auth/login")
                .loginProcessingUrl("/auth/login")
                .permitAll();
        return http.build();
    }

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

    @Bean
    public AuthProviderImpl authProvider(BankAccountsDetailService bankAccountsDetailService) {
        return new AuthProviderImpl(bankAccountsDetailService, getPasswordEncoder());
    }
}

实现UserDetails的Java类

public class BankAccountDetails implements UserDetails {

    private final BankAccount bankAccount;
    private final Long phoneNumber;

    public BankAccountDetails(BankAccount bankAccount) {
        this.bankAccount = bankAccount;
        this.phoneNumber = bankAccount.getPhoneNumber();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_USER")); 
    }

    @Override
    public String getPassword() {
        return this.bankAccount.getPin();
    }

    @Override
    public String getUsername() {
        return this.bankAccount.getFullName();
    }

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

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

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

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

    public Long getPhoneNumber() {
        return phoneNumber;
    }

    public BankAccount getBankAccount() {
        return this.bankAccount;
    }
}

自定义认证提供者

@Component
public class AuthProviderImpl implements AuthenticationProvider {

    private final BankAccountsDetailService bankAccountsDetailService;
    private final PasswordEncoder passwordEncoder;

    private static final Logger logger = LoggerFactory.getLogger(AuthProviderImpl.class);

    @Autowired
    public AuthProviderImpl(BankAccountsDetailService bankAccountsDetailService, PasswordEncoder passwordEncoder) {
        this.bankAccountsDetailService = bankAccountsDetailService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public PhoneNumberAuthToken authenticate(Authentication authentication) throws AuthenticationException {
        logger.info("authenticate method is called");

        Object principal = authentication.getPrincipal();
        if (principal == null) {
            logger.error("Principal is null");
            throw new BadCredentialsException("Principal is null");
        }

        String phoneNumberStr = principal.toString();
        logger.info("Received phone number: {}", phoneNumberStr);

        try {
            Long phoneNumber = Long.parseLong(phoneNumberStr);
            String pin = authentication.getCredentials().toString();
            logger.info("Phone number: {}", phoneNumber);
            logger.info("PIN: {}", pin);

            // load bank account details by phone number
            BankAccountDetails bankAccountDetails = (BankAccountDetails) bankAccountsDetailService.loadUserByPhoneNumber(phoneNumber);
            if (bankAccountDetails == null) {
                logger.error("Bank account not found for phone number: {}", phoneNumber);
                throw new BadCredentialsException("Неверный номер телефона или PIN-код");
            }

            // Checking PIN-code with PasswordEncoder
            if (!passwordEncoder.matches(pin, bankAccountDetails.getPassword())) {
                logger.error("PIN does not match for phone number: {}", phoneNumber);
                throw new BadCredentialsException("Incorrect Phone number or PIN-code");
            }

            logger.info("Authentication successful for phone number: {}", phoneNumber);
            return new PhoneNumberAuthToken(bankAccountDetails.getAuthorities(), phoneNumber, pin);

        } catch (NumberFormatException e) {
            logger.error("Invalid phone number format: {}", phoneNumberStr, e);
            throw new BadCredentialsException("Invalid phone number format");
        } catch (UsernameNotFoundException e) {
            logger.error("User not found for phone number: {}", phoneNumberStr, e);
            throw new BadCredentialsException("Invalid phone number or PIN");
        } catch (Exception e) {
            logger.error("Authentication error", e);
            throw new BadCredentialsException("Authentication error");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return PhoneNumberAuthToken.class.isAssignableFrom(authentication);
    }
}

自定义认证令牌

public class PhoneNumberAuthToken extends AbstractAuthenticationToken {

    private final Long phoneNumber;

    private final String pin;

    public PhoneNumberAuthToken(Collection<? extends GrantedAuthority> authorities, Long phoneNumber, String pin) {
        super(authorities);
        this.phoneNumber = phoneNumber;
        this.pin = pin;
        setAuthenticated(false);
    }

    @Override
    public Object getCredentials() {
        return pin;
    }

    @Override
    public Object getPrincipal() {
        return phoneNumber;
    }
}

认证表单代码

<body>
<div class="login-container">
    <h2 class="mt-5">Login form</h2>
    <form action="#" th:action="@{/auth/login}" method="post" class="mt-4">
        <div class="mb-3">
            <label for="phoneNumber" class="form-label">Phone number</label>
            <input type="text" class="form-control" id="phoneNumber" name="phoneNumber" required>
        </div>
        <div class="mb-3">
            <label for="pin" class="form-label">PIN-код</label>
            <input type="password" class="form-control" id="pin" name="pin" required>
        </div>
        <button type="submit" class="btn btn-primary">Login </button>
    </form>

    <!-- Displaying authentication errors -->
    <div th:if="${param.error}" class="alert alert-danger mt-3">
        Incorrect phone number or PIN-code.
    </div>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>
</body>

核心错误分析

  1. 表单参数与默认认证不匹配:Spring Security默认表单认证读取username和password参数,但你的表单提交的是phoneNumber和pin,导致默认的UsernamePasswordAuthenticationToken无法正确获取参数,且你的自定义AuthProviderImpl只支持PhoneNumberAuthToken,根本不会被触发。
  2. 认证提供者的支持范围错误:AuthProviderImpl的supports方法仅接受PhoneNumberAuthToken,但表单提交后Spring Security生成的是UsernamePasswordAuthenticationToken,导致自定义认证逻辑完全没执行,走了默认流程后因找不到用户抛出错误。
  3. 认证令牌状态未正确设置:PhoneNumberAuthToken构造函数中调用了setAuthenticated(false),即使认证成功,令牌仍处于未认证状态,Spring Security会判定认证失败。
  4. 潜在的PIN加密问题:如果数据库中存储的PIN是明文,而你用BCryptPasswordEncoder做匹配,会直接匹配失败。

修复步骤

1. 配置表单认证的自定义参数名

在Security配置的formLogin中指定参数名,让Spring Security正确读取表单数据:

.formLogin()
    .loginPage("/auth/login")
    .loginProcessingUrl("/auth/login")
    .usernameParameter("phoneNumber") // 绑定手机号参数
    .passwordParameter("pin") // 绑定PIN参数
    .permitAll();

2. 修改认证提供者的支持逻辑

让AuthProviderImpl支持默认的UsernamePasswordAuthenticationToken,确保表单提交后能触发自定义认证:

@Override
public boolean supports(Class<?> authentication) {
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)
            || PhoneNumberAuthToken.class.isAssignableFrom(authentication);
}

3. 修正认证令牌的状态

认证成功后将令牌设置为已认证状态,修改PhoneNumberAuthToken的构造函数:

public PhoneNumberAuthToken(Collection<? extends GrantedAuthority> authorities, Long phoneNumber, String pin) {
    super(authorities);
    this.phoneNumber = phoneNumber;
    this.pin = pin;
    setAuthenticated(true); // 认证成功后标记为已认证
}

4. 验证PIN的加密存储

确保数据库中存储的PIN是通过BCryptPasswordEncoder加密后的字符串,而非明文。如果是新用户注册,需要先对PIN加密再存储:

// 注册时的加密示例
String encryptedPin = passwordEncoder.encode(rawPin);
bankAccount.setPin(encryptedPin);

5. 补充日志排查

在AuthProviderImpl的authenticate方法开头增加日志,确认方法是否被调用:

logger.info("AuthProviderImpl invoked, authentication type: {}", authentication.getClass().getName());

如果未打印该日志,检查Security配置中是否正确注册了AuthProviderImpl,以及supports方法逻辑是否正确。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 22:15:54