
纲要多因子认证整体流程回顾用户首次登录密码编码升级逻辑迁移至UserService用户状态校验启用、锁定、过期、密码过期提供发送方式选择接口POST /totp/send使用Optional.flatMap与ifPresentOrElse处理多级可选值利用Pair技巧在函数式链中同时返回用户与 TOTP根据MfaType枚举分发至短信或邮件服务TOTP 验证接口POST /totp/verify项目代码结构总览时序图展示发送与验证交互完整可运行示例代码密码升级与用户状态校验用户首次成功登录后如果使用旧的密码编码方式需要自动升级为更安全的编码格式。我们将原本在UserDetailsServiceImplementation中的升级逻辑迁移到UserService以降低对 Spring Security 内部接口的依赖使业务代码更内聚。ServicepublicclassUserService{privatefinalPasswordEncoderpasswordEncoder;privatefinalUserRepositoryuserRepository;publicUserService(PasswordEncoderpasswordEncoder,UserRepositoryuserRepository){this.passwordEncoderpasswordEncoder;this.userRepositoryuserRepository;}publicvoidupgradePasswordEncoding(Useruser,StringrawPassword){if(passwordEncoder.upgradeEncoding(user.getPassword())){user.setPassword(passwordEncoder.encode(rawPassword));userRepository.save(user);}}}在登录成功回调中调用即可if(passwordEncoder.upgradeEncoding(user.getPassword())){userService.upgradePasswordEncoding(user,authentication.getCredentials().toString());}接下来需要校验用户状态是否启用、账户是否过期、是否锁定、密码是否过期。即使当前业务暂未用到这些字段标准逻辑仍应抛出对应的异常。统一使用自定义Problem类型的异常便于前端解析。if(!user.isEnabled()){thrownewAccountProblem(用户未激活请联系管理员,HttpStatus.UNAUTHORIZED);}if(!user.isAccountNonLocked()){thrownewAccountProblem(用户账户已锁定,HttpStatus.UNAUTHORIZED);}if(!user.isAccountNonExpired()){thrownewAccountProblem(用户账户已过期,HttpStatus.UNAUTHORIZED);}if(!user.isCredentialsNonExpired()){thrownewAccountProblem(用户密码已过期,HttpStatus.UNAUTHORIZED);}AccountProblem为自定义异常可参考如下结构publicclassAccountProblemextendsRuntimeException{privatefinalStringtitle;privatefinalHttpStatusstatus;publicAccountProblem(Stringdetail,HttpStatusstatus){super(detail);this.title账户状态异常;this.statusstatus;}// getters}发送方式选择与 TOTP 生成当用户完成第一因素密码验证后系统应提示前端进入二次认证页面。前端通过调用POST /totp/send接口告知后端用户选择的发送方式短信或邮件以及临时标识mfaId。定义请求 DTO 与发送方式枚举publicclassSendTOTPDTO{NotBlankprivateStringmfaId;NotNullprivateMfaTypemfaType;// getters, setters, constructors}publicenumMfaType{SMS,EMAIL}Controller 层接收请求从缓存中获取用户生成 TOTP 并根据选择分发RestControllerRequestMapping(/totp)publicclassMfaController{privatefinalUserCacheServiceuserCacheService;privatefinalUserServiceuserService;privatefinalSmsServicesmsService;privatefinalEmailServiceemailService;publicMfaController(UserCacheServiceuserCacheService,UserServiceuserService,SmsServicesmsService,EmailServiceemailService){this.userCacheServiceuserCacheService;this.userServiceuserService;this.smsServicesmsService;this.emailServiceemailService;}PostMapping(/send)publicResponseEntity?sendTotp(ValidRequestBodySendTOTPDTOdto){userCacheService.retrieveUser(dto.getMfaId()).flatMap(user-userService.createTOTP(user.getMfaKey()).map(totp-newAbstractMap.SimpleImmutableEntry(user,totp))).ifPresentOrElse(pair-{Useruserpair.getKey();Stringtotppair.getValue();if(dto.getMfaType()MfaType.SMS){smsService.send(user.getMobile(),totp);}else{emailService.send(user.getEmail(),totp);}},()-{thrownewInvalidTotpProblem(非法的 TOTP 请求);});returnResponseEntity.ok().build();}}核心技巧说明retrieveUser返回OptionalUsercreateTOTP返回OptionalString。若直接map会得到OptionalOptionalString后续处理繁琐。使用flatMap将嵌套Optional拍平并在map中利用AbstractMap.SimpleImmutableEntry类似 Pair同时携带用户和生成的 TOTP避免专门创建包装类。ifPresentOrElse为函数式处理分支有值时发送无值时抛出异常。UserService.createTOTP简单封装 TOTP 工具类publicOptionalStringcreateTOTP(StringmfaKey){try{StringtotpTotpUtils.generateTotp(mfaKey);returnOptional.of(totp);}catch(Exceptione){returnOptional.empty();}}TOTP 验证用户输入动态验证码后前端调用POST /totp/verify进行校验。请求 DTO 包含mfaId与code。publicclassVerifyTOTPDTO{NotBlankprivateStringmfaId;NotBlankprivateStringcode;// getters, setters, constructors}Controller 处理验证PostMapping(/verify)publicResponseEntityAuthResponseverifyTotp(ValidRequestBodyVerifyTOTPDTOdto){UseruseruserCacheService.verifyTotp(dto.getMfaId(),dto.getCode()).orElseThrow(()-newInvalidTotpProblem(一次性验证码错误));// 登录成功生成最终令牌StringtokenuserService.loginSuccess(user);returnResponseEntity.ok(newAuthResponse(token));}userCacheService.verifyTotp内部会从缓存中取出 TOTP 进行比对并返回完整的用户对象若验证失败则返回Optional.empty()。项目结构概览src/main/java/com/example/security/ ├── config/ │ └── SecurityConfig.java ├── controller/ │ └── MfaController.java ├── dto/ │ ├── SendTOTPDTO.java │ └── VerifyTOTPDTO.java ├── enums/ │ └── MfaType.java ├── exception/ │ └── AccountProblem.java │ └── InvalidTotpProblem.java ├── service/ │ ├── UserService.java │ ├── SmsService.java │ ├── EmailService.java │ └── TotpUtils.java ├── cache/ │ └── UserCacheService.java └── entity/ └── User.java时序图以下时序图展示用户选择发送方式并验证的完整交互流程SmsService/EmailServiceUserServiceUserCacheServiceMfaControllerFrontendSmsService/EmailServiceUserServiceUserCacheServiceMfaControllerFrontendalt[mfaType SMS][mfaType EMAIL]alt[用户存在][用户不存在]alt[验证成功][验证失败]POST /totp/send {mfaId, mfaType}retrieveUser(mfaId)OptionalUsercreateTOTP(user.mfaKey)OptionalTOTP包装为 PairUser, TOTPsend(user.mobile, totp)send(user.email, totp)200 OK抛出 InvalidTotpProblemPOST /totp/verify {mfaId, code}verifyTotp(mfaId, code)OptionalUserloginSuccess(user)token200 AuthResponseOptional.empty()抛出 InvalidTotpProblem完整可运行示例以下提供MfaController的完整代码包含所有依赖注入与异常处理可直接复制至 Spring Boot 项目中使用确保相关 Service 与 DTO 已实现。packagecom.example.security.controller;importcom.example.security.cache.UserCacheService;importcom.example.security.dto.SendTOTPDTO;importcom.example.security.dto.VerifyTOTPDTO;importcom.example.security.dto.AuthResponse;importcom.example.security.entity.User;importcom.example.security.enums.MfaType;importcom.example.security.exception.InvalidTotpProblem;importcom.example.security.service.EmailService;importcom.example.security.service.SmsService;importcom.example.security.service.UserService;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.*;importjavax.validation.Valid;importjava.util.AbstractMap;importjava.util.Optional;RestControllerRequestMapping(/totp)publicclassMfaController{privatefinalUserCacheServiceuserCacheService;privatefinalUserServiceuserService;privatefinalSmsServicesmsService;privatefinalEmailServiceemailService;publicMfaController(UserCacheServiceuserCacheService,UserServiceuserService,SmsServicesmsService,EmailServiceemailService){this.userCacheServiceuserCacheService;this.userServiceuserService;this.smsServicesmsService;this.emailServiceemailService;}PostMapping(/send)publicResponseEntityVoidsendTotp(ValidRequestBodySendTOTPDTOdto){OptionalUseruserOptuserCacheService.retrieveUser(dto.getMfaId());userOpt.flatMap(user-userService.createTOTP(user.getMfaKey()).map(totp-newAbstractMap.SimpleImmutableEntry(user,totp))).ifPresentOrElse(pair-{Useruserpair.getKey();Stringtotppair.getValue();if(dto.getMfaType()MfaType.SMS){smsService.send(user.getMobile(),totp);}else{emailService.send(user.getEmail(),totp);}},()-{thrownewInvalidTotpProblem(非法的 TOTP 请求);});returnResponseEntity.ok().build();}PostMapping(/verify)publicResponseEntityAuthResponseverifyTotp(ValidRequestBodyVerifyTOTPDTOdto){UseruseruserCacheService.verifyTotp(dto.getMfaId(),dto.getCode()).orElseThrow(()-newInvalidTotpProblem(一次性验证码错误));StringtokenuserService.loginSuccess(user);returnResponseEntity.ok(newAuthResponse(token));}}代码依赖的 DTO 与枚举packagecom.example.security.dto;importcom.example.security.enums.MfaType;importjavax.validation.constraints.NotBlank;importjavax.validation.constraints.NotNull;publicclassSendTOTPDTO{NotBlankprivateStringmfaId;NotNullprivateMfaTypemfaType;// getters and setters omitted for brevity}packagecom.example.security.dto;importjavax.validation.constraints.NotBlank;publicclassVerifyTOTPDTO{NotBlankprivateStringmfaId;NotBlankprivateStringcode;}packagecom.example.security.enums;publicenumMfaType{SMS,EMAIL}异常类示例packagecom.example.security.exception;publicclassInvalidTotpProblemextendsRuntimeException{publicInvalidTotpProblem(Stringmessage){super(message);}}总结本文基于 Spring Security 与 OAuth2 体系完善了多因子认证中“发送方式选择”与“TOTP 验证”两个关键后端接口。通过迁移密码升级逻辑、细化用户状态检查、使用Optional.flatMap与Pair技巧优化函数式处理使代码更加简洁且职责清晰。文中的代码可直接集成至现有认证流程提升系统安全性。