ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java框架快速入门: Spring Security+OAuth2之多因子认证缓存服务

Java框架快速入门: Spring Security+OAuth2之多因子认证缓存服务 纲要多因子认证(MFA)与TOTP核心流程为何需要缓存用户临时状态技术选型RedisRedisson客户端核心实现UserCacheService生成随机MFA标识并存储用户信息设置基于TOTP时间步长的过期时间按MFA ID获取缓存用户校验TOTP码并清理缓存嵌入式Redis测试环境代码结构与可运行示例启动与验证背景与问题在实现基于时间的一次性密码TOTP的多因子认证时用户初次登录后需要输入六位验证码。服务端必须在两次请求之间记住“正在等待MFA验证”的用户信息且该信息只是临时有效通常几分钟。直接使用数据库会增加无效IO且可能遗留脏数据引入带有时效的缓存是最佳选择。本文基于Spring Boot Spring Security环境使用Redis作为缓存中间件Redisson作为访问客户端封装一个轻量级的UserCacheService完整展示多因子认证中的缓存逻辑。依赖与环境核心依赖在pom.xml中如下所示dependencygroupIdorg.redisson/groupIdartifactIdredisson-spring-boot-starter/artifactIdversion3.17.5/version/dependency!-- 嵌入式Redis仅用于测试 --dependencygroupIdit.ozimov/groupIdartifactIdembedded-redis/artifactIdversion0.7.3/versionscopetest/scope/dependencyRedisson负责与Redis交互嵌入式Redis可以让你在本地测试时无需单独启动Redis服务。常量定义为缓存区域命名并统一管理在常量接口中定义一个字符串常量packagecom.example.security.constant;publicinterfaceConstants{StringUSER_MFA_CACHEuserMfaCache;}这个名称将作为Redis中Map结构的标识。工具类随机MFA ID生成当用户进入MFA验证阶段时服务端生成一个唯一随机串作为Key与用户绑定存入Redis。工具类如下packagecom.example.security.util;importorg.springframework.stereotype.Component;importjava.security.SecureRandom;ComponentpublicclassRandomUtil{privatestaticfinalStringCHARSABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789;privatefinalSecureRandomrandomnewSecureRandom();publicStringgenerateRandomString(intlength){StringBuildersbnewStringBuilder(length);for(inti0;ilength;i){sb.append(CHARS.charAt(random.nextInt(CHARS.length())));}returnsb.toString();}}这里生成12位字母数字混合串可根据安全需求调整长度。核心服务UserCacheService该Service负责缓存待验证的用户cacheUser返回MFA ID。根据MFA ID取出用户getUser。校验TOTP码并清除缓存verifyTotp返回验证成功的用户。完整实现如下packagecom.example.security.service;importcom.example.security.constant.Constants;importcom.example.security.model.User;importcom.example.security.util.RandomUtil;importlombok.RequiredArgsConstructor;importorg.redisson.api.RMapCache;importorg.redisson.api.RedissonClient;importorg.springframework.stereotype.Service;importjava.util.Optional;importjava.util.concurrent.TimeUnit;ServiceRequiredArgsConstructorpublicclassUserCacheService{privatefinalRedissonClientredissonClient;privatefinalRandomUtilrandomUtil;/** * 缓存用户返回MFA ID * param user 待验证用户 * return 生成的MFA ID */publicStringcacheUser(Useruser){RMapCacheString,UsercacheredissonClient.getMapCache(Constants.USER_MFA_CACHE);StringmfaIdrandomUtil.generateRandomString(12);// 如果缓存中不存在该ID则放入if(!cache.containsKey(mfaId)){intttlInSecondsTotpUtil.DEFAULT_TIME_STEP_SECONDS;// 例如300秒cache.put(mfaId,user,ttlInSeconds,TimeUnit.SECONDS);}returnmfaId;}/** * 根据MFA ID获取缓存的用户 * param mfaId 前端带回的MFA ID * return Optional包装的用户 */publicOptionalUsergetUser(StringmfaId){RMapCacheString,UsercacheredissonClient.getMapCache(Constants.USER_MFA_CACHE);if(cache.containsKey(mfaId)){returnOptional.ofNullable(cache.get(mfaId));}returnOptional.empty();}/** * 验证TOTP码成功则删除缓存并返回用户 * param mfaId MFA ID * param code 用户提交的验证码 * return 验证成功返回用户否则返回Optional.empty() */publicOptionalUserverifyTotp(StringmfaId,Stringcode){RMapCacheString,UsercacheredissonClient.getMapCache(Constants.USER_MFA_CACHE);// 先检查缓存是否存在if(!cache.containsKey(mfaId)){returnOptional.empty();}UsercachedUsercache.get(mfaId);if(cachedUsernull){returnOptional.empty();}try{byte[]secretKeyBase64.getDecoder().decode(cachedUser.getMfaSecret());if(TotpUtil.verifyTotp(secretKey,code)){// 验证成功移除缓存cache.remove(mfaId);returnOptional.of(cachedUser);}}catch(Exceptione){// 密钥无效等异常视为验证失败}returnOptional.empty();}}要点说明RMapCache提供了带TTL的Map结构put(key, value, ttl, timeUnit)会在TTL到期后自动清除。TTL取自由TotpUtil定义的DEFAULT_TIME_STEP_SECONDS通常300秒即5分钟与TOTP窗口保持一致。verifyTotp中解码用户的MFA密钥Base64存储调用TotpUtil.verifyTotp验证动态码成功后移除缓存保证一次性使用。TOTP工具参考为完整性给出TotpUtil中的关键常量与方法签名packagecom.example.security.util;publicclassTotpUtil{publicstaticfinalintDEFAULT_TIME_STEP_SECONDS300;// 5分钟publicstaticbooleanverifyTotp(byte[]secret,Stringcode){// 使用TOTP算法校验此处省略具体实现可使用Google Auth库}}用户实体示例packagecom.example.security.model;importlombok.Data;DatapublicclassUser{privateLongid;privateStringusername;privateStringpassword;privatebooleanusingMfa;privateStringmfaSecret;// Base64编码的TOTP密钥}集成到认证流程下面是一个简化的Controller展示如何调用packagecom.example.security.controller;importcom.example.security.model.User;importcom.example.security.service.UserCacheService;importlombok.RequiredArgsConstructor;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.*;importjava.util.Map;importjava.util.Optional;RestControllerRequestMapping(/auth)RequiredArgsConstructorpublicclassAuthController{privatefinalUserCacheServiceuserCacheService;/** * 第一因子认证通过后生成MFA ID返回给前端 */PostMapping(/mfa/init)publicResponseEntity?initMfa(RequestBodyUserauthenticatedUser){StringmfaIduserCacheService.cacheUser(authenticatedUser);returnResponseEntity.ok(Map.of(mfaId,mfaId));}/** * 前端提交MFA ID和验证码完成验证 */PostMapping(/mfa/verify)publicResponseEntity?verifyMfa(RequestParamStringmfaId,RequestParamStringcode){OptionalUserverifiedUseruserCacheService.verifyTotp(mfaId,code);if(verifiedUser.isPresent()){// 签发JWT或创建SecurityContextreturnResponseEntity.ok(Map.of(token,generated_jwt_token));}returnResponseEntity.status(401).body(MFA verification failed);}}启动与验证1. 启动Redis在开发环境可使用Docker快速启动dockerrun-d--nameredis-p6379:6379 redis:7-alpine2. 配置application.ymlspring:redis:host:localhostport:6379# Redisson 自动配置无需额外配置3. 验证接口向/auth/mfa/init发送第一因子认证通过的用户JSON得到mfaId随后调用/auth/mfa/verify?mfaIdxxxcode123456完成MFA验证。项目代码结构src/main/java/com/example/security/ ├── constant/ │ └── Constants.java ├── controller/ │ └── AuthController.java ├── model/ │ └── User.java ├── service/ │ └── UserCacheService.java └── util/ ├── RandomUtil.java └── TotpUtil.java总结本文介绍了在 Spring Security OAuth2 环境下实现 TOTP 多因子认证时通过 Redis 缓存临时用户状态的完整方案。核心思路是用户通过第一因子认证后服务端生成随机 MFA ID将用户对象以该 ID 为键存入带 TTL 的 Redis Map 缓存TTL 与 TOTP 时间步长一致。验证时前端回传 MFA ID 与动态码服务端取出用户、校验 TOTP 并立即删除缓存确保一次有效。采用 Redisson 的 RMapCache 简化过期控制结合嵌入式 Redis 完成测试最终将缓存服务融入认证 Controller形成可运行的轻量级多因子认证缓存层。
返回列表