纲要
多因子认证(MFA)核心概念
usingMFA:布尔字段,标识用户是否启用两步验证MFAKey:每个用户独立的 TOTP 密钥X-MFA-Delicate:自定义响应头,携带MFA标识及requestId- 用户缓存:第一步认证成功后暂存用户信息,生成
requestId,用于第二步验证关联
核心流程
- 用户名密码登录(第一步)
- 查询用户并校验密码
- 检查
usingMFA字段false:直接签发JWT,登录完成true:缓存用户信息,生成requestId,返回401及自定义头X-MFA-Delicate: MFA, requestId=xxx
- 前端接收后跳转至验证码输入页面,提交
requestId与 TOTP 验证码 - 后端从缓存获取用户,取出
MFAKey验证 TOTP - 验证通过后签发
JWT,登录完成
涉及的实体与代码改造
User实体新增字段UserService增加密码匹配与认证逻辑LoginController改造登录接口,区分 MFA 与非 MFA 响应UserCacheService设计缓存接口(后续实现)
多因子认证流程概览
多因子认证(MFA)是在传统用户名密码基础上增加一层验证的安全机制。在已具备 TOTP 生成、验证以及邮件/短信发送能力后,需要将其组装成完整的两步认证流程。整体流程如下图所示:
实体类改造
为了支持按用户启用 MFA,User实体需要增加两个字段:usingMfa(是否启用两步验证)和mfaKey(TOTP 密钥)。同时注意以下几点:
- 使用
@Column精确映射数据库字段,usingMfa默认值为false mfaKey使用@JsonIgnore避免在 JSON 序列化时泄露- 使用 Lombok
@Builder时需为usingMfa设置默认值
示例
packagecom.example.demo.entity;importcom.fasterxml.jackson.annotation.JsonIgnore;importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;importjavax.persistence.*;@Data@Builder@NoArgsConstructor@AllArgsConstructor@Entity@Table(name="users")publicclassUser{@Id@GeneratedValue(strategy=GenerationType.IDENTITY)privateLongid;@Column(nullable=false,unique=true)privateStringusername;@Column(nullable=false)privateStringpassword;@Column(nullable=false)privatebooleanenabled;@Column(name="using_mfa",nullable=false)@Builder.DefaultprivatebooleanusingMfa=false;@Column(name="mfa_key")@JsonIgnoreprivateStringmfaKey;}数据库对应的表结构(示例 SQL):
CREATETABLEusers(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.userRepository=userRepository;this.passwordEncoder=passwordEncoder;this.totp=totp;}publicUserregister(Stringusername,StringrawPassword){StringmfaKey=totp.generateKey();// 生成密钥并转为String存储Useruser=User.builder().username(username).password(passwordEncoder.encode(rawPassword)).enabled(true).usingMfa(false)// 默认不启用,可由用户后续自行开启.mfaKey(mfaKey).build();returnuserRepository.save(user);}}登录逻辑改造
原有登录接口直接返回 JWT,引入 MFA 后需根据用户usingMfa字段决定响应方式。以下是改造后的控制器核心逻辑。
首先,为UserService添加一个按用户名和原始密码进行认证的方法,返回Optional<User>:
@ServicepublicclassUserService{// 其他注入...publicOptional<User>authenticate(Stringusername,StringrawPassword){returnuserRepository.findByUsername(username).filter(user->passwordEncoder.matches(rawPassword,user.getPassword())).filter(user->user.isEnabled());// 可加入更多账户状态检查}}UserRepository需提供findByUsername方法:
publicinterfaceUserRepositoryextendsJpaRepository<User,Long>{Optional<User>findByUsername(Stringusername);}然后,在登录控制器中,根据usingMfa分流处理:
@RestControllerpublicclassAuthController{privatefinalUserServiceuserService;privatefinalUserCacheServiceuserCacheService;publicAuthController(UserServiceuserService,UserCacheServiceuserCacheService){this.userService=userService;this.userCacheService=userCacheService;}@PostMapping("/login")publicResponseEntity<?>login(@RequestBodyLoginRequestrequest){Optional<User>userOpt=userService.authenticate(request.getUsername(),request.getPassword());if(userOpt.isEmpty()){returnResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码错误");}Useruser=userOpt.get();if(!user.isUsingMfa()){// 未启用MFA,直接返回JWTStringtoken=generateToken(user);// 自行实现JWT生成returnResponseEntity.ok(newAuthResponse(token));}// 启用MFA:缓存用户信息并返回特殊响应StringrequestId=userCacheService.cacheUser(user);HttpHeadersheaders=newHttpHeaders();headers.add("X-MFA-Delicate","MFA, requestId="+requestId);returnResponseEntity.status(HttpStatus.UNAUTHORIZED).headers(headers).body("需要两步验证");}privateStringgenerateToken(Useruser){// 实际使用JWT工具生成,此处省略return"jwt-token-for-"+user.getUsername();}}请求体与响应体可定义为简单的 DTO:
// LoginRequest.java@DatapublicclassLoginRequest{privateStringusername;privateStringpassword;}// AuthResponse.java@Data@AllArgsConstructorpublicclassAuthResponse{privateStringtoken;}第二步验证接口
前端收到401和X-MFA-Delicate头后,引导用户输入 TOTP 验证码,然后调用第二步验证接口。该接口从缓存中取出用户,使用其mfaKey进行 TOTP 校验,成功后返回 JWT。
@PostMapping("/mfa/verify")publicResponseEntity<?>verifyMfa(@RequestBodyMfaVerifyRequestrequest){Optional<User>userOpt=userCacheService.getUser(request.getRequestId());if(userOpt.isEmpty()){returnResponseEntity.status(HttpStatus.UNAUTHORIZED).body("会话已过期,请重新登录");}Useruser=userOpt.get();// 假设TOTP验证工具类已注入booleanisValid=totp.verify(user.getMfaKey(),request.getCode());if(!isValid){returnResponseEntity.status(HttpStatus.UNAUTHORIZED).body("验证码错误");}// 验证成功,清除缓存,返回JWTuserCacheService.removeUser(request.getRequestId());Stringtoken=generateToken(user);returnResponseEntity.ok(newAuthResponse(token));}// MfaVerifyRequest.java@DatapublicclassMfaVerifyRequest{privateStringrequestId;privateStringcode;}用户缓存服务设计
UserCacheService负责暂存第一步认证成功的用户信息,并生成唯一requestId。这里使用内存ConcurrentHashMap实现一个简单版本,实际生产环境应使用 Redis 并设置过期时间。
@ServicepublicclassUserCacheService{privatefinalMap<String,User>cache=newConcurrentHashMap<>();publicStringcacheUser(Useruser){StringrequestId=UUID.randomUUID().toString();cache.put(requestId,user);returnrequestId;}publicOptional<User>getUser(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虽非标准头,但可被前端识别并用于流程控制,避免将认证状态放在普通响应体中而引发歧义 - 灵活启用 MFA:
usingMfa字段赋予业务极大的灵活性,可按角色、风险等级动态调整,例如异地登录时强制要求两步验证
以上内容完整展示了基于 Spring Security 与 OAuth2 生态下,多因子认证逻辑的实体改造、认证分流及缓存配合方案。
后续可进一步集成 Spring Security 的过滤器链,将 MFA 校验作为额外认证提供者,实现更平滑的鉴权集成。