news 2026/9/13 6:29:20

Java框架快速入门: Spring Security+OAuth2之多因子认证逻辑与实体类改造

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java框架快速入门: Spring Security+OAuth2之多因子认证逻辑与实体类改造

纲要

  • 多因子认证(MFA)核心概念

    • usingMFA:布尔字段,标识用户是否启用两步验证
    • MFAKey:每个用户独立的 TOTP 密钥
    • X-MFA-Delicate:自定义响应头,携带MFA标识及requestId
    • 用户缓存:第一步认证成功后暂存用户信息,生成requestId,用于第二步验证关联
  • 核心流程

    1. 用户名密码登录(第一步)
    2. 查询用户并校验密码
    3. 检查usingMFA字段
      • false:直接签发JWT,登录完成
      • true:缓存用户信息,生成requestId,返回401及自定义头X-MFA-Delicate: MFA, requestId=xxx
    4. 前端接收后跳转至验证码输入页面,提交requestId与 TOTP 验证码
    5. 后端从缓存获取用户,取出MFAKey验证 TOTP
    6. 验证通过后签发JWT,登录完成
  • 涉及的实体与代码改造

    • User实体新增字段
    • UserService增加密码匹配与认证逻辑
    • LoginController改造登录接口,区分 MFA 与非 MFA 响应
    • UserCacheService设计缓存接口(后续实现)

多因子认证流程概览

多因子认证(MFA)是在传统用户名密码基础上增加一层验证的安全机制。在已具备 TOTP 生成、验证以及邮件/短信发送能力后,需要将其组装成完整的两步认证流程。整体流程如下图所示:

服务端客户端服务端客户端alt[验证成功][验证失败]alt[usingMFA = false][usingMFA = true]POST /login (username, password)校验用户名密码200 OK (JWT Token)缓存用户信息,生成 requestId401 UnauthorizedX-MFA-Delicate: MFA, requestId=xxx跳转验证码页面POST /mfa/verify (requestId, code)从缓存获取用户,取出 MFAKey验证 TOTP200 OK (JWT Token)401 验证码错误

实体类改造

为了支持按用户启用 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;}

第二步验证接口

前端收到401X-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虽非标准头,但可被前端识别并用于流程控制,避免将认证状态放在普通响应体中而引发歧义
  • 灵活启用 MFAusingMfa字段赋予业务极大的灵活性,可按角色、风险等级动态调整,例如异地登录时强制要求两步验证

以上内容完整展示了基于 Spring Security 与 OAuth2 生态下,多因子认证逻辑的实体改造、认证分流及缓存配合方案。

后续可进一步集成 Spring Security 的过滤器链,将 MFA 校验作为额外认证提供者,实现更平滑的鉴权集成。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 6:28:14

Basilisk模拟实战:C语言自适应网格流体仿真与Shell脚本自动化

简介&#xff1a;压缩包服务于博士阶段基于 Basilisk 的数值模拟研究&#xff0c;聚焦 C 语言开发与 Shell 自动化流程&#xff0c;适合正在攻读数理、流体或地球物理方向、需要快速上手开源模拟框架的研究生。包内共 11 个文件&#xff0c;包含 C 源文件与头文件、Shell 脚本、…

作者头像 李华
网站建设 2026/9/13 6:28:07

AI驱动游戏出海增长:从买量瓶颈到专属语言引擎的实战策略

做游戏出海这几年&#xff0c;大家心里都清楚&#xff0c;最头疼的从来不是产品研发本身&#xff0c;而是“好不容易做出来的游戏&#xff0c;怎么让海外玩家愿意下载、愿意留下”。买量成本一年比一年高&#xff0c;本地化又经常是“译了但没完全译”&#xff0c;玩家一眼看出…

作者头像 李华
网站建设 2026/9/13 6:25:54

企业AI平台接入能力横评:业务系统72小时打通实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 6:22:39

CarSim与Simulink联合仿真实现停车场低速导航跟踪

1. 项目背景与核心需求 停车场低速导航跟踪是智能驾驶领域的关键技术难点之一。与高速公路场景不同&#xff0c;停车场环境具有以下典型特征&#xff1a; 空间结构复杂&#xff08;直角弯、窄道、坡道混合&#xff09; 动态障碍物多&#xff08;行人、推车、宠物随机出现&…

作者头像 李华
网站建设 2026/9/13 6:17:31

5分钟解除PDF限制:用PDFPatcher免费去除PDF复制与打印限制完整指南

5分钟解除PDF限制&#xff1a;用PDFPatcher免费去除PDF复制与打印限制完整指南 【免费下载链接】PDFPatcher PDF补丁丁——PDF工具箱&#xff0c;可以编辑书签、剪裁旋转页面、解除限制、提取或合并文档&#xff0c;探查文档结构&#xff0c;提取图片、转成图片等等 项目地址…

作者头像 李华