当前位置: 首页 > news >正文

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

01

整体思路 3 步走

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

02

自定义认证提供者

publicclassCustomAuthenticationProviderimplementsAuthenticationProvider{privatefinalUserDetailsServiceuserDetailsService;// 账号密码验证privatefinalPasswordEncoderpasswordEncoder;// 密码加密器privatefinalPhoneNumberUserServicephoneNumberUserService;// 手机号验证publicCustomAuthenticationProvider(UserDetailsServiceuserDetailsService,PasswordEncoderpasswordEncoder,PhoneNumberUserServicephoneNumberUserService){this.userDetailsService=userDetailsService;this.passwordEncoder=passwordEncoder;this.phoneNumberUserService=phoneNumberUserService;}@OverridepublicAuthenticationauthenticate(Authenticationauthentication)throwsAuthenticationException{Stringprincipal=(String)authentication.getPrincipal();// username:xxx 或 phone:xxxStringcredentials=(String)authentication.getCredentials();// 密码或验证码UserDetailsuserDetails;if(principal.startsWith("username:")){// 账号密码登录Stringusername=principal.substring("username:".length());userDetails=userDetailsService.loadUserByUsername(username);if(!passwordEncoder.matches(credentials,userDetails.getPassword())){thrownewBadCredentialsException("密码错误");}}elseif(principal.startsWith("phone:")){// 手机号登录StringphoneNumber=principal.substring("phone:".length());userDetails=phoneNumberUserService.loadUserByPhoneNumber(phoneNumber);// 这里验证码校验可放在 service 内,也可前置过滤器else{thrownewBadCredentialsException("登录方式不支持");}// 生成已认证令牌UsernamePasswordAuthenticationTokenresult=newUsernamePasswordAuthenticationToken(userDetails,credentials,userDetails.getAuthorities());result.setDetails(authentication.getDetails());returnresult;}@Overridepublicbooleansupports(Class<?>authentication){returnUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);}}}

注解:

  1. 1.前缀识别:用username:phone:做路由,避免写两套接口。

  2. 2.职责分离:验证码校验交给PhoneNumberUserService,保持单一职责。

  3. 3.线程安全:所有依赖通过构造器注入,无共享可变状态,天然并发友好。

03

双 Service 实现

UserDetailsService(账号密码版)

@Service@RequiredArgsConstructorpublicclassUserDetailsServiceImplimplementsUserDetailsService{privatefinalUserMapperuserMapper;privatefinalMenuMappermenuMapper;@OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{Useruser=userMapper.selectOne(newLambdaQueryWrapper<User>().eq(User::getUserName,username));if(user==null)thrownewUsernameNotFoundException("用户不存在");List<String>perms=menuMapper.selectPermsByUserId(user.getId());perms.add(user.getRoles());// 合并角色returnnewLoginUser(user,perms);}}

PhoneNumberUserService(手机号验证码版)

@Service@RequiredArgsConstructorpublicclassPhoneNumberUserService{privatefinalUserMapperuserMapper;privatefinalMenuMappermenuMapper;privatefinalRedisTemplate<String,String>redisTemplate;// 缓存验证码publicUserDetailsloadUserByPhoneNumber(StringphoneNumber){// 1️ 查库Useruser=userMapper.selectOne(newLambdaQueryWrapper<User>().eq(User::getPhonenumber,phoneNumber));if(user==null)thrownewRuntimeException("手机号未注册");// 2️ 查权限List<String>perms=menuMapper.selectPermsByUserId(user.getId());perms.add(user.getRoles());// 3️验证码校验示例(可前置过滤器)//String codeInRedis = redisTemplate.opsForValue().get("SMS:" + phoneNumber);returnnewLoginUser(user,perms);}}

注解:

  1. 1.LambdaQueryWrapper:MyBatis-Plus 写法,链式清爽。

  2. 2.角色权限合并:把角色当权限塞到同一集合,后续授权更丝滑。

  3. 3.验证码解耦:校验逻辑可放在 Service,也可前置过滤器,灵活插拔。

04

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

@Configuration@EnableWebSecurity@RequiredArgsConstructorpublicclassSecurityConfig{privatefinalAuthenticationConfigurationauthenticationConfiguration;//密码加密器@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}@BeanpublicUserDetailsServiceuserDetailsService(){returnnewUserDetailsServiceImpl();}@BeanpublicPhoneNumberUserServicephoneNumberUserService(){returnnewPhoneNumberUserService();}@BeanpublicCustomAuthenticationProvidercustomAuthenticationProvider(){returnnewCustomAuthenticationProvider(userDetailsService(),passwordEncoder(),phoneNumberUserService());}@BeanpublicAuthenticationManagerauthenticationManager()throwsException{// 替换默认 AuthenticationManagerreturnnewProviderManager(customAuthenticationProvider());}}

注解:

  1. 1.ProviderManager:Spring Security 的核心调度器,塞入我们的 Provider 就能接管认证。

  2. 2.构造器注入:Spring 推荐写法,避免循环依赖。

  3. 3.无 @Autowired:全部显式 Bean,方便单测 Mock。

05

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

@RestController@RequestMapping("/auth")@RequiredArgsConstructorpublicclassAuthController{privatefinalAuthenticationManagerauthenticationManager;privatefinalRedisTemplate<String,Object>redisTemplate;@PostMapping("/login")publicResultlogin(@RequestBodyLoginDTOdto){Stringprincipal=dto.getLoginType()==1?"username:"+dto.getUsername():"phone:"+dto.getPhone();UsernamePasswordAuthenticationTokentoken=newUsernamePasswordAuthenticationToken(principal,dto.getCredential());Authenticationauthenticate=authenticationManager.authenticate(token);LoginUserloginUser=(LoginUser)authenticate.getPrincipal();Stringjwt=JwtUtil.createJWT(loginUser.getUser().getId().toString());redisTemplate.opsForValue().set("login:"+loginUser.getUser().getId(),loginUser);returnResult.OK("登录成功",Map.of("token",jwt));}}

注解:

  1. 1.DTO 统一:前端传loginType=1账号密码,2手机号验证码,后端零 if-else。

  2. 2.JWT + Redis:无状态 Token + 在线用户信息缓存,分布式登录稳稳的。

  3. 3.异常透传:认证失败直接抛异常,被全局异常处理器统一包装,前端拿到统一格式。

测试

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

相关文章:

  • 3步极速方案:轻松破解网盘下载限速难题
  • 老笔记本焕新记:手把手教你给惠普光影精灵2加装三星970 EVO Plus固态和内存条
  • 别再只用AUC了!用Python手写DeLong检验,科学比较两个机器学习模型的性能差异
  • CANopen EDS文件可视化编辑工具集(含DS301/DS401/DSP302模板)
  • 如何总结B站视频整理成知识库,我实测了一年的工作流正式公开
  • 构建有多慢,数据说了算:用Prometheus监控CI/CD流水线中Docker构建性能
  • TCL携手腾讯CodeBuddy:AI重构研发流水线,提效降本开启组织变革
  • 零代码自建进销存 vs 成品SaaS,中小企业该怎么选?2026完整决策指南
  • 基于 ThinkPHP 8 + Vue 3 的 LikeShop:产品矩阵与技术架构概览
  • MATLAB训练好的LSTM模型免编译直通Simulink仿真环境
  • Sora 2简历视频制作实战指南(HR总监认证的ATS友好型脚本结构)
  • 新装麒麟系统软件商店连不上?手把手教你配置软件源和网络权限(避坑指南)
  • 云渲染如何选择?这几点很关键
  • Ai好记 vs Get笔记:AI音视频笔记工具深度测评对比
  • 终极网盘直链下载助手完整指南:九大网盘一键极速下载方案
  • 摄氏度、华氏度、开尔文互转,HarmonyOS TempUtil 六个方法搞定
  • 2026年怎么选稳定安全性价比高的云手机?
  • 蓝牙安全机制与配对绑定
  • 终极网页回溯工具:Wayback Machine浏览器扩展的5个核心功能完全指南
  • 深入Linux内存管理:从Redis的overcommit_memory警告,聊聊OOM Killer和你的服务器稳定性
  • Umi-OCR实战指南:5个场景解锁开源离线OCR工具的高效应用
  • JetBrains Maple Mono:终极开源编程字体融合方案详解
  • hermes日常使用问题
  • 2026年成都搬家公司TOP推荐:技术维度拆解与选择推荐 - 优质品牌商家
  • 如何运输艺术印刷品:运输艺术品的技巧
  • HarmonyOS TypeUtil 基础类型检测详解:isBoolean/isNumber/isString/isObject/isArray 完整教程
  • 华硕笔记本终极性能控制:G-Helper轻量化解决方案完全指南
  • 4G Cat.1 通信模组怎么选?有哪些关键参数?
  • 如何用Path of Building PoE2实现流放之路2角色构建的终极指南:3步打造完美角色
  • 从零打造3D打印井字棋机器人:Arduino与舵机运动控制实战