Spring Security自定义AuthenticationManager实现手机号/密码双认证

 更新时间:2026年06月03日 09:33:45   作者:码语智行  
这篇文章给大家介绍了Spring Security自定义AuthenticationManager实现手机号/密码双认证,本文结合实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧

01整体思路 3 步走

  1. 1. 自定义认证提供者 CustomAuthenticationProvider
    识别登录方式,分发给对应 UserDetailsService
  2. 2. 双 Service
    • UserDetailsService 验证账号密码
    • PhoneNumberUserService 验证手机号验证码
  3. 3. 配置注入:把自定义提供者塞进 Spring Security,让它乖乖听话。

02自定义认证提供者

public class CustomAuthenticationProvider implements AuthenticationProvider {  
    private final UserDetailsService userDetailsService;    // 账号密码验证  
    private final PasswordEncoder passwordEncoder;       // 密码加密器  
    private final PhoneNumberUserService phoneNumberUserService; // 手机号验证  
    public CustomAuthenticationProvider(UserDetailsService userDetailsService,  
                                        PasswordEncoder passwordEncoder,  
                                        PhoneNumberUserService phoneNumberUserService) {  
        this.userDetailsService = userDetailsService;  
        this.passwordEncoder = passwordEncoder;  
        this.phoneNumberUserService = phoneNumberUserService;  
    }  
    @Override  
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {  
        String principal = (String) authentication.getPrincipal();  // username:xxx 或 phone:xxx        
        String credentials = (String) authentication.getCredentials(); // 密码或验证码  
        UserDetails userDetails;  
        if (principal.startsWith("username:")) {         // 账号密码登录  
            String username = principal.substring("username:".length());  
            userDetails = userDetailsService.loadUserByUsername(username);  
            if (!passwordEncoder.matches(credentials, userDetails.getPassword())) {  
                throw new BadCredentialsException("密码错误");  
            }  
        } else if (principal.startsWith("phone:")) {         // 手机号登录  
            String phoneNumber = principal.substring("phone:".length());  
            userDetails = phoneNumberUserService.loadUserByPhoneNumber(phoneNumber);         // 这里验证码校验可放在 service 内,也可前置过滤器                      
		else{  
			throw new BadCredentialsException("登录方式不支持");  
		}          
		// 生成已认证令牌  
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails, credentials, userDetails.getAuthorities());  
			result.setDetails(authentication.getDetails());  
		return result;  
        }  
	@Override public boolean supports (Class < ? > authentication){  
		return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);  
	}  
    }  
}

注解:

  • 1. 前缀识别:用 username: 和 phone: 做路由,避免写两套接口。
  • 2. 职责分离:验证码校验交给 PhoneNumberUserService,保持单一职责。
  • 3. 线程安全:所有依赖通过构造器注入,无共享可变状态,天然并发友好。

03双 Service 实现

UserDetailsService(账号密码版)

@Service  
@RequiredArgsConstructor  
public class UserDetailsServiceImpl implements UserDetailsService {  
    private final UserMapper userMapper;  
    private final MenuMapper menuMapper;  
    @Override  
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {  
        User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getUserName, username));  
        if (user == null) throw new UsernameNotFoundException("用户不存在");  
        List<String> perms = menuMapper.selectPermsByUserId(user.getId());  
        perms.add(user.getRoles()); // 合并角色         
		return new LoginUser(user, perms);  
    }  
}

PhoneNumberUserService(手机号验证码版)

@Service  
@RequiredArgsConstructor  
public class PhoneNumberUserService {  
    private final UserMapper userMapper;  
    private final MenuMapper menuMapper;  
    private final RedisTemplate<String, String> redisTemplate; // 缓存验证码          
    public UserDetails loadUserByPhoneNumber(String phoneNumber) {       // 1️ 查库         
User user = userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhonenumber, phoneNumber));  
        if (user == null)  
            throw new RuntimeException("手机号未注册");          // 2️ 查权限         
List<String> perms = menuMapper.selectPermsByUserId(user.getId());  
        perms.add(user.getRoles());          // 3️验证码校验示例(可前置过滤器)       
        //String codeInRedis = redisTemplate.opsForValue().get("SMS:" + phoneNumber);  
        return new LoginUser(user, perms);  
    }  
}

注解:

  • 1. LambdaQueryWrapper:MyBatis-Plus 写法,链式清爽。
  • 2. 角色权限合并:把角色当权限塞到同一集合,后续授权更丝滑。
  • 3. 验证码解耦:校验逻辑可放在 Service,也可前置过滤器,灵活插拔。

04SecurityConfig:把自定义提供者塞进去

@Configuration  
@EnableWebSecurity  
@RequiredArgsConstructor  
public class SecurityConfig {  
    private final AuthenticationConfiguration authenticationConfiguration;  
  //密码加密器
    @Bean  
    public PasswordEncoder passwordEncoder() {  
        return new BCryptPasswordEncoder();  
    }  
    @Bean  
    public UserDetailsService userDetailsService() {  
        return new UserDetailsServiceImpl();  
    }  
    @Bean  
    public PhoneNumberUserService phoneNumberUserService() {  
        return new PhoneNumberUserService();  
    }  
    @Bean  
    public CustomAuthenticationProvider customAuthenticationProvider() {  
        return new CustomAuthenticationProvider(userDetailsService(), passwordEncoder(), phoneNumberUserService());  
    }  
    @Bean  
    public AuthenticationManager authenticationManager() throws Exception {       
    // 替换默认 AuthenticationManager
    return new ProviderManager(customAuthenticationProvider());  
    }  
}

注解:

  • 1. ProviderManager:Spring Security 的核心调度器,塞入我们的 Provider 就能接管认证。
  • 2. 构造器注入:Spring 推荐写法,避免循环依赖。
  • 3. 无 @Autowired:全部显式 Bean,方便单测 Mock。

05登录接口:一行代码双通道

@RestController  
@RequestMapping("/auth")  
@RequiredArgsConstructor  
public class AuthController {  
    private final AuthenticationManager authenticationManager;  
    private final RedisTemplate<String, Object> redisTemplate;  
    @PostMapping("/login")  
    public Result login(@RequestBody LoginDTO dto) {  
        String principal = dto.getLoginType() == 1 ? "username:" + dto.getUsername() : "phone:" + dto.getPhone();  
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, dto.getCredential());  
        Authentication authenticate = authenticationManager.authenticate(token);  
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();  
        String jwt = JwtUtil.createJWT(loginUser.getUser().getId().toString());  
        redisTemplate.opsForValue().set("login:" + loginUser.getUser().getId(), loginUser);  
        return Result.OK("登录成功", Map.of("token", jwt));  
    }  
}

注解:

  • 1. DTO 统一:前端传 loginType=1 账号密码,2 手机号验证码,后端零 if-else。
  • 2. JWT + Redis:无状态 Token + 在线用户信息缓存,分布式登录稳稳的。
  • 3. 异常透传:认证失败直接抛异常,被全局异常处理器统一包装,前端拿到统一格式。

测试

登录方式请求体返回
账号密码{"loginType":1,"username":"yuqn","credential":"123456"}{"msg":"登录成功","token":"eyJ..."}
手机验证码{"loginType":2,"phone":"13800138000","credential":"8888"}同上

到此这篇关于Spring Security自定义AuthenticationManager实现手机号/密码双认证的文章就介绍到这了,更多相关Spring Security自定义AuthenticationManager内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • java使用swt显示图片示例分享

    java使用swt显示图片示例分享

    这篇文章主要介绍了java使用swt显示图片示例,修改后就可变为图片浏览器,需要的朋友可以参考下
    2014-02-02
  • mybatis使用双层<foreach>循环嵌套方式

    mybatis使用双层<foreach>循环嵌套方式

    在Mybatis中使用双层循环嵌套插入数据可以有效减少数据库IO操作,提高性能,通过在mapper.xml中定义双层集合来实现,外层集合为实体类中的集合字段,内层集合为集合字段中的另一个集合,通过这种方式,可以在业务代码中减少循环插入的次数
    2024-09-09
  • mybatis foreach 属性及其三种使用情况详解

    mybatis foreach 属性及其三种使用情况详解

    这篇文章主要介绍了mybatis foreach 属性及其三种使用情况详解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • Mybatis流式查询并实现将结果分批写入文件

    Mybatis流式查询并实现将结果分批写入文件

    这篇文章主要介绍了Mybatis流式查询并实现将结果分批写入文件方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08
  • springboot引用kettle实现对接oracle数据的示例代码

    springboot引用kettle实现对接oracle数据的示例代码

    这篇文章主要介绍了springboot引用kettle实现对接oracle数据,其实kettle集成到springboot里面没有多少代码,这个功能最主要的还是ktr文件的编写,只要ktr编写好了,放到指定文件夹下,写个定时任务就完事了,需要的朋友可以参考下
    2022-12-12
  • springboot常见的一些相关注解总结

    springboot常见的一些相关注解总结

    在springboot中,摒弃了spring以往项目中大量繁琐的配置,遵循约定大于配置的原则,通过自身默认配置,极大的降低了项目搭建的复杂度,这篇文章主要介绍了springboot常见的一些相关注解,需要的朋友可以参考下
    2025-10-10
  • Maven中Junit测试@Test等注解无法识别的问题及解决

    Maven中Junit测试@Test等注解无法识别的问题及解决

    这篇文章主要介绍了Maven中Junit测试@Test等注解无法识别的问题及解决方案,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • 常用的java日期比较和日期计算方法小结

    常用的java日期比较和日期计算方法小结

    这篇文章主要为大家详细总结了常用的java日期比较和日期计算方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-09-09
  • java并发编程专题(九)----(JUC)浅析CyclicBarrier

    java并发编程专题(九)----(JUC)浅析CyclicBarrier

    这篇文章主要介绍了java CyclicBarrier的相关资料,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07
  • CentOS8.2安装Java 14.0.2的教程详解

    CentOS8.2安装Java 14.0.2的教程详解

    这篇文章主要介绍了CentOS8.2安装Java 14.0.2的详细教程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-12-12

最新评论