多因子认证组件 (MFA)
API文档
MFA 管理内置控制器 API 文档(Apifox) - 绑定、激活、解绑、可信设备管理、设为主管理设备、备份码验证等接口定义与示例
组件作用
项目开发中,多因子认证(MFA)是一个重要的安全增强功能,而不同的客户会有不同的安全要求和部署环境。通常情况下,我们需要自行实现TOTP/HOTP算法、密钥管理、设备信任、审计日志等功能,这些功能的实现复杂度高,且容易出错。
为了解决该问题,我们设计了MFA组件,该组件提供了统一的多因子认证接口,屏蔽了底层实现细节,使得业务代码可以不用关心密钥管理、验证算法、设备信任等底层实现,将所有差异屏蔽到配置文件中,代码中提供统一的管理和验证接口进行调用,通过调整配置文件的内容来达到对不同部署环境的支持,从而提高了代码的可读性和可维护性。
重要架构说明:MFA组件采用分离式架构设计,拆分为两个独立的模块:
-
rydeen-component-mfa-validation:验证模块,部署在
rydeen-gateway-service中- 职责:MFA验证逻辑,只读GlobalCache(rydeen-component-cache),零数据库依赖
- 特点:轻量级、高性能、毫秒级响应(<10ms)
-
rydeen-component-mfa-management:管理模块,部署在
rydeen-general-service中- 职责:MFA管理功能(绑定、解绑、状态管理等),操作数据库
- 特点:完整的CRUD操作,使用Liquibase管理DDL
目前我们支持的认证方式有:TOTP(基于时间的一次性密码)、HOTP(基于事件的一次性密码)、SMS验证码、Email验证码、备份码。基本已经覆盖了常见的企业级MFA需求。当需要更换认证方式或部署环境的时候,我们仅仅需要引入对应的组件包并调整配置文件即可,MFA组件包含的包清单如下:
核心包:
- rydeen-component-mfa-core
- rydeen-component-mfa-validation(网关验证模块)
- rydeen-component-mfa-management(管理服务模块)
组件用法
引入依赖
MFA组件配置方式如下:
首先在项目所属的Maven模块中引入组件坐标:
网关服务(验证模块)
在 rydeen-gateway-service 中引入验证模块:
<dependency>
<groupId>cn.com.rydeen.component</groupId>
<artifactId>rydeen-component-mfa-validation</artifactId>
</dependency>
通用服务(管理模块)
在 rydeen-general-service 中引入管理模块:
<dependency>
<groupId>cn.com.rydeen.component</groupId>
<artifactId>rydeen-component-mfa-management</artifactId>
</dependency>
<!-- 依赖组件 -->
<dependency>
<groupId>cn.com.rydeen.component</groupId>
<artifactId>rydeen-component-dao</artifactId>
</dependency>
<dependency>
<groupId>cn.com.rydeen.component</groupId>
<artifactId>rydeen-component-cache</artifactId>
</dependency>
注意:两个模块需要同时引入,验证模块部署在网关服务,管理模块部署在通用服务。
配置文件
MFA组件的配置统一在Nacos配置中心管理,网关服务和通用服务共享同一份配置。配置包含两部分:验证模块配置(网关服务使用)和管理模块配置(通用服务使用)。
想要理解MFA组件的配置文件,首先需要了解MFA组件的几个基本概念:
- 验证模块(validation):部署在网关服务,负责MFA验证逻辑,只读GlobalCache,零数据库依赖
- 管理模块(management):部署在通用服务,负责MFA管理功能(绑定、解绑、状态管理等),操作数据库
- 分离式架构:验证与管理完全分离,降低耦合度,符合网关轻量化原则
- 缓存优先:热点数据优先使用缓存,减少数据库访问,提升性能
- 统一配置:配置统一在Nacos配置中心管理,两个服务共享同一份配置
配置示例
MFA组件的配置统一在Nacos配置中心管理,网关服务和通用服务共享同一份配置:
platform:
component:
mfa:
# 启用MFA(验证模块和管理模块共用)
enabled: true
# 管理模块配置(通用服务使用)
management:
issuer: "Rydeen Inc." # 发行方名称(显示在Authenticator应用中)
controller-enabled: true # 启用Controller(默认)
api-prefix: "/api/mfa" # API路径前缀
ttl-hours: 24 # 缓存TTL(小时)
# TOTP配置(管理模块和验证模块共用)
totp:
algorithm: "SHA1" # 算法(SHA1/SHA256/SHA512),默认SHA1
digits: 6 # 验证码位数
period: 30 # 时间窗口周期(秒)
window-size: 1 # 窗口大小(容错窗口,±N表示允许前后N个时间窗口)
code-length: 6 # 验证码长度(与digits相同)
time-window: 30 # 时间窗口(秒,与period相同)
# 安全策略配置(管理模块和验证模块共用)
security:
# 密钥管理配置
key-management:
provider: vault # vault | kms | hsm | local
# vault: # Vault配置(当provider=vault时使用)
# endpoint: "http://vault:8200"
# token: "${VAULT_TOKEN}"
# transit-engine-path: "transit"
# kms: # 云KMS配置(当provider=kms时使用)
# provider: "aws" # aws | aliyun | tencent
# endpoint: "https://kms.example.com"
# 备份码配置
backup-code:
count: 10 # 备份码数量
length: 8 # 备份码长度
hash-algorithm: BCrypt # 哈希算法
# 可信设备配置
trusted-device:
enabled: true # 启用可信设备
max-devices: 10 # 最大设备数
default-trust-days: 30 # 默认信任天数
# 防重放攻击配置
replay-prevention-ttl-multiplier: 3 # TTL倍数(TTL = 时间窗口 * 倍数)
# 账户锁定配置
max-attempts: 5 # 最大失败次数
lock-duration-seconds: 300 # 锁定时长(秒)
gateway:
tenant:
enable: false # 如果MFA未显式配置enable-tenant,会自动读取此配置
注意:
- 配置统一在Nacos配置中心管理,网关服务和通用服务共享同一份配置
- 两个模块的
enable-tenant配置必须保持一致 - 如果未显式配置
enable-tenant,MFA组件会自动读取platform.gateway.tenant.enable配置 - 缓存配置使用平台统一的
rydeen-component-cache配置,无需在MFA组件中单独配置
管理服务API(rydeen-general-service)
部署位置:rydeen-general-service
职责:处理所有MFA管理操作,包括绑定、解绑、状态查询、可信设备管理等
完整API文档:详细接口定义、请求/响应示例及数据模型可参考:MFA 管理接口 - platform
绑定MFA设备
POST /api/mfa/bind
Content-Type: application/json
{
"userId": "user123",
"deviceType": "TOTP",
"tenantId": "tenant001"
}
Response 200:
{
"code": 0,
"msg": "success",
"data": {
"qrCodeUrl": "otpauth://totp/Rydeen:user123?secret=JBSWY3DPEHPK3PXP&issuer=Rydeen",
"secretKey": "JBSWY3DPEHPK3PXP",
"backupCodes": ["12345678", "87654321", ...],
"expiresIn": 600
}
}
处理流程:
- 生成密钥(使用SecretKeyManager,存储到KMS/Redis)
- 生成备份码(使用BackupCodeManager,BCrypt哈希)
- 生成二维码(使用QrCodeManager)
- 保存到数据库(事务,状态为NOT_ACTIVATED)
- 同步到GlobalCache(分布式锁)
- 发布审计事件(BIND操作)
- 返回结果
激活MFA设备
POST /api/mfa/activate
Content-Type: application/json
{
"userId": "user123",
"code": "123456",
"deviceId": "device-fingerprint",
"deviceName": "iPhone 14 Pro",
"deviceFingerprint": "sha256:...",
"trustDevice": true,
"tenantId": "tenant001"
}
Response 200:
{
"code": 0,
"msg": "success"
}
处理流程:
- 从数据库查询用户MFA信息
- 从KMS检索密钥(使用SecretKeyManager)
- 验证TOTP验证码(使用TotpValidationEngine)
- 更新状态为已启用(ENABLED)
- 同步到GlobalCache
- 如果用户选择信任设备,注册为可信设备(使用TrustedDeviceManager)
- 发布审计事件(ACTIVATE操作)
解绑MFA设备
POST /api/mfa/unbind
Content-Type: application/json
{
"userId": "user123",
"tenantId": "tenant001"
}
Response 200:
{
"code": 0,
"msg": "success"
}
处理流程:
- 从数据库查询用户MFA信息
- 逻辑删除(使用MyBatis-Plus的deleteById)
- 从密钥管理器删除密钥(使用SecretKeyManager)
- 从GlobalCache删除
- 发布审计事件(UNBIND操作)
查询MFA状态
GET /api/mfa/status?userId=user123&tenantId=tenant001
Response 200:
{
"code": 0,
"msg": "success",
"data": {
"userId": "user123",
"status": "ENABLED",
"deviceType": "TOTP",
"bindTime": "2024-01-15T10:30:00Z",
"lastUsedTime": "2024-01-15T14:20:00Z",
"trustedDevices": 2,
"backupCodesRemaining": 8
}
}
可信设备管理
查询可信设备列表
GET /api/mfa/trusted-devices?userId=user123&tenantId=tenant001¤tDeviceId=device-fingerprint
Response 200:
{
"code": 0,
"msg": "success",
"data": {
"devices": [
{
"deviceId": "abc123def456...",
"deviceName": "iPhone 14 Pro",
"trustedUntil": "2024-02-15T10:30:00Z",
"lastUsedTime": "2024-01-20T14:20:00Z",
"createdAt": "2024-01-15T10:30:00Z",
"isPrimary": true
}
],
"total": 1,
"maxDevices": 10,
"currentDeviceIsPrimary": true
}
}
撤销可信设备
DELETE /api/mfa/trusted-devices/{deviceId}?userId=user123&tenantId=tenant001¤tDeviceId=device-fingerprint
Response 200:
{
"code": 0,
"msg": "success"
}
撤销所有可信设备
DELETE /api/mfa/trusted-devices/all?userId=user123&tenantId=tenant001¤tDeviceId=device-fingerprint
Response 200:
{
"code": 0,
"msg": "success",
"data": 2 # 撤销的设备数量
}
设为主管理设备
PUT /api/mfa/trusted-devices/{deviceId}/primary
Content-Type: application/json
{
"userId": "user123",
"currentDeviceId": "device-fingerprint",
"tenantId": "tenant001"
}
Response 200:
{
"code": 0,
"msg": "success"
}
功能说明:
- 仅当前主设备可将另一台设备设为主设备
- 主设备可移除其他可信设备,非主设备仅可查看
- 主设备用于管理其他设备的信任状态
验证并消耗备份码
POST /api/mfa/backup-codes/verify
Content-Type: application/json
{
"userId": "user123",
"code": "12345678",
"tenantId": "tenant001"
}
Response 200:
{
"code": 0,
"msg": "success"
}
功能说明:
- 用户输入8位备份码时调用
- 验证通过则从列表中移除该码(一次性使用)
- 备份码验证失败时发布审计事件(VERIFY操作,authMethod=BACKUP_CODE)
网关验证API(rydeen-gateway-service)
部署位置:rydeen-gateway-service
职责:MFA验证逻辑,只读GlobalCache,零数据库依赖
特点:
- 网关验证层不提供RESTful API
- 只通过过滤器拦截登录请求
- 只读GlobalCache(rydeen-component-cache),不操作数据库
验证流程
网关验证层通过 IssueTokensFilter 过滤器拦截登录请求,流程如下:
- 业务服务登录:业务服务(如
rydeen-general-service)处理登录请求 - 检查MFA状态:业务服务调用
MfaBindManager.checkLoginMfa()检查是否需要MFA- 如果设备已信任且未过期,返回
accessToken,跳过MFA验证 - 如果设备未信任或已绑定MFA,返回
mfaRequired=true,不返回accessToken
- 如果设备已信任且未过期,返回
- 网关过滤器拦截:
IssueTokensFilter检查业务响应中是否包含accessToken- 如果包含
accessToken:直接放行,不进行MFA验证 - 如果不包含
accessToken:调用MfaValidationService.checkMfaStatus()检查MFA状态
- 如果包含
- MFA状态检查:网关层从GlobalCache读取MFA元数据,返回前端展示用的信息
- MFA验证:用户输入验证码后,网关层调用
MfaValidationService.verifyMfaCode()验证- 验证成功:发布审计事件(VERIFY操作,result=SUCCESS)
- 验证失败:发布审计事件(VERIFY操作,result=FAILED)
- 账户锁定:发布审计事件(VERIFY操作,result=BLOCKED)
关键方法:
MfaBindManager.checkLoginMfa():业务服务层方法,检查是否需要MFA(考虑可信设备)MfaValidationService.checkMfaStatus():网关层方法,只读缓存返回MFA元数据MfaValidationService.verifyMfaCode():网关层方法,验证MFA码(TOTP/备份码)
注意:
- 网关验证层不进行设备信任检查,设备信任检查由业务登录层(
MfaBindManager.checkLoginMfa())完成 - 网关验证层不降级到数据库,缓存不可用时快速失败,保持网关轻量化
审计事件监听
MFA组件通过 Spring ApplicationEventPublisher 发布审计事件,业务系统通过 @EventListener 监听并自行决定如何处理(持久化、签名、归档等)。
事件模型
MFA组件定义 MfaAuditEvent 事件类,继承 ApplicationEvent:
位置:cn.com.rydeen.component.mfa.core.event.MfaAuditEvent
字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
tenantId | String | 否 | 租户ID(如果未启用租户则为 null) |
userId | String | 是 | 用户ID(业务系统User表的主键ID) |
operationType | MfaOperationTypeEnum | 是 | 操作类型枚举(BIND/UNBIND/VERIFY/ACTIVATE/DISABLE) |
authMethod | String | 是 | 认证方式(TOTP/HOTP/SMS/EMAIL/BACKUP_CODE) |
ipAddress | String | 否 | IP地址(自动从HTTP请求提取) |
userAgent | String | 否 | 用户代理(自动从HTTP请求提取) |
deviceId | String | 否 | 设备ID(设备指纹) |
result | String | 是 | 操作结果(SUCCESS/FAILED/BLOCKED) |
errorCode | String | 否 | 错误码(失败时) |
errorMessage | String | 否 | 错误消息(失败时) |
durationMs | Long | 否 | 操作耗时(毫秒) |
timestamp | LocalDateTime | 是 | 事件时间戳(自动生成) |
操作类型枚举(MfaOperationTypeEnum):
| 枚举值 | 编码 | 描述 | 使用场景 |
|---|---|---|---|
BIND | "BIND" | 绑定设备 | 用户绑定 MFA 设备时 |
UNBIND | "UNBIND" | 解绑设备 | 用户解绑 MFA 设备时 |
VERIFY | "VERIFY" | 验证码验证 | 用户验证 TOTP 码或备份码时 |
ACTIVATE | "ACTIVATE" | 激活设备 | 用户激活 MFA 设备时 |
DISABLE | "DISABLE" | 禁用设备 | 管理员禁用用户 MFA 时(暂未实现) |
事件发布时机
MFA组件在以下关键操作点自动发布审计事件:
1. 绑定设备(BIND)
- 位置:
MfaBindManager.bindDevice() - 时机:绑定成功后
- 事件内容:
operationType=BIND,result=SUCCESS
2. 激活设备(ACTIVATE)
- 位置:
MfaBindManager.activateDevice() - 时机:
- 激活成功:
result=SUCCESS - 激活失败(验证码错误):
result=FAILED,errorCode=MFA_CODE_INVALID - 激活失败(用户不存在):
result=FAILED,errorCode=MFA_NOT_BOUND
- 激活成功:
3. 解绑设备(UNBIND)
- 位置:
MfaBindManager.unbindDevice() - 时机:
- 解绑成功:
result=SUCCESS - 解绑失败(用户不存在):
result=FAILED,errorCode=MFA_NOT_BOUND
- 解绑成功:
4. 验证 MFA 码(VERIFY)
- 位置:
MfaBindManager.verifyMfaCode()(备份码验证)MfaValidationServiceImpl.verifyMfaCode()(TOTP 验证)
- 时机:
- 验证成功:
result=SUCCESS,authMethod=TOTP或BACKUP_CODE - 验证失败(验证码错误):
result=FAILED,errorCode=MFA_CODE_INVALID - 验证失败(用户未绑定):
result=FAILED,errorCode=MFA_NOT_BOUND - 验证失败(MFA 未启用):
result=FAILED,errorCode=MFA_NOT_ENABLED - 账户锁定:
result=BLOCKED,errorCode=ACCOUNT_LOCKED - 重放攻击:
result=FAILED,errorCode=MFA_CODE_USED - 验证异常:
result=FAILED,errorCode=MFA_VERIFY_ERROR
- 验证成功:
事件发布实现
MFA 组件内部使用 MfaAuditEventPublisher 工具类封装事件发布逻辑:
工具类位置:cn.com.rydeen.component.mfa.management.util.MfaAuditEventPublisher
核心功能:
- 自动提取 IP 地址和 User-Agent(从 HTTP 请求上下文)
- 提供简化方法:
publishSuccess()、publishFailure()、publishBlocked() - 异常处理:事件发布失败不影响主流程
业务系统监听与处理
业务系统通过 @EventListener 监听 MFA 审计事件,自行决定处理方式:
基础监听器示例:
package com.example.business.mfa.listener;
import cn.com.rydeen.component.mfa.core.event.MfaAuditEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* MFA 审计事件监听器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MfaAuditLogListener {
@EventListener
@Async("mfaAuditExecutor") // 使用自定义线程池(可选)
public void handleMfaAuditEvent(MfaAuditEvent event) {
try {
log.info("收到MFA审计事件: operationType={}, userId={}, result={}",
event.getOperationType() != null ? event.getOperationType().getCode() : null,
event.getUserId(),
event.getResult());
// 业务系统自行决定如何处理(见下方选项)
processAuditEvent(event);
} catch (Exception e) {
log.error("处理MFA审计事件失败", e);
}
}
private void processAuditEvent(MfaAuditEvent event) {
// 业务系统自行实现处理逻辑
// 选项1:持久化到数据库
// 选项2:数字签名后持久化
// 选项3:发送到消息队列
// 选项4:发送到归档服务
// 选项5:告警通知
// 选项6:统计分析
// 选项7:组合处理
}
}
配置异步线程池(可选):
方式1:虚拟线程(推荐,Java 21+)
虚拟线程适合IO密集型任务(如数据库写入、日志记录),可以创建数百万个线程,在IO阻塞时自动释放平台线程,提高资源利用率:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("mfaAuditExecutor")
public Executor mfaAuditExecutor() {
// 使用虚拟线程执行器(Java 21+)
// 优势:轻量级、高并发、自动调度、适合IO密集型任务
return Executors.newVirtualThreadPerTaskExecutor();
}
}
方式2:传统线程池(Java 21以下版本)
如果使用 Java 21 以下版本,可以使用传统线程池:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("mfaAuditExecutor")
public Executor mfaAuditExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("mfa-audit-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
虚拟线程优势:
- ✅ 轻量级:可以创建数百万个虚拟线程,不受平台线程数量限制
- ✅ I/O 优化:在 I/O 阻塞时自动释放平台线程,提高资源利用率
- ✅ 简化管理:无需手动管理线程池大小,由 JVM 自动调度
- ✅ 适合审计场景:审计事件处理通常是IO密集型操作(数据库写入、日志记录等),非常适合使用虚拟线程
处理选项:
业务系统可根据需求选择以下处理方式:
- ✅ 持久化到数据库:业务系统可自行创建
mfa_operation_log表并持久化(MFA 组件不提供表结构) - ✅ 数字签名:使用 RSA 私钥签名,确保日志完整性
- ✅ 归档处理:定期归档到对象存储(S3/OSS)
- ✅ 告警通知:异常事件发送告警
- ✅ 统计分析:聚合分析、报表生成
- ✅ 不处理:如果业务系统不需要审计日志,可以不监听事件
数据库表结构建议:
CREATE TABLE mfa_operation_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
tenant_id VARCHAR(64),
user_id VARCHAR(64) NOT NULL,
operation_type VARCHAR(20) NOT NULL COMMENT '操作类型:BIND/UNBIND/VERIFY/ACTIVATE/DISABLE',
auth_method VARCHAR(20) COMMENT '认证方式:TOTP/HOTP/SMS/EMAIL/BACKUP_CODE',
ip_address VARCHAR(45) COMMENT 'IP地址',
user_agent VARCHAR(500) COMMENT '用户代理',
device_id VARCHAR(128) COMMENT '设备ID',
result VARCHAR(20) NOT NULL COMMENT '操作结果:SUCCESS/FAILED/BLOCKED',
error_code VARCHAR(50) COMMENT '错误码',
error_message VARCHAR(500) COMMENT '错误消息',
duration_ms BIGINT COMMENT '操作耗时(毫秒)',
signature VARCHAR(512) COMMENT '数字签名(可选)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_id (user_id),
INDEX idx_tenant_user (tenant_id, user_id),
INDEX idx_operation_type (operation_type),
INDEX idx_result (result),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='MFA操作审计日志表';
完整集成示例:
步骤1:创建实体类
package com.example.business.mfa.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("mfa_operation_log")
public class MfaOperationLog {
@TableId(type = IdType.AUTO)
private Long id;
private String tenantId;
private String userId;
private String operationType;
private String authMethod;
private String ipAddress;
private String userAgent;
private String deviceId;
private String result;
private String errorCode;
private String errorMessage;
private Long durationMs;
private String signature;
private LocalDateTime createdAt;
}
步骤2:创建 Mapper
package com.example.business.mfa.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.business.mfa.entity.MfaOperationLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MfaOperationLogMapper extends BaseMapper<MfaOperationLog> {
}
步骤3:创建监听器
package com.example.business.mfa.listener;
import cn.com.rydeen.component.mfa.core.event.MfaAuditEvent;
import com.example.business.mfa.entity.MfaOperationLog;
import com.example.business.mfa.mapper.MfaOperationLogMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@RequiredArgsConstructor
public class MfaAuditLogListener {
private final MfaOperationLogMapper logMapper;
@EventListener
@Async("mfaAuditExecutor")
public void handleMfaAuditEvent(MfaAuditEvent event) {
try {
// 转换为实体对象
MfaOperationLog entity = convertToEntity(event);
// 持久化到数据库
logMapper.insert(entity);
log.debug("MFA审计日志已保存: operationType={}, userId={}, result={}",
event.getOperationType() != null ? event.getOperationType().getCode() : null,
event.getUserId(),
event.getResult());
} catch (Exception e) {
log.error("保存MFA审计日志失败: operationType={}, userId={}",
event.getOperationType() != null ? event.getOperationType().getCode() : null,
event.getUserId(), e);
}
}
private MfaOperationLog convertToEntity(MfaAuditEvent event) {
MfaOperationLog entity = new MfaOperationLog();
entity.setTenantId(event.getTenantId());
entity.setUserId(event.getUserId());
entity.setOperationType(event.getOperationType() != null
? event.getOperationType().getCode() : null);
entity.setAuthMethod(event.getAuthMethod());
entity.setIpAddress(event.getIpAddress());
entity.setUserAgent(event.getUserAgent());
entity.setDeviceId(event.getDeviceId());
entity.setResult(event.getResult());
entity.setErrorCode(event.getErrorCode());
entity.setErrorMessage(event.getErrorMessage());
entity.setDurationMs(event.getDurationMs());
entity.setCreatedAt(event.getTimestamp());
return entity;
}
}
步骤4:配置异步线程池(已在上面配置,此处略)
步骤5:创建数据库表(已在上面提供,此处略)
更多处理选项示例:
选项1:持久化到数据库(见上方完整示例)
选项2:数字签名后持久化
@Component
@RequiredArgsConstructor
public class MfaAuditLogListener {
private final MfaOperationLogMapper logMapper;
private final SignatureService signatureService; // 业务系统自行实现
@EventListener
@Async
public void handleMfaAuditEvent(MfaAuditEvent event) {
MfaOperationLog entity = convertToEntity(event);
// 生成数字签名(使用 RSA 私钥)
String signature = signatureService.sign(event);
entity.setSignature(signature);
// 持久化
logMapper.insert(entity);
}
}
选项3:发送到消息队列
@Component
@RequiredArgsConstructor
public class MfaAuditLogListener {
private final MessageService messageService; // 业务系统自行实现
@EventListener
@Async
public void handleMfaAuditEvent(MfaAuditEvent event) {
// 发送到消息队列(异步处理、归档等)
messageService.send("mfa-audit-topic", event);
}
}
选项4:发送到归档服务
@Component
@RequiredArgsConstructor
public class MfaAuditLogListener {
private final ArchiveService archiveService; // 业务系统自行实现
@EventListener
@Async
public void handleMfaAuditEvent(MfaAuditEvent event) {
// 归档到对象存储(S3/OSS)
archiveService.archive("mfa-audit", event);
}
}
选项5:告警通知
@Component
@RequiredArgsConstructor
public class MfaAuditLogListener {
private final AlertService alertService; // 业务系统自行实现
@EventListener
@Async
public void handleMfaAuditEvent(MfaAuditEvent event) {
// 高风险事件发送告警
if ("BLOCKED".equals(event.getResult())
|| "MFA_CODE_USED".equals(event.getErrorCode())) {
alertService.sendAlert("MFA高风险事件", event);
}
}
}
选项6:统计分析
@Component
@RequiredArgsConstructor
public class MfaAuditLogListener {
private final StatisticsService statisticsService; // 业务系统自行实现
@EventListener
@Async
public void handleMfaAuditEvent(MfaAuditEvent event) {
// 实时统计(成功率、失败率等)
statisticsService.recordMfaEvent(event);
}
}
选项7:组合处理
@Component
@RequiredArgsConstructor
public class MfaAuditLogListener {
private final MfaOperationLogMapper logMapper;
private final SignatureService signatureService;
private final AlertService alertService;
private final StatisticsService statisticsService;
@EventListener
@Async
public void handleMfaAuditEvent(MfaAuditEvent event) {
// 1. 持久化到数据库
MfaOperationLog entity = convertToEntity(event);
String signature = signatureService.sign(event);
entity.setSignature(signature);
logMapper.insert(entity);
// 2. 高风险事件告警
if ("BLOCKED".equals(event.getResult())
|| "MFA_CODE_USED".equals(event.getErrorCode())) {
alertService.sendAlert("MFA高风险事件", event);
}
// 3. 实时统计
statisticsService.recordMfaEvent(event);
}
}
最佳实践:
1. 异步处理
- ✅ 使用
@Async异步处理事件,避免阻塞主流程 - ✅ 配置专用线程池,避免影响其他异步任务
2. 异常处理
- ✅ 事件处理失败不应影响主流程,只记录错误日志
- ✅ 使用 try-catch 包裹处理逻辑
3. 性能优化
- ✅ 批量处理:对于高并发场景,可批量收集事件后批量处理
- ✅ 异步队列:使用消息队列缓冲,避免事件丢失
4. 数据完整性
- ✅ 数字签名:对重要审计日志进行数字签名,防止篡改
- ✅ 定期归档:定期归档历史日志到对象存储
5. 监控告警
- ✅ 监控事件处理延迟和失败率
- ✅ 高风险事件(账户锁定、重放攻击)及时告警
优势:
- ✅ 组件轻量化:MFA 组件不依赖数据库,只发布事件,保持组件独立性
- ✅ 业务灵活性:业务系统可自行决定是否启用审计、如何处理
- ✅ 无中间件依赖:使用 Spring 内置事件机制,无需 Redis Stream、MQ 等外部中间件
- ✅ 解耦设计:MFA 组件与审计实现完全解耦,业务系统可自由选择处理方式
- ✅ 非阻塞发布:事件发布采用异步模式,不阻塞主业务流程
设计原则
独立性与解耦
MFA组件完全独立于业务系统的User表结构:
- ✅ 只关联User表主键ID:MFA组件只需要业务系统User表的主键ID(
userId),不维护任何用户信息 - ✅ 不依赖User表结构:不同业务系统的User表结构可以完全不同,MFA组件都能适配
- ✅ 零侵入集成:业务系统无需为MFA修改已有的User模块代码
- ✅ 灵活适配:支持String或Long类型的User表主键(统一转换为String存储)
租户功能可选
MFA组件支持可配置的租户功能,并自动读取网关配置:
- ✅ 租户可选:通过
enableTenant配置项控制是否启用租户功能(默认false) - ✅ 自动读取网关配置:如果未显式配置,自动读取
platform.gateway.tenant.enable(推荐方式) - ✅ 无租户系统:如果系统没有租户概念,
tenantId可以为null,数据库tenant_id字段为NULL - ✅ 有租户系统:如果系统有租户概念,启用租户后
tenantId必须提供 - ⚠️ 配置一致性:两个模块(validation和management)的
enableTenant配置必须保持一致
网关轻量化原则
- 零数据库依赖:网关验证层严格禁止操作数据库
- 只读缓存:网关验证层只从GlobalCache(rydeen-component-cache)读取数据,不写入
- 快速失败:缓存不可用时快速失败,不阻塞请求,不降级到数据库
核心特性
🔐 多重认证方式
- TOTP认证:基于时间的一次性密码(Time-based One-Time Password),符合RFC 6238标准
- HOTP认证:基于事件的一次性密码(HMAC-based One-Time Password),符合RFC 4226标准
- 备份码:8位数字备份码,BCrypt哈希存储,一次性使用
🛡️ 安全保障
- 密钥加密存储:使用KMS/HSM管理主密钥,用户密钥AES-256-GCM加密,存储在KMS/Redis中,不存储在数据库
- 防重放攻击:记录已使用验证码,TTL控制,防止重复使用
- 审计日志完整性:通过Spring ApplicationEvent发布审计事件,业务系统可自行决定是否签名、归档
- 防暴力破解:智能失败次数限制,达到阈值后账户锁定
⚡ 高性能设计
- GlobalCache缓存:热点数据高速访问(rydeen-component-cache),网关层毫秒级响应(<10ms)
- 架构分离:验证与管理完全分离,网关零数据库依赖
- 异步处理:审计事件发布采用异步模式,不阻塞主流程
🎯 易用性强
- Spring Boot Starter:一键集成,自动配置
- RESTful API:标准化接口设计(管理模块)
- 自动配置:开箱即用,Liquibase自动创建数据库表
- 详细文档:完善的使用指南和API文档
相关标准
- RFC 6238 - TOTP: Time-Based One-Time Password Algorithm
- RFC 4226 - HOTP: An HMAC-Based One-Time Password Algorithm
- NIST 800-63B - Digital Identity Guidelines
- OWASP MFA Guide - Multi-Factor Authentication Guide