Java框架快速入门: Spring Security+OAuth2之多因子认证逻辑与实体类改造

发布时间:2026/9/13 6:29:22
Java框架快速入门: Spring Security+OAuth2之多因子认证逻辑与实体类改造
纲要多因子认证MFA核心概念usingMFA布尔字段标识用户是否启用两步验证MFAKey每个用户独立的 TOTP 密钥X-MFA-Delicate自定义响应头携带MFA标识及requestId用户缓存第一步认证成功后暂存用户信息生成requestId用于第二步验证关联核心流程用户名密码登录第一步查询用户并校验密码检查usingMFA字段false直接签发JWT登录完成true缓存用户信息生成requestId返回401及自定义头X-MFA-Delicate: MFA, requestIdxxx前端接收后跳转至验证码输入页面提交requestId与 TOTP 验证码后端从缓存获取用户取出MFAKey验证 TOTP验证通过后签发JWT登录完成涉及的实体与代码改造User实体新增字段UserService增加密码匹配与认证逻辑LoginController改造登录接口区分 MFA 与非 MFA 响应UserCacheService设计缓存接口后续实现多因子认证流程概览多因子认证MFA是在传统用户名密码基础上增加一层验证的安全机制。在已具备 TOTP 生成、验证以及邮件/短信发送能力后需要将其组装成完整的两步认证流程。整体流程如下图所示服务端客户端服务端客户端alt[验证成功][验证失败]alt[usingMFA false][usingMFA true]POST /login (username, password)校验用户名密码200 OK (JWT Token)缓存用户信息生成 requestId401 UnauthorizedX-MFA-Delicate: MFA, requestIdxxx跳转验证码页面POST /mfa/verify (requestId, code)从缓存获取用户取出 MFAKey验证 TOTP200 OK (JWT Token)401 验证码错误实体类改造为了支持按用户启用 MFAUser实体需要增加两个字段usingMfa是否启用两步验证和mfaKeyTOTP 密钥。同时注意以下几点使用Column精确映射数据库字段usingMfa默认值为falsemfaKey使用JsonIgnore避免在 JSON 序列化时泄露使用 LombokBuilder时需为usingMfa设置默认值示例packagecom.example.demo.entity;importcom.fasterxml.jackson.annotation.JsonIgnore;importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;importjavax.persistence.*;DataBuilderNoArgsConstructorAllArgsConstructorEntityTable(nameusers)publicclassUser{IdGeneratedValue(strategyGenerationType.IDENTITY)privateLongid;Column(nullablefalse,uniquetrue)privateStringusername;Column(nullablefalse)privateStringpassword;Column(nullablefalse)privatebooleanenabled;Column(nameusing_mfa,nullablefalse)Builder.DefaultprivatebooleanusingMfafalse;Column(namemfa_key)JsonIgnoreprivateStringmfaKey;}数据库对应的表结构示例 SQLCREATETABLEusers(idBIGINTAUTO_INCREMENTPRIMARYKEY,usernameVARCHAR(50)NOTNULLUNIQUE,passwordVARCHAR(200)NOTNULL,enabledBOOLEANNOTNULLDEFAULTTRUE,using_mfaBOOLEANNOTNULLDEFAULTFALSE,mfa_keyVARCHAR(100));用户注册时生成 MFA Key在用户注册流程中若需为用户预先生成 TOTP 密钥可通过注入TOTP工具类完成。以下示例展示如何在UserService的注册方法中自动生成并存储密钥ServicepublicclassUserService{privatefinalUserRepositoryuserRepository;privatefinalPasswordEncoderpasswordEncoder;privatefinalTOTPtotp;// 假设已实现的TOTP工具类publicUserService(UserRepositoryuserRepository,PasswordEncoderpasswordEncoder,TOTPtotp){this.userRepositoryuserRepository;this.passwordEncoderpasswordEncoder;this.totptotp;}publicUserregister(Stringusername,StringrawPassword){StringmfaKeytotp.generateKey();// 生成密钥并转为String存储UseruserUser.builder().username(username).password(passwordEncoder.encode(rawPassword)).enabled(true).usingMfa(false)// 默认不启用可由用户后续自行开启.mfaKey(mfaKey).build();returnuserRepository.save(user);}}登录逻辑改造原有登录接口直接返回 JWT引入 MFA 后需根据用户usingMfa字段决定响应方式。以下是改造后的控制器核心逻辑。首先为UserService添加一个按用户名和原始密码进行认证的方法返回OptionalUserServicepublicclassUserService{// 其他注入...publicOptionalUserauthenticate(Stringusername,StringrawPassword){returnuserRepository.findByUsername(username).filter(user-passwordEncoder.matches(rawPassword,user.getPassword())).filter(user-user.isEnabled());// 可加入更多账户状态检查}}UserRepository需提供findByUsername方法publicinterfaceUserRepositoryextendsJpaRepositoryUser,Long{OptionalUserfindByUsername(Stringusername);}然后在登录控制器中根据usingMfa分流处理RestControllerpublicclassAuthController{privatefinalUserServiceuserService;privatefinalUserCacheServiceuserCacheService;publicAuthController(UserServiceuserService,UserCacheServiceuserCacheService){this.userServiceuserService;this.userCacheServiceuserCacheService;}PostMapping(/login)publicResponseEntity?login(RequestBodyLoginRequestrequest){OptionalUseruserOptuserService.authenticate(request.getUsername(),request.getPassword());if(userOpt.isEmpty()){returnResponseEntity.status(HttpStatus.UNAUTHORIZED).body(用户名或密码错误);}UseruseruserOpt.get();if(!user.isUsingMfa()){// 未启用MFA直接返回JWTStringtokengenerateToken(user);// 自行实现JWT生成returnResponseEntity.ok(newAuthResponse(token));}// 启用MFA缓存用户信息并返回特殊响应StringrequestIduserCacheService.cacheUser(user);HttpHeadersheadersnewHttpHeaders();headers.add(X-MFA-Delicate,MFA, requestIdrequestId);returnResponseEntity.status(HttpStatus.UNAUTHORIZED).headers(headers).body(需要两步验证);}privateStringgenerateToken(Useruser){// 实际使用JWT工具生成此处省略returnjwt-token-for-user.getUsername();}}请求体与响应体可定义为简单的 DTO// LoginRequest.javaDatapublicclassLoginRequest{privateStringusername;privateStringpassword;}// AuthResponse.javaDataAllArgsConstructorpublicclassAuthResponse{privateStringtoken;}第二步验证接口前端收到401和X-MFA-Delicate头后引导用户输入 TOTP 验证码然后调用第二步验证接口。该接口从缓存中取出用户使用其mfaKey进行 TOTP 校验成功后返回 JWT。PostMapping(/mfa/verify)publicResponseEntity?verifyMfa(RequestBodyMfaVerifyRequestrequest){OptionalUseruserOptuserCacheService.getUser(request.getRequestId());if(userOpt.isEmpty()){returnResponseEntity.status(HttpStatus.UNAUTHORIZED).body(会话已过期请重新登录);}UseruseruserOpt.get();// 假设TOTP验证工具类已注入booleanisValidtotp.verify(user.getMfaKey(),request.getCode());if(!isValid){returnResponseEntity.status(HttpStatus.UNAUTHORIZED).body(验证码错误);}// 验证成功清除缓存返回JWTuserCacheService.removeUser(request.getRequestId());StringtokengenerateToken(user);returnResponseEntity.ok(newAuthResponse(token));}// MfaVerifyRequest.javaDatapublicclassMfaVerifyRequest{privateStringrequestId;privateStringcode;}用户缓存服务设计UserCacheService负责暂存第一步认证成功的用户信息并生成唯一requestId。这里使用内存ConcurrentHashMap实现一个简单版本实际生产环境应使用 Redis 并设置过期时间。ServicepublicclassUserCacheService{privatefinalMapString,UsercachenewConcurrentHashMap();publicStringcacheUser(Useruser){StringrequestIdUUID.randomUUID().toString();cache.put(requestId,user);returnrequestId;}publicOptionalUsergetUser(StringrequestId){returnOptional.ofNullable(cache.get(requestId));}publicvoidremoveUser(StringrequestId){cache.remove(requestId);}}项目结构概览改造涉及的文件与层级关系src/main/java/com/example/demo/ ├── entity │ └── User.java ├── repository │ └── UserRepository.java ├── service │ ├── UserService.java │ └── UserCacheService.java ├── controller │ └── AuthController.java ├── dto │ ├── LoginRequest.java │ ├── AuthResponse.java │ └── MfaVerifyRequest.java └── DemoApplication.java安全考量与扩展密码存储始终使用PasswordEncoder如 BCrypt进行哈希处理缓存安全requestId应具有时效性建议使用 Redis 并设置 3‑5 分钟过期自定义响应头X-MFA-Delicate虽非标准头但可被前端识别并用于流程控制避免将认证状态放在普通响应体中而引发歧义灵活启用 MFAusingMfa字段赋予业务极大的灵活性可按角色、风险等级动态调整例如异地登录时强制要求两步验证以上内容完整展示了基于 Spring Security 与 OAuth2 生态下多因子认证逻辑的实体改造、认证分流及缓存配合方案。后续可进一步集成 Spring Security 的过滤器链将 MFA 校验作为额外认证提供者实现更平滑的鉴权集成。