Java框架快速入门: Spring Security+OAuth2之云服务集成与多因子认证设计 📅 发布时间:2026/9/12 16:26:08 👁 浏览次数: 纲要云服务认证基础AccessKey ID与AccessKey Secret的密钥对模型短信服务要素签名、模板跨平台对照阿里云、Leancloud邮件发送方案SMTP与Web API的对比与选型邮件服务中的API Key鉴权多因子认证MFA架构设计用户实体扩展mfaEnabled与totpSecret字段TOTP密钥生成与持久化两步验证的交互流程利用请求 ID 与缓存实现无状态关联代码实现项目结构树用户实体、TOTP 服务、MFA 服务阿里云短信客户端封装可视化流程MFA 认证时序图请求 ID 与缓存映射类图云服务认证与基础概念绝大多数云服务都通过一对密钥来标识调用方身份并完成请求签名。常见命名形式为AccessKey ID与AccessKey Secret也可能称为AppID / AppSecret或API Key / Secret Key。ID作为公开标识Secret用于生成签名两者缺一不可。以阿里云短信服务为例需在 RAM 控制台创建AccessKey获得上述密钥对。同时还需要配置短信签名标识发送者身份如“XX科技”需提前审核。短信模板定义短信正文结构及占位符不同业务场景使用不同模板通过TemplateCode指定。Leancloud 等 BaaS 平台同样提供AppID与AppKey部分还提供MasterKey用于管理操作。其短信服务也要求签名和模板并建议绑定自定义 API 域名以避免默认域名的限流问题。邮件服务SMTP 与 Web API方式原理优点缺点SMTP直连邮件服务器端口按协议投递标准化、通用性强连接耗时、资源占用高Web API调用 RESTful 接口异步发送响应快、解耦业务系统依赖服务商 API 稳定性生产环境推荐Web API。服务商如 SendGrid通常提供一个API Key在 SMTP 场景作为密码在 API 场景作为Bearer Token使用。这种方式交互时间短不会阻塞应用线程。多因子认证MFA设计用户模型扩展在用户实体中增加两个关键字段mfaEnabled布尔型标识是否启用多因子认证。可由用户自主开启也可由管理员统一控制灵活性高。totpSecret字符串存储该用户的 TOTP 共享密钥每个用户唯一。密钥生成后持久化到数据库用于每次验证码校验。两步验证流程用户使用用户名/密码完成第一级认证后若mfaEnabledtrue服务端不直接颁发令牌而是返回一个特殊响应HTTP 401 Unauthorized X-Auth-Identity: MFA_REQUIRED, requestIduuid其中requestId是一个临时凭证服务端将其与已认证的用户信息绑定存入缓存如 Redis设置较短过期时间。客户端引导用户输入 TOTP 验证码再次请求验证接口并附上requestId和验证码。服务端根据requestId从缓存取出用户主体校验 TOTP成功后生成正式令牌并清除缓存。整个过程无需在第二次请求中重复传递密码保持了无状态服务的简洁性。项目结构树src/main/java/com/example/security/ ├── config/ │ ├── SecurityConfig.java │ └── RedisConfig.java ├── entity/ │ └── User.java ├── repository/ │ └── UserRepository.java ├── service/ │ ├── TotpService.java │ ├── SmsService.java │ └── MfaService.java ├── filter/ │ └── MfaAuthenticationFilter.java └── dto/ ├── MfaRequiredResponse.java └── MfaVerifyRequest.java核心代码实现用户实体packagecom.example.security.entity;importjakarta.persistence.*;EntityTable(nameusers)publicclassUser{IdGeneratedValue(strategyGenerationType.IDENTITY)privateLongid;Column(nullablefalse,uniquetrue)privateStringusername;Column(nullablefalse)privateStringpassword;Column(nullablefalse)privatebooleanmfaEnabledfalse;Column(length64)privateStringtotpSecret;// 省略 getter/setter}TOTP 密钥生成与验证服务packagecom.example.security.service;importcom.eatthepath.otp.TimeBasedOneTimePasswordGenerator;importorg.apache.commons.codec.binary.Base32;importjavax.crypto.spec.SecretKeySpec;importjava.security.Key;importjava.security.SecureRandom;importjava.time.Instant;publicclassTotpService{privatestaticfinalTimeBasedOneTimePasswordGeneratortotpnewTimeBasedOneTimePasswordGenerator();publicStringgenerateSecret(){SecureRandomrandomnewSecureRandom();byte[]bytesnewbyte[20];random.nextBytes(bytes);returnnewBase32().encodeToString(bytes);}publicbooleanvalidateCode(Stringsecret,Stringcode){try{byte[]decodedKeynewBase32().decode(secret);KeykeynewSecretKeySpec(decodedKey,HmacSHA1);Stringexpectedtotp.generateOneTimePassword(key,Instant.now());returnexpected.equals(code);}catch(Exceptione){returnfalse;}}}MFA 服务与缓存交互packagecom.example.security.service;importcom.example.security.dto.MfaRequiredResponse;importcom.example.security.entity.User;importcom.example.security.repository.UserRepository;importorg.springframework.data.redis.core.StringRedisTemplate;importjava.util.UUID;importjava.util.concurrent.TimeUnit;publicclassMfaService{privatefinalStringRedisTemplateredisTemplate;privatefinalTotpServicetotpService;publicMfaService(StringRedisTemplateredisTemplate,TotpServicetotpService){this.redisTemplateredisTemplate;this.totpServicetotpService;}publicMfaRequiredResponserequireMfa(Useruser){StringrequestIdUUID.randomUUID().toString();redisTemplate.opsForValue().set(mfa:requestId,user.getUsername(),5,TimeUnit.MINUTES);returnnewMfaRequiredResponse(MFA_REQUIRED,requestId);}publicbooleanverifyMfa(StringrequestId,Stringcode,UserRepositoryuserRepo){StringusernameredisTemplate.opsForValue().get(mfa:requestId);if(usernamenull){returnfalse;}UseruseruserRepo.findByUsername(username).orElse(null);if(usernull||!user.isMfaEnabled()){returnfalse;}booleanvalidtotpService.validateCode(user.getTotpSecret(),code);if(valid){redisTemplate.delete(mfa:requestId);}returnvalid;}}阿里云短信客户端示例packagecom.example.security.service;importcom.aliyun.dysmsapi20170525.Client;importcom.aliyun.dysmsapi20170525.models.SendSmsRequest;importcom.aliyun.teaopenapi.models.Config;publicclassSmsService{privatefinalClientclient;privatefinalStringsignName;privatefinalStringtemplateCode;publicSmsService(StringaccessKeyId,StringaccessKeySecret,StringsignName,StringtemplateCode)throwsException{ConfigconfignewConfig().setAccessKeyId(accessKeyId).setAccessKeySecret(accessKeySecret);config.endpointdysmsapi.aliyuncs.com;this.clientnewClient(config);this.signNamesignName;this.templateCodetemplateCode;}publicvoidsend(StringphoneNumber,Stringcode){SendSmsRequestrequestnewSendSmsRequest().setPhoneNumbers(phoneNumber).setSignName(signName).setTemplateCode(templateCode).setTemplateParam({\code\:\code\});client.sendSms(request);}}流程可视化MFA 认证时序图RedisAuthServerClientRedisAuthServerClientalt[校验通过][校验失败]alt[用户未启用 MFA][用户已启用 MFA]POST /login (username, password)验证用户名/密码200 返回 access_tokenSET mfa:{requestId} username (TTL 5min)401 X-Auth-Identity: MFA_REQUIRED, requestId弹出二次验证输入框POST /auth/mfa/verify (requestId, code)GET mfa:{requestId}username加载用户, 校验 TOTPDEL mfa:{requestId}200 返回 access_token403 验证码错误请求 ID 与缓存键结构写入读取并校验加载LoginStep1String usernameString passwordMfaRequiredResponseString header: X-Auth-IdentityString requestIdRedisCachekey: mfa: requestIdvalue: usernamettl: 5minMfaVerifyRequestString requestIdString codeUserString totpSecretboolean mfaEnabled总结本文梳理了云服务集成中的认证术语与 MFA 架构设计思路。通过扩展用户实体、引入 TOTP 密钥并借助请求 ID 与 Redis 缓存可在 Spring Security OAuth2 体系下实现无状态的两步验证流程。文中给出的阿里云短信封装、TOTP 校验等代码可作为实际开发的起点开发者可根据具体云平台接入对应的 SDK。