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

发布时间:2026/6/3 1:50:23

Spring Security自定义AuthenticationManager实现手机号/密码双认证 01整体思路 3 步走1.自定义认证提供者CustomAuthenticationProvider识别登录方式分发给对应UserDetailsService。2.双 Service•UserDetailsService验证账号密码•PhoneNumberUserService验证手机号验证码3.配置注入把自定义提供者塞进 Spring Security让它乖乖听话。02自定义认证提供者publicclassCustomAuthenticationProviderimplementsAuthenticationProvider{privatefinalUserDetailsServiceuserDetailsService;// 账号密码验证privatefinalPasswordEncoderpasswordEncoder;// 密码加密器privatefinalPhoneNumberUserServicephoneNumberUserService;// 手机号验证publicCustomAuthenticationProvider(UserDetailsServiceuserDetailsService,PasswordEncoderpasswordEncoder,PhoneNumberUserServicephoneNumberUserService){this.userDetailsServiceuserDetailsService;this.passwordEncoderpasswordEncoder;this.phoneNumberUserServicephoneNumberUserService;}OverridepublicAuthenticationauthenticate(Authenticationauthentication)throwsAuthenticationException{Stringprincipal(String)authentication.getPrincipal();// username:xxx 或 phone:xxxStringcredentials(String)authentication.getCredentials();// 密码或验证码UserDetailsuserDetails;if(principal.startsWith(username:)){// 账号密码登录Stringusernameprincipal.substring(username:.length());userDetailsuserDetailsService.loadUserByUsername(username);if(!passwordEncoder.matches(credentials,userDetails.getPassword())){thrownewBadCredentialsException(密码错误);}}elseif(principal.startsWith(phone:)){// 手机号登录StringphoneNumberprincipal.substring(phone:.length());userDetailsphoneNumberUserService.loadUserByPhoneNumber(phoneNumber);// 这里验证码校验可放在 service 内也可前置过滤器else{thrownewBadCredentialsException(登录方式不支持);}// 生成已认证令牌UsernamePasswordAuthenticationTokenresultnewUsernamePasswordAuthenticationToken(userDetails,credentials,userDetails.getAuthorities());result.setDetails(authentication.getDetails());returnresult;}Overridepublicbooleansupports(Class?authentication){returnUsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);}}}注解1.前缀识别用username:和phone:做路由避免写两套接口。2.职责分离验证码校验交给PhoneNumberUserService保持单一职责。3.线程安全所有依赖通过构造器注入无共享可变状态天然并发友好。03双 Service 实现UserDetailsService账号密码版ServiceRequiredArgsConstructorpublicclassUserDetailsServiceImplimplementsUserDetailsService{privatefinalUserMapperuserMapper;privatefinalMenuMappermenuMapper;OverridepublicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{UseruseruserMapper.selectOne(newLambdaQueryWrapperUser().eq(User::getUserName,username));if(usernull)thrownewUsernameNotFoundException(用户不存在);ListStringpermsmenuMapper.selectPermsByUserId(user.getId());perms.add(user.getRoles());// 合并角色returnnewLoginUser(user,perms);}}PhoneNumberUserService手机号验证码版ServiceRequiredArgsConstructorpublicclassPhoneNumberUserService{privatefinalUserMapperuserMapper;privatefinalMenuMappermenuMapper;privatefinalRedisTemplateString,StringredisTemplate;// 缓存验证码publicUserDetailsloadUserByPhoneNumber(StringphoneNumber){// 1️ 查库UseruseruserMapper.selectOne(newLambdaQueryWrapperUser().eq(User::getPhonenumber,phoneNumber));if(usernull)thrownewRuntimeException(手机号未注册);// 2️ 查权限ListStringpermsmenuMapper.selectPermsByUserId(user.getId());perms.add(user.getRoles());// 3️验证码校验示例可前置过滤器//String codeInRedis redisTemplate.opsForValue().get(SMS: phoneNumber);returnnewLoginUser(user,perms);}}注解1.LambdaQueryWrapperMyBatis-Plus 写法链式清爽。2.角色权限合并把角色当权限塞到同一集合后续授权更丝滑。3.验证码解耦校验逻辑可放在 Service也可前置过滤器灵活插拔。04SecurityConfig把自定义提供者塞进去ConfigurationEnableWebSecurityRequiredArgsConstructorpublicclassSecurityConfig{privatefinalAuthenticationConfigurationauthenticationConfiguration;//密码加密器BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}BeanpublicUserDetailsServiceuserDetailsService(){returnnewUserDetailsServiceImpl();}BeanpublicPhoneNumberUserServicephoneNumberUserService(){returnnewPhoneNumberUserService();}BeanpublicCustomAuthenticationProvidercustomAuthenticationProvider(){returnnewCustomAuthenticationProvider(userDetailsService(),passwordEncoder(),phoneNumberUserService());}BeanpublicAuthenticationManagerauthenticationManager()throwsException{// 替换默认 AuthenticationManagerreturnnewProviderManager(customAuthenticationProvider());}}注解1.ProviderManagerSpring Security 的核心调度器塞入我们的 Provider 就能接管认证。2.构造器注入Spring 推荐写法避免循环依赖。3.无 Autowired全部显式 Bean方便单测 Mock。05登录接口一行代码双通道RestControllerRequestMapping(/auth)RequiredArgsConstructorpublicclassAuthController{privatefinalAuthenticationManagerauthenticationManager;privatefinalRedisTemplateString,ObjectredisTemplate;PostMapping(/login)publicResultlogin(RequestBodyLoginDTOdto){Stringprincipaldto.getLoginType()1?username:dto.getUsername():phone:dto.getPhone();UsernamePasswordAuthenticationTokentokennewUsernamePasswordAuthenticationToken(principal,dto.getCredential());AuthenticationauthenticateauthenticationManager.authenticate(token);LoginUserloginUser(LoginUser)authenticate.getPrincipal();StringjwtJwtUtil.createJWT(loginUser.getUser().getId().toString());redisTemplate.opsForValue().set(login:loginUser.getUser().getId(),loginUser);returnResult.OK(登录成功,Map.of(token,jwt));}}注解1.DTO 统一前端传loginType1账号密码2手机号验证码后端零 if-else。2.JWT Redis无状态 Token 在线用户信息缓存分布式登录稳稳的。3.异常透传认证失败直接抛异常被全局异常处理器统一包装前端拿到统一格式。测试登录方式请求体返回账号密码{loginType:1,username:yuqn,credential:123456}{msg:登录成功,token:eyJ...}手机验证码{loginType:2,phone:13800138000,credential:8888}同上

相关新闻