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>
核心错误分析
- 表单参数与默认认证不匹配:Spring Security默认表单认证读取
username和password参数,但你的表单提交的是phoneNumber和pin,导致默认的UsernamePasswordAuthenticationToken无法正确获取参数,且你的自定义AuthProviderImpl只支持PhoneNumberAuthToken,根本不会被触发。 - 认证提供者的支持范围错误:
AuthProviderImpl的supports方法仅接受PhoneNumberAuthToken,但表单提交后Spring Security生成的是UsernamePasswordAuthenticationToken,导致自定义认证逻辑完全没执行,走了默认流程后因找不到用户抛出错误。 - 认证令牌状态未正确设置:
PhoneNumberAuthToken构造函数中调用了setAuthenticated(false),即使认证成功,令牌仍处于未认证状态,Spring Security会判定认证失败。 - 潜在的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
相关产品推荐
相关产品推荐

