纲要
- 云服务认证基础
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 共享密钥,每个用户唯一。密钥生成后持久化到数据库,用于每次验证码校验。
两步验证流程
用户使用用户名/密码完成第一级认证后,若mfaEnabled=true,服务端不直接颁发令牌,而是返回一个特殊响应:
HTTP 401 Unauthorized X-Auth-Identity: MFA_REQUIRED, requestId=<uuid>其中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.*;@Entity@Table(name="users")publicclassUser{@Id@GeneratedValue(strategy=GenerationType.IDENTITY)privateLongid;@Column(nullable=false,unique=true)privateStringusername;@Column(nullable=false)privateStringpassword;@Column(nullable=false)privatebooleanmfaEnabled=false;@Column(length=64)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{privatestaticfinalTimeBasedOneTimePasswordGeneratortotp=newTimeBasedOneTimePasswordGenerator();publicStringgenerateSecret(){SecureRandomrandom=newSecureRandom();byte[]bytes=newbyte[20];random.nextBytes(bytes);returnnewBase32().encodeToString(bytes);}publicbooleanvalidateCode(Stringsecret,Stringcode){try{byte[]decodedKey=newBase32().decode(secret);Keykey=newSecretKeySpec(decodedKey,"HmacSHA1");Stringexpected=totp.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.redisTemplate=redisTemplate;this.totpService=totpService;}publicMfaRequiredResponserequireMfa(Useruser){StringrequestId=UUID.randomUUID().toString();redisTemplate.opsForValue().set("mfa:"+requestId,user.getUsername(),5,TimeUnit.MINUTES);returnnewMfaRequiredResponse("MFA_REQUIRED",requestId);}publicbooleanverifyMfa(StringrequestId,Stringcode,UserRepositoryuserRepo){Stringusername=redisTemplate.opsForValue().get("mfa:"+requestId);if(username==null){returnfalse;}Useruser=userRepo.findByUsername(username).orElse(null);if(user==null||!user.isMfaEnabled()){returnfalse;}booleanvalid=totpService.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{Configconfig=newConfig().setAccessKeyId(accessKeyId).setAccessKeySecret(accessKeySecret);config.endpoint="dysmsapi.aliyuncs.com";this.client=newClient(config);this.signName=signName;this.templateCode=templateCode;}publicvoidsend(StringphoneNumber,Stringcode){SendSmsRequestrequest=newSendSmsRequest().setPhoneNumbers(phoneNumber).setSignName(signName).setTemplateCode(templateCode).setTemplateParam("{\"code\":\""+code+"\"}");client.sendSms(request);}}流程可视化
MFA 认证时序图
请求 ID 与缓存键结构
总结
本文梳理了云服务集成中的认证术语与 MFA 架构设计思路。通过扩展用户实体、引入 TOTP 密钥,并借助请求 ID 与 Redis 缓存,可在 Spring Security + OAuth2 体系下实现无状态的两步验证流程。
文中给出的阿里云短信封装、TOTP 校验等代码可作为实际开发的起点,开发者可根据具体云平台接入对应的 SDK。