当前位置: 首页 > 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.zskr.cn/news/1450720.html

相关文章:

  • 3步极速方案:轻松破解网盘下载限速难题
  • 如何总结B站视频整理成知识库,我实测了一年的工作流正式公开
  • Sora 2简历视频制作实战指南(HR总监认证的ATS友好型脚本结构)
  • 蓝牙安全机制与配对绑定
  • 深入Linux内存管理:从Redis的overcommit_memory警告,聊聊OOM Killer和你的服务器稳定性
  • Umi-OCR实战指南:5个场景解锁开源离线OCR工具的高效应用
  • HarmonyOS TypeUtil 基础类型检测详解:isBoolean/isNumber/isString/isObject/isArray 完整教程
  • 如何用Path of Building PoE2实现流放之路2角色构建的终极指南:3步打造完美角色
  • HR做薪酬体系,必须先搞懂岗位价值评估
  • QueryExcel:基于NPOI的Excel批量数据检索系统架构解析
  • 如何用WeChatMsg永久保存微信聊天记录?你的数字记忆守护终极指南
  • 别再浪费你的游戏数据了!用Python+PyTorch实现DQN经验回放(附完整代码)
  • 发现用明道中文编程语言打包的hanoi.exe文件是22M,有点大啊,还能通过什么技术手段更小一些吗?(先维持原样)
  • Claude Code 平替来了?DeepSeek-TUI 保姆级安装教程
  • 性能相当于第四代骁龙8s
  • 双系统党必看:Ubuntu 18.04下Windows 10启动盘制作与bootmgfw.efi丢失修复全记录
  • QRemeshify:基于QuadWild算法的Blender四边形重拓扑技术深度解析
  • 多设备组网与Mesh网络入门
  • 仿真绿植绿化技术核心要点及服务商选择参考推荐:仿真绿植绿化工程/仿真绿植绿化电话/四川仿真绿植绿化/优选指南 - 优质品牌商家
  • GlosSI 入门指南:让 Steam 控制器在任意游戏和应用中畅玩
  • 别再死记硬背了!用Python手撸一个ID3决策树,从熵到分类器一次搞懂
  • 专为食品进出口打造的外贸ERP!智能生成发票、质检报告高效合规
  • 2026年近期如何筛选靠谱的气力输送设备优质厂家:以天顺机械为例的专业解析 - 2026年企业资讯
  • 告别编译噩梦:我在Ubuntu 18.04/20.04上为Xenomai 3.2.1打Linux 5.10.76补丁的五个关键抉择
  • ## 2026深圳全屋定制实测:西丽自有工厂木点点ENF套餐到底值不值? - 产品测评官
  • VoiceFixer语音修复工具:3种模式一键解决噪音、失真和低质量音频问题
  • 组织与交付 如何让产品 工程 合规 在 Agent 项目里不互相拖后腿
  • 三步永久保存微信聊天记录:WeChatMsg免费数据备份终极指南
  • 第41篇|补光与水印:效果选项如何参与最终照片记录
  • 别再手动对比了!Ubuntu 22.04上5分钟搞定Beyond Compare 4安装与汉化(附最新密钥获取方法)