Java框架快速入门: Spring Security+OAuth2之多因子认证缓存服务

纲要

  • 多因子认证(MFA)与TOTP核心流程
  • 为何需要缓存用户临时状态
  • 技术选型:Redis + Redisson 客户端
  • 核心实现:UserCacheService
    • 生成随机MFA标识并存储用户信息
    • 设置基于TOTP时间步长的过期时间
    • 按MFA ID获取缓存用户
    • 校验TOTP码并清理缓存
  • 嵌入式 Redis 测试环境
  • 代码结构与可运行示例
  • 启动与验证

背景与问题

在实现基于时间的一次性密码(TOTP)的多因子认证时,用户初次登录后需要输入六位验证码。服务端必须在两次请求之间记住"正在等待MFA验证"的用户信息,且该信息只是临时有效(通常几分钟)。

直接使用数据库会增加无效IO且可能遗留脏数据,引入带有时效的缓存是最佳选择。

本文基于Spring Boot + Spring Security环境,使用Redis作为缓存中间件,Redisson作为访问客户端,封装一个轻量级的UserCacheService,完整展示多因子认证中的缓存逻辑。

依赖与环境

核心依赖在pom.xml中如下所示:

xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.5</version>
</dependency>

<!-- 嵌入式Redis,仅用于测试 -->
<dependency>
    <groupId>it.ozimov</groupId>
    <artifactId>embedded-redis</artifactId>
    <version>0.7.3</version>
    <scope>test</scope>
</dependency>

Redisson负责与Redis交互,嵌入式Redis可以让你在本地测试时无需单独启动Redis服务。

常量定义

为缓存区域命名并统一管理,在常量接口中定义一个字符串常量:

java 复制代码
package com.example.security.constant;

public interface Constants {
    String USER_MFA_CACHE = "userMfaCache";
}

这个名称将作为Redis中Map结构的标识。

工具类:随机MFA ID生成

当用户进入MFA验证阶段时,服务端生成一个唯一随机串作为Key,与用户绑定存入Redis。工具类如下:

java 复制代码
package com.example.security.util;

import org.springframework.stereotype.Component;
import java.security.SecureRandom;

@Component
public class RandomUtil {

    private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    private final SecureRandom random = new SecureRandom();

    public String generateRandomString(int length) {
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            sb.append(CHARS.charAt(random.nextInt(CHARS.length())));
        }
        return sb.toString();
    }
}

这里生成12位字母数字混合串,可根据安全需求调整长度。

核心服务:UserCacheService

该Service负责:

  1. 缓存待验证的用户(cacheUser),返回MFA ID。
  2. 根据MFA ID取出用户(getUser)。
  3. 校验TOTP码,并清除缓存(verifyTotp),返回验证成功的用户。

完整实现如下:

java 复制代码
package com.example.security.service;

import com.example.security.constant.Constants;
import com.example.security.model.User;
import com.example.security.util.RandomUtil;
import lombok.RequiredArgsConstructor;
import org.redisson.api.RMapCache;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class UserCacheService {

    private final RedissonClient redissonClient;
    private final RandomUtil randomUtil;

    /**
     * 缓存用户,返回MFA ID
     * @param user 待验证用户
     * @return 生成的MFA ID
     */
    public String cacheUser(User user) {
        RMapCache<String, User> cache = redissonClient.getMapCache(Constants.USER_MFA_CACHE);
        String mfaId = randomUtil.generateRandomString(12);

        // 如果缓存中不存在该ID,则放入
        if (!cache.containsKey(mfaId)) {
            int ttlInSeconds = TotpUtil.DEFAULT_TIME_STEP_SECONDS; // 例如300秒
            cache.put(mfaId, user, ttlInSeconds, TimeUnit.SECONDS);
        }
        return mfaId;
    }

    /**
     * 根据MFA ID获取缓存的用户
     * @param mfaId 前端带回的MFA ID
     * @return Optional包装的用户
     */
    public Optional<User> getUser(String mfaId) {
        RMapCache<String, User> cache = redissonClient.getMapCache(Constants.USER_MFA_CACHE);
        if (cache.containsKey(mfaId)) {
            return Optional.ofNullable(cache.get(mfaId));
        }
        return Optional.empty();
    }

    /**
     * 验证TOTP码,成功则删除缓存并返回用户
     * @param mfaId MFA ID
     * @param code 用户提交的验证码
     * @return 验证成功返回用户,否则返回Optional.empty()
     */
    public Optional<User> verifyTotp(String mfaId, String code) {
        RMapCache<String, User> cache = redissonClient.getMapCache(Constants.USER_MFA_CACHE);
        // 先检查缓存是否存在
        if (!cache.containsKey(mfaId)) {
            return Optional.empty();
        }
        User cachedUser = cache.get(mfaId);
        if (cachedUser == null) {
            return Optional.empty();
        }
        try {
            byte[] secretKey = Base64.getDecoder().decode(cachedUser.getMfaSecret());
            if (TotpUtil.verifyTotp(secretKey, code)) {
                // 验证成功,移除缓存
                cache.remove(mfaId);
                return Optional.of(cachedUser);
            }
        } catch (Exception e) {
            // 密钥无效等异常,视为验证失败
        }
        return Optional.empty();
    }
}

要点说明:

  • RMapCache提供了带TTL的Map结构,put(key, value, ttl, timeUnit)会在TTL到期后自动清除。
  • TTL取自由TotpUtil定义的DEFAULT_TIME_STEP_SECONDS(通常300秒即5分钟),与TOTP窗口保持一致。
  • verifyTotp中解码用户的MFA密钥(Base64存储),调用TotpUtil.verifyTotp验证动态码,成功后移除缓存,保证一次性使用。

TOTP工具参考

为完整性,给出TotpUtil中的关键常量与方法签名:

java 复制代码
package com.example.security.util;

public class TotpUtil {
    public static final int DEFAULT_TIME_STEP_SECONDS = 300; // 5分钟

    public static boolean verifyTotp(byte[] secret, String code) {
        // 使用TOTP算法校验,此处省略具体实现,可使用Google Auth库
    }
}

用户实体示例

java 复制代码
package com.example.security.model;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private boolean usingMfa;
    private String mfaSecret; // Base64编码的TOTP密钥
}

集成到认证流程

下面是一个简化的Controller展示如何调用:

java 复制代码
package com.example.security.controller;

import com.example.security.model.User;
import com.example.security.service.UserCacheService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;
import java.util.Optional;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {

    private final UserCacheService userCacheService;

    /**
     * 第一因子认证通过后,生成MFA ID返回给前端
     */
    @PostMapping("/mfa/init")
    public ResponseEntity<?> initMfa(@RequestBody User authenticatedUser) {
        String mfaId = userCacheService.cacheUser(authenticatedUser);
        return ResponseEntity.ok(Map.of("mfaId", mfaId));
    }

    /**
     * 前端提交MFA ID和验证码完成验证
     */
    @PostMapping("/mfa/verify")
    public ResponseEntity<?> verifyMfa(@RequestParam String mfaId,
                                       @RequestParam String code) {
        Optional<User> verifiedUser = userCacheService.verifyTotp(mfaId, code);
        if (verifiedUser.isPresent()) {
            // 签发JWT或创建SecurityContext
            return ResponseEntity.ok(Map.of("token", "generated_jwt_token"));
        }
        return ResponseEntity.status(401).body("MFA verification failed");
    }
}

启动与验证

1. 启动Redis

在开发环境可使用Docker快速启动:

bash 复制代码
docker run -d --name redis -p 6379:6379 redis:7-alpine

2. 配置application.yml

yaml 复制代码
spring:
  redis:
    host: localhost
    port: 6379
# Redisson 自动配置,无需额外配置

3. 验证接口

/auth/mfa/init发送第一因子认证通过的用户JSON,得到mfaId,随后调用/auth/mfa/verify?mfaId=xxx&code=123456完成MFA验证。

项目代码结构

dir 复制代码
src/main/java/com/example/security/
├── constant/
│   └── Constants.java
├── controller/
│   └── AuthController.java
├── model/
│   └── User.java
├── service/
│   └── UserCacheService.java
└── util/
    ├── RandomUtil.java
    └── TotpUtil.java

总结

本文介绍了在 Spring Security + OAuth2 环境下实现 TOTP 多因子认证时,通过 Redis 缓存临时用户状态的完整方案。

核心思路是:用户通过第一因子认证后,服务端生成随机 MFA ID,将用户对象以该 ID 为键存入带 TTL 的 Redis Map 缓存(TTL 与 TOTP 时间步长一致)。验证时前端回传 MFA ID 与动态码,服务端取出用户、校验 TOTP 并立即删除缓存,确保一次有效。

采用 Redisson 的 RMapCache 简化过期控制,结合嵌入式 Redis 完成测试,最终将缓存服务融入认证 Controller,形成可运行的轻量级多因子认证缓存层。

相关推荐
雪芽蓝域zzs1 小时前
第三十六节:keep-alive 页面缓存(Vue3 + 后台管理系统)
前端·vue.js·缓存
wuminyu1 小时前
Kafka配置TLS/SSL加密传输时零拷贝失效分析
java·linux·c语言·jvm·c++
我是苏苏1 小时前
Redis开发09:Windows系统下主从复制的配置教程
数据库·redis·缓存
边境悍匪1 小时前
蜗牛学苑 Java 智能体学习 Day41|ElementPlus 布局、Vue‑Router 登录、Axios 思维导图复盘
java·vue.js·学习
心中有你02142 小时前
Java Swing实现校园最短路径导航系统(Dijkstra算法完整源码+详细解析)
java·开发语言·算法
重生之小比特2 小时前
【Java SE】数组的定义与使用
java·开发语言
摆烂z2 小时前
定时任务同步数据--续跑&重试
java·mybatis·ai编程
CAE虚拟与现实3 小时前
docker desktop中的build功能是要build什么
java·docker·容器
allnlei5 小时前
s6-overlay - 装在 Docker 容器里的轻量级管家
java·docker·容器