Java 接口防重放企业实战完整落地代码
一、什么是重放攻击?
攻击者通过抓包、网络监听等手段,非法截取客户端发送给服务端的完整合法请求报文,不修改报文内容、参数、签名等任何数据,在任意时间多次、重复发送至服务端,利用服务端对重复请求无拦截校验的漏洞,触发业务重复执行的恶意攻击方式。该攻击核心特点是复用合法有效请求,伪装成正常业务请求,服务端无法天然区分是用户正常请求还是攻击者重放请求。
核心攻击原理:正常接口请求无时效性、唯一性校验,合法报文一旦被窃取,即可无限复用,突破前端防抖、按钮置灰等简易防护手段。
典型业务危害:触发重复下单、重复扣款、重复积分发放、重复注册、订单状态错乱、数据冗余脏数据、接口刷量压测、资金损失等核心业务风险。
典型攻击场景:接口回调请求反复提交、请求劫持、重复提交、参数篡改、过期请求刷量。
二、防重放核心四大方案(面试必问)
1. 时间戳 Timestamp(最基础通用、面试核心高频)
一、完整定义(面试背诵版)
时间戳防重放是接口安全最基础、必备的第一道防护。核心思想是:所有接口请求具备时效性,拒绝永久有效报文。客户端每次请求必须携带当前毫秒级时间戳,服务端通过时间窗口校验,判定请求是否过期,彻底拦截长期抓包重放攻击。
二、核心原理
请求头携带客户端毫秒时间戳 X-Timestamp,服务端固定校验两条规则:
-
校验客户端时间戳与服务器当前时间差值不超过固定有效时间窗口(生产通用 5 分钟)
-
超出时间窗口直接 403 拦截,杜绝老旧报文被无限重放
三、优缺点(面试满分作答)
-
优点:逻辑极简、零存储成本、高性能无损耗、无需依赖Redis/数据库、兼容所有请求方式,是所有接口防重放的基础标配。
-
核心缺点:无法防止时间窗口期内的重复重放;客户端与服务端时钟不同步、客户端时间异常超前/滞后,会导致合法请求误拦截。
四、生产核心避坑点(面试加分)
-
必须使用毫秒时间戳:禁止使用秒级时间戳,秒级精度太低,极易被爆破重放。
-
时间窗口不能过大/过小:过小(1分钟)移动端网络卡顿容易请求超时误拦截;过大(10分钟)会放大重放攻击窗口期风险。
-
兼容时钟偏移:不做绝对时间相等校验,只做时间绝对值差值校验,适配客户端本地时间不准的场景。
-
禁止服务端生成时间戳:服务端生成时间戳无法校验请求时效性,失去防重放意义,必须由客户端每次实时生成。
五、时钟偏移解决方案(企业实战)
针对客户端和服务端时间不同步导致的误拦截问题,主流解决方案:
-
服务端采用正负时间窗口兼容:允许客户端时间小幅超前/滞后服务端时间。
-
前端初始化时同步服务端时间,以服务端时间偏移量修正本地时间戳。
-
核心业务不单独依赖时间戳,必须搭配 Nonce随机串 + 签名 组合防护。
六、技术优化方案
单独时间戳只能防过期重放,无法防窗口期内的重复重放,
生产环境必须组合优化:时间戳 + Nonce一次性随机数 + 签名校验,构成基础安全三角防护体系。
七、企业生产级增强代码(完整版、可直接上线)
针对简易版代码存在的异常裸露、返回格式不统一、无法适配集群、容错性差等问题,提供生产可用、规范严谨、面试可深度讲解的增强版本。
1. 全局统一返回结果类(生产标准)
/**
* 全局统一返回实体
*/
public class Result<T> {
private Integer code;
private String msg;
private T data;
public static <T> Result<T> fail(Integer code, String msg) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMsg(msg);
return result;
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMsg("操作成功");
result.setData(data);
return result;
}
public static <T> Result<T> success() {
return success(null);
}
// getter/setter
public Integer getCode() { return code; }
public void setCode(Integer code) { this.code = code; }
public String getMsg() { return msg; }
public void setMsg(String msg) { this.msg = msg; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
}
2. 增强版时间戳工具类(容错+严谨校验)
/**
* 生产级时间戳防重放工具类
* 修复:时钟漂移、负数时间戳、空值、格式异常等各类线上问题
*/
public class TimestampCheckUtil {
// 有效时间窗口 5分钟(毫秒)
private static final long VALID_WINDOW = 5 * 60 * 1000;
/**
* 校验时间戳是否合法有效
* @param clientTs 客户端毫秒时间戳
* @return true=合法请求 false=过期/非法重放请求
*/
public static boolean isValid(Long clientTs) {
// 1. 空值、负数、0 直接拦截
if (clientTs == null || clientTs <= 0) {
return false;
}
long now = System.currentTimeMillis();
// 2. 兼容客户端时间小幅超前/滞后,取绝对值
long offset = Math.abs(now - clientTs);
// 3. 超出窗口判定为过期重放
return offset <= VALID_WINDOW;
}
}
3. 生产级拦截器(统一JSON返回、异常全覆盖)
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 生产级时间戳防重放拦截器
* 特性:统一返回格式、全局异常捕获、规范响应头、兼容线上各类异常
*/
@Component
public class AntiReplayTimestampInterceptor implements HandlerInterceptor {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final String TS_HEADER = "X-Timestamp";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 设置响应格式
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
// 获取请求头时间戳
String tsValue = request.getHeader(TS_HEADER);
// 1. 时间戳为空
if (tsValue == null || tsValue.trim().isEmpty()) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "请求非法,缺少时间戳")));
return false;
}
Long clientTs;
try {
// 2. 时间戳格式校验,防止非数字参数
clientTs = Long.parseLong(tsValue.trim());
} catch (NumberFormatException e) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "时间戳格式非法")));
return false;
}
// 3. 时间戳过期/重放校验
if (!TimestampCheckUtil.isValid(clientTs)) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "请求已过期,禁止重放攻击")));
return false;
}
// 全部校验通过,放行请求
return true;
}
}
4. 拦截器配置类(精准路径拦截、放行白名单)
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
// 注入时间戳防重放拦截器
private final AntiReplayTimestampInterceptor antiReplayTimestampInterceptor;
public WebSecurityConfig(AntiReplayTimestampInterceptor antiReplayTimestampInterceptor) {
this.antiReplayTimestampInterceptor = antiReplayTimestampInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(antiReplayTimestampInterceptor)
// 拦截所有业务接口
.addPathPatterns("/api/**")
// 放行无需安全校验的接口
.excludePathPatterns("/api/login", "/api/register", "/api/captcha/get");
}
}
代码实战 & 面试总结(必背)
-
核心逻辑:通过客户端毫秒时间戳与服务端当前时间做差值,限定5分钟有效窗口,过期请求直接拦截,杜绝永久报文重放。
-
代码优势:无Redis依赖、无复杂加密、性能极高、适配小型项目快速落地、零业务侵入。
-
代码缺陷:无法拦截窗口期内的重复请求,单独使用不安全,生产核心业务必须搭配Nonce随机串使用。
-
前端适配:每次请求请求头携带
X-Timestamp: Date.now()即可,每次请求时间戳唯一。
八、生产级关键优化点(面试高分核心)
-
全覆盖异常捕获:针对空时间戳、空字符串、非数字格式、负数时间戳等所有非法场景统一拦截,避免程序报错堆栈泄露。
-
标准化JSON返回:摒弃原生拼接JSON字符串,使用Jackson序列化,格式统一、无语法错误,适配前端统一异常处理。
-
解决时钟漂移问题:采用双向时间差值校验,兼容客户端时间快/慢服务端的场景,大幅降低合法请求误拦截率。
-
精准路径管控:支持自定义白名单,登录、验证码等公共接口放行,不影响基础业务使用。
-
零内存泄漏:静态全局ObjectMapper,避免频繁创建对象导致JVM内存浪费,适配高并发场景。
九、面试追问标准答案
Q:生产环境只用时间戳防重放可以吗?
A:不可以,仅能作为第一层基础防护。时间戳只能拦截超过5分钟的过期重放报文,无法拦截窗口期内的重复抓包重放。生产环境必须采用 时间戳 + Nonce随机串 + HMAC签名 三重组合防护,时间戳解决过期重放,Nonce解决窗口期重复请求,签名解决参数篡改问题。
Q:为什么要用毫秒级时间戳,不用秒级?
A:秒级时间戳精度过低,1秒内可发送大量重复请求,攻击者可快速爆破重放;毫秒级精度可保证单次请求时间唯一,极大提升攻击门槛。
2. 随机串 Nonce / 一次性随机数(主流搭配时间戳、面试核心高频)
一、完整定义(面试背诵版)
Nonce(Number Used Once,一次性随机数)是单请求唯一、一次性失效的随机字符串,是时间戳方案的核心互补防护手段。
核心思想:解决时间窗口内的重复重放漏洞。在5分钟有效时间窗口内,每一次合法接口请求的Nonce必须全局唯一,一旦被使用立即作废,杜绝攻击者抓包后在窗口期内重复提交请求。
二、核心原理
客户端每次发起接口请求,随机生成一串32位UUID随机串作为 X-Nonce 请求头,配合时间戳组合校验,完整校验逻辑:
-
客户端每次请求生成全新、永不重复的Nonce,禁止复用、禁止固定写死;
-
服务端校验时间戳合法(未过期)后,立即校验当前Nonce是否存在于Redis缓存;
-
不存在则判定为合法请求,写入Redis缓存,设置和时间戳一致的5分钟过期时间;
-
已存在则直接拦截,判定为窗口期重放攻击/重复请求。
核心组合流程:时效性(Timestamp) + 唯一性(Nonce) = 彻底杜绝所有重放攻击
三、存储设计(单机/分布式全覆盖)
-
单机场景:可使用本地Caffeine缓存/内存缓存,高性能无中间件依赖,适合单体小型项目。
-
分布式/微服务场景(生产主流):统一使用Redis全局存储,所有服务节点共享Nonce记录,彻底解决集群多节点校验不一致问题。
-
缓存设计规范:Key=自定义前缀+Nonce值,过期时间严格对齐5分钟时间窗口,做到过期自动清理。
四、优缺点(面试满分作答)
-
核心优点:完美弥补时间戳短板,杜绝窗口期内重复重放;单次请求永久失效,安全性极高;搭配时间戳使用无死角防护;适配所有业务接口。
-
核心缺点:需要依赖缓存存储,有少量Redis开销;需要客户端配合生成随机串;单独使用无法解决请求过期问题,必须和时间戳绑定使用。
五、生产核心避坑点(面试加分)
-
Nonce长度规范:生产统一使用32位无横线UUID,禁止过短随机串,防止随机碰撞导致误拦截。
-
禁止客户端复用Nonce:每次请求必须重新生成,前端缓存、重复调用接口必须刷新Nonce和Timestamp。
-
必须原子写入Redis:高并发场景必须使用
SET NX EX原子命令,防止并发同一请求同时放行,击穿防重机制。 -
过期时间严格对齐:Nonce缓存过期时间必须和时间戳窗口一致,避免缓存残留、内存堆积或提前失效。
-
禁止本地缓存做集群校验:微服务集群绝对不能用本地内存,会出现节点校验不一致、重放绕过漏洞。
六、技术适配方案
时间戳负责拦截过期老旧报文,Nonce负责拦截窗口期内重复报文,二者强绑定、缺一不可,构成接口防重放基础双防线,再叠加签名防篡改,形成企业标准安全三角体系。
✅ 企业生产级实战代码(Nonce + Timestamp 组合防重放)
基于前文时间戳代码扩展,实现生产可用、原子防重、异常全覆盖、集群适配的完整组合防重放拦截器,可直接上线。
1. 防重放统一常量
/**
* 防重放全局常量
* 统一时间窗口、缓存前缀、请求头字段
*/
public class AntiReplayConst {
// 有效时间窗口 5分钟
public static final long VALID_TIME_WINDOW = 5 * 60 * 1000;
// Redis Nonce缓存前缀
public static final String REPLAY_NONCE_PREFIX = "replay:nonce:";
// 请求头常量
public static final String HEADER_TIMESTAMP = "X-Timestamp";
public static final String HEADER_NONCE = "X-Nonce";
}
2. Nonce防重Redis工具类(原子操作、生产级)
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import static com.demo.consts.AntiReplayConst.*;
@Component
public class NonceReplayUtil {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 校验Nonce是否重复(原子防重)
* @param nonce 一次性随机串
* @return true=重复请求/重放攻击 false=合法首次请求
*/
public boolean isRepeatNonce(String nonce) {
String key = REPLAY_NONCE_PREFIX + nonce;
// NX: 不存在才写入 EX: 设置过期时间
// 原子操作,杜绝并发重复请求击穿
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "1", VALID_TIME_WINDOW / 1000, TimeUnit.SECONDS);
// 写入失败 = key已存在 = 重复请求
return Boolean.FALSE.equals(success);
}
}
3. 组合防重拦截器(Timestamp + Nonce 双校验)
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import static com.demo.consts.AntiReplayConst.*;
@Component
public class AntiReplayInterceptor implements HandlerInterceptor {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Resource
private NonceReplayUtil nonceReplayUtil;
/**
* 双校验:时间戳过期校验 + Nonce唯一校验
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
// 1. 获取请求头
String tsStr = request.getHeader(HEADER_TIMESTAMP);
String nonce = request.getHeader(HEADER_NONCE);
// 基础非空校验
if (tsStr == null || tsStr.trim().isEmpty() || nonce == null || nonce.trim().isEmpty()) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "请求缺少安全校验参数")));
return false;
}
// 2. 时间戳合法性校验
Long clientTs;
try {
clientTs = Long.parseLong(tsStr.trim());
} catch (NumberFormatException e) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "时间戳格式非法")));
return false;
}
long now = System.currentTimeMillis();
if (Math.abs(now - clientTs) > VALID_TIME_WINDOW) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "请求已过期,禁止重放")));
return false;
}
// 3. Nonce唯一防重校验(核心:拦截窗口期内重放)
if (nonceReplayUtil.isRepeatNonce(nonce)) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "请勿重复请求,疑似重放攻击")));
return false;
}
return true;
}
}
4. 刷新拦截器配置
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
@Resource
private AntiReplayInterceptor antiReplayInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(antiReplayInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/login", "/api/register", "/api/captcha/get");
}
}
七、代码实战 & 面试必背总结
-
防护闭环:Timestamp防「过期重放」,Nonce防「窗口期内重复重放」,彻底解决纯时间戳的安全漏洞。
-
原子安全:基于Redis NX原子命令,解决高并发下同一请求同时通过校验的击穿问题。
-
集群兼容:全局Redis存储,微服务多节点共享校验数据,无集群漏洞。
-
自动清理:Nonce缓存随时间窗口自动过期,无需手动清理,无内存堆积压力。
八、高频面试追问
Q:为什么Nonce必须配合时间戳,不能单独使用?
A:如果仅有Nonce无时间戳,攻击者可抓取多年前的旧报文,只要Nonce缓存过期,就能再次重放请求,无法拦截长期过期报文攻击,存在严重安全漏洞。
Q:Nonce缓存为什么要设置过期时间?永久存储不行吗?
A:绝对不行。永久存储会导致Redis数据无限堆积,内存溢出;且5分钟时间窗口外的旧Nonce已经无攻击价值,过期自动清理是生产最优方案。
3. 全局唯一流水号 ID(业务层防重、最终兜底方案)
一、完整定义(面试背诵版)
全局唯一流水号防重,是业务层面最终兜底的防重、幂等方案。区别于 Timestamp、Nonce 的「请求层防重」,流水号属于业务级防重。核心思想:每一笔业务操作对应一个全局唯一、永不重复的业务流水号,服务端以流水号为唯一维度判断业务是否已处理,彻底杜绝重复业务执行。
该方案不依赖请求时效性、不依赖随机串,永久防重、永久幂等,是支付、订单、充值、退款、转账、优惠券发放等资金级核心业务的强制落地方案。
二、核心原理
客户端/上游系统生成全局唯一业务流水号(BizNo),随业务请求一同传入服务端,服务端执行固定校验流程:
-
接口接收业务流水号 BizNo;
-
优先查询 Redis/数据库,判断当前流水号是否已完成业务处理;
-
已存在:直接拦截,返回「请勿重复提交」;
-
不存在:执行业务逻辑,成功后将流水号持久化记录,永久标记已处理。
三、流水号生成规范(生产标准)
禁止使用简单UUID(无序、不可追溯),生产主流两种方案:
-
雪花算法(Snowflake):分布式唯一ID、有序、可溯源、高性能,订单/支付首选;
-
时间戳+机器码+随机数:自定义业务流水号,可读性强、便于日志排查。
四、适用场景(面试必答)
-
资金交易类:支付、退款、转账、充值、提现;
-
权益发放类:优惠券、积分、会员资格、红包发放;
-
状态变更类:订单确认、发货、完结、回调通知;
-
所有不允许重复执行、对数据一致性要求极高的核心业务。
五、优缺点(面试满分作答)
-
核心优点: 1. 无时间窗口限制,永久防重、永久幂等; 2. 业务层面兜底,即使请求层防重(ts+nonce)被绕过,也不会产生脏数据; 3. 便于问题溯源、对账排查、日志追踪; 4. 天然适配分布式、微服务、回调重试场景。
-
核心缺点: 1. 需要额外存储(数据库/Redis)记录流水号; 2. 属于业务层实现,无法拦截单纯的接口重放攻击,只能兜底数据重复问题; 3. 需要上游配合传递唯一流水号。
六、生产核心避坑点(面试加分)
-
流水号必须客户端/上游生成:服务端生成无法保证幂等性,重试请求会生成新ID,导致失效。
-
必须做幂等前置校验:先查再执行业务,禁止先执行再记录,防止并发重复执行。
-
数据库务必加唯一索引:Redis校验存在极小并发击穿,数据库唯一索引做最终兜底。
-
幂等记录持久化:核心业务禁止只存Redis,必须落库,防止缓存丢失导致幂等失效。
-
失败业务允许重试:仅拦截成功状态的重复请求,未完成/失败请求支持正常重试。
七、和 ts/nonce 防重的核心区别(高频面试反问)
-
Timestamp/Nonce:属于接口安全层,防抓包、防重放、防恶意攻击,拦截非法请求;
-
业务流水号:属于业务数据层,防重复业务执行、保证数据幂等、兜底容错;
生产标准组合:接口层(ts+nonce+sign)防攻击 + 业务层(流水号幂等)防脏数据,双层防护万无一失。
✅ 企业生产级实战代码(全局流水号幂等防重)
提供 Redis快速幂等 + 数据库最终兜底 双方案,金融级落地,可直接上线。
1. 业务幂等常量
/**
* 业务幂等防重常量
*/
public class IdempotentConst {
// 业务流水号幂等Key前缀
public static final String BIZ_IDEM_PREFIX = "idem:bizno:";
// 幂等过期时间(7天,根据业务对账周期调整)
public static final long BIZ_IDEM_EXPIRE_SECONDS = 7 * 24 * 60 * 60L;
}
2. 幂等工具类(Redis原子校验)
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import static com.demo.consts.IdempotentConst.*;
@Component
public class BizIdempotentUtil {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 校验业务流水号是否重复
* @param bizNo 全局唯一业务流水号
* @return true=首次请求可执行 false=重复请求/已处理
*/
public boolean checkBizNoUnique(String bizNo) {
if (bizNo == null || bizNo.trim().isEmpty()) {
return false;
}
String key = BIZ_IDEM_PREFIX + bizNo;
// 原子NX防击穿:不存在则写入,标记已处理
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(key, "processed", BIZ_IDEM_EXPIRE_SECONDS, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
/**
* 删除幂等Key,异常场景支持重试
* @param bizNo 业务流水号
*/
public void removeBizKey(String bizNo) {
if (bizNo == null || bizNo.trim().isEmpty()) {
return;
}
String key = BIZ_IDEM_PREFIX + bizNo;
stringRedisTemplate.delete(key);
}
}
3. 数据库兜底SQL(核心必备)
-- 业务订单表、支付表新增流水号唯一索引
-- 杜绝Redis缓存失效、并发击穿导致的重复数据
ALTER TABLE t_order
ADD UNIQUE INDEX uk_order_biz_no (biz_no);
ALTER TABLE t_pay_record
ADD UNIQUE INDEX uk_pay_biz_no (biz_no);
4. Controller实战落地(业务层防重完整示例)
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/api/pay")
public class PayController {
@Resource
private BizIdempotentUtil bizIdempotentUtil;
/**
* 支付提交接口 - 业务幂等防重落地
*/
@PostMapping("/submit")
public Result<String> submitPay(String bizNo, Long amount) {
// 1. 业务流水号幂等校验(前置拦截)
boolean unique = bizIdempotentUtil.checkBizNoUnique(bizNo);
if (!unique) {
return Result.fail(400, "业务已处理,请勿重复提交");
}
try {
// 2. 执行业务逻辑:创建订单、扣余额、生成支付记录
// ...... 业务代码 ......
// 3. 数据库落库(唯一索引兜底,重复直接抛异常)
return Result.success("支付提交成功");
} catch (Exception e) {
// 异常删除幂等Key,支持失败重试
bizIdempotentUtil.removeBizKey(bizNo);
return Result.fail(500, "业务处理失败,请重试");
}
}
}
八、代码实战 & 面试必背总结
-
双层兜底机制:Redis原子校验拦截大部分重复请求,数据库唯一索引兜底极端并发场景,零重复数据。
-
支持失败重试:业务异常自动删除幂等标记,避免临时故障导致用户无法重试。
-
永久业务幂等:区别于5分钟短期防重,适配订单对账、回调重试、长期重试场景。
-
攻击无感防护:即使攻击者抓取请求长期重放,也无法重复执行业务。
九、高频面试追问
Q:为什么有了ts+nonce还要做业务流水号幂等?
A:ts+nonce 是接口安全防护,只防恶意重放攻击;而网络重试、网关重试、用户刷新页面、回调重试等合法重复请求依然会触发业务重复,必须依靠业务流水号幂等兜底,保证数据绝对一致。
Q:Redis幂等为什么还要加数据库唯一索引?
A:Redis存在缓存过期、节点宕机、主从同步延迟等极端情况,存在极小概率击穿风险,数据库唯一索引是最终数据兜底,保证生产绝对不会产生重复脏数据。
4. 签名 Signature(防篡改 + 防重放双保障、企业级核心安全屏障)
一、完整定义(面试背诵版)
签名机制是接口安全的最终校验屏障,区别于时间戳、Nonce 仅能防重放的单一能力,Signature 同时实现防重放 + 防参数篡改双重防护
。核心思想:基于请求全部明文参数+安全密钥,通过不可逆哈希算法生成唯一指纹,请求一旦参数改动、报文伪造、密钥冒用,签名立即失效,彻底解决接口参数篡改、伪造请求、恶意重放漏洞。
Timestamp、Nonce 只能保证「请求不重复、不过期」,无法防止参数被篡改;签名是唯一能拦截篡改攻击的核心方案,生产开放API、支付、金融、第三方对接接口必须强制开启。
二、核心原理
客户端基于本次请求所有信息,通过固定加密规则生成签名,服务端复刻相同规则校验一致性,完整核心逻辑:
-
客户端收集本次请求所有业务参数、时间戳、一次性随机串、应用标识;
-
按照固定字典序排序拼接,剔除空参数、无效参数,避免签名不一致;
-
拼接私密密钥(AppSecret),通过 HMAC-SHA256 生成不可逆签名指纹;
-
请求头携带公开参数与签名,密钥仅服务端留存,客户端永不暴露;
-
服务端复刻相同排序、拼接、加密规则,重新计算签名与客户端签名比对,一致则合法,不一致直接拦截。
三、标准企业级签名规则(面试必背)
生产统一标准拼接公式(杜绝乱序导致验签失败):
参数字典升序拼接 + X-Timestamp + X-Nonce + AppSecret → HMAC-SHA256 → 小写32位签名
合法请求头完整携带参数:X-AppId、X-Timestamp、X-Nonce、X-Sign
四、核心作用(满分面试答案)
-
防参数篡改:攻击者修改URL参数、Body参数、分页参数、金额参数,任意字段改动都会导致签名完全不一致,直接拦截恶意篡改请求。
-
防伪造请求:密钥仅服务端持有,无密钥无法生成合法签名,杜绝攻击者伪造虚假接口请求。
-
辅助防重放:绑定单次请求的 ts+nonce,签名与请求强绑定,无法复用旧签名搭配新参数重放。
-
校验请求合法性:区分合法客户端与非法爬虫、脚本、恶意工具请求。
五、主流加密算法对比(面试高频)
-
MD5:过时、可碰撞、安全性低,仅老旧项目使用,新项目禁止采用。
-
SHA256:单向哈希、无密钥加持易被爆破,适合公开无高安全要求接口。
-
HMAC-SHA256(生产首选):密钥内置哈希运算、抗碰撞、防伪造、安全性最高,金融/支付/开放API强制使用。
六、优缺点分析
-
优点:双重防护(防重放+防篡改)、加密强度高、抗伪造能力强、适配分布式集群、不依赖大量存储、适配所有接口场景。
-
缺点:客户端对接成本高、需要前后端/第三方严格对齐加密规则、增加少量接口计算开销、规则不规范易出现误拦截。
七、生产核心避坑点(面试加分项)
-
必须字典序排序:参数顺序不一致会导致正常请求验签失败,必须固定升序规则。
-
空参数统一过滤:空值、null、空字符串禁止参与拼接,避免两端拼接不一致。
-
Secret 严禁前端硬编码:密钥泄露可被伪造所有签名,前端需通过加密下发、后端动态配置。
-
Body参数必须参与签名:POST/JSON请求必须序列化Body摘要参与签名,仅校验URL参数存在篡改漏洞。
-
禁止固定签名:每次请求必须基于全新ts+nonce生成新签名,禁止复用签名。
✅ 企业生产级实战代码(HMAC-SHA256 完整签名+验签)
适配前文 ts+nonce 防重放逻辑,完整打通时间戳+随机串+签名三层接口安全防护,可直接上线替换原有拦截器。
1. 补充全局安全常量
/**
* 防重放&签名全局常量
*/
public class AntiReplayConst {
// 有效时间窗口 5分钟
public static final long VALID_TIME_WINDOW = 5 * 60 * 1000;
// Redis Nonce缓存前缀
public static final String REPLAY_NONCE_PREFIX = "replay:nonce:";
// 签名配置
public static final String APP_ID = "business_api_001";
public static final String APP_SECRET = "Jf9s2kL8mP0qR3tX7yZ1bN4vC6dG8hK";
// 请求头常量
public static final String HEADER_TIMESTAMP = "X-Timestamp";
public static final String HEADER_NONCE = "X-Nonce";
public static final String HEADER_APP_ID = "X-AppId";
public static final String HEADER_SIGN = "X-Sign";
}
2. 生产级HMAC-SHA256签名工具类
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import java.util.Map;
import java.util.TreeMap;
/**
* 生产级签名工具类
* 规则:参数字典升序 + 时间戳 + nonce + 密钥 HMAC-SHA256加密
*/
public class SignUtil {
/**
* 生成签名
* @param params 请求参数
* @param timestamp 时间戳
* @param nonce 一次性随机串
* @param secret 密钥
* @return 小写签名字符串
*/
public static String generateSign(Map<String, String> params, String timestamp, String nonce, String secret) {
// 1. 参数按字典升序排序,保证顺序绝对统一
TreeMap<String, String> sortMap = new TreeMap<>(params);
StringBuilder sb = new StringBuilder();
// 2. 拼接非空参数,过滤空值防止两端不一致
for (Map.Entry<String, String> entry : sortMap.entrySet()) {
if (StrUtil.isNotBlank(entry.getValue())) {
sb.append(entry.getKey()).append(entry.getValue());
}
}
// 3. 拼接时间戳、随机串、密钥
sb.append(timestamp).append(nonce).append(secret);
// 4. HMAC-SHA256加密,转小写统一格式
HMac hMac = new HMac(HmacAlgorithm.HmacSHA256, secret.getBytes());
return hMac.digestHex(sb.toString()).toLowerCase();
}
/**
* 校验签名
* @param params 请求参数
* @param timestamp 时间戳
* @param nonce 一次性随机串
* @param secret 密钥
* @param clientSign 客户端签名
* @return true=签名合法 false=篡改/伪造
*/
public static boolean verifySign(Map<String, String> params, String timestamp, String nonce, String secret, String clientSign) {
if (StrUtil.isBlank(clientSign)) {
return false;
}
String serverSign = generateSign(params, timestamp, nonce, secret);
// 服务端计算签名与客户端签名强一致比对
return serverSign.equals(clientSign);
}
}
3. 终极三层防护拦截器(ts + nonce + sign)
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import static com.demo.consts.AntiReplayConst.*;
@Component
public class FullSecurityInterceptor implements HandlerInterceptor {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Resource
private NonceReplayUtil nonceReplayUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setContentType("application/json;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
// 1. 获取全部安全请求头
String appId = request.getHeader(HEADER_APP_ID);
String tsStr = request.getHeader(HEADER_TIMESTAMP);
String nonce = request.getHeader(HEADER_NONCE);
String clientSign = request.getHeader(HEADER_SIGN);
// 基础非空校验
if (StrUtil.hasBlank(appId, tsStr, nonce, clientSign)) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "缺少安全校验请求头,请求非法")));
return false;
}
// 2. 校验应用合法性
if (!APP_ID.equals(appId)) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "非法客户端")));
return false;
}
// 3. 时间戳过期校验
Long clientTs;
try {
clientTs = Long.parseLong(tsStr.trim());
} catch (NumberFormatException e) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "时间戳格式非法")));
return false;
}
long now = System.currentTimeMillis();
if (Math.abs(now - clientTs) > VALID_TIME_WINDOW) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "请求已过期,禁止重放")));
return false;
}
// 4. Nonce防重放校验(拦截窗口期重复请求)
if (nonceReplayUtil.isRepeatNonce(nonce)) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "请勿重复请求,疑似重放攻击")));
return false;
}
// 5. 组装请求参数,验签防篡改(核心)
Map<String, String> paramMap = new HashMap<>();
request.getParameterMap().forEach((k, v) -> paramMap.put(k, v[0]));
boolean signPass = SignUtil.verifySign(paramMap, tsStr, nonce, APP_SECRET, clientSign);
if (!signPass) {
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(Result.fail(403, "签名非法,参数已被篡改或请求伪造")));
return false;
}
// 三层校验全部通过,放行请求
return true;
}
}
4. 刷新拦截器配置
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
@Resource
private FullSecurityInterceptor fullSecurityInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(fullSecurityInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/login", "/api/register", "/api/captcha/get");
}
}
八、代码实战 & 生产落地核心总结(面试必背)
-
全链路安全闭环:本次终极拦截器整合时间戳时效校验、Nonce唯一防重、HMAC-SHA256签名验签三层接口防护,彻底解决接口过期重放、窗口期重复请求、参数篡改、请求伪造四大核心安全漏洞,是互联网金融、支付交易、开放API接口的标准落地方案。
-
杜绝各类线上漏洞:相较于单一防重方案,组合防护可拦截所有恶意攻击场景,包括抓包长期重放、短时批量重放、参数篡改薅羊毛、伪造非法请求等,同时规避时钟偏移、参数乱序、空值拼接导致的合法请求误拦截问题,兼容性与安全性拉满。
-
高并发集群适配:基于Redis NX EX原子命令实现分布式防重,规避微服务多节点并发击穿问题;统一字典序参数签名规则,解决集群环境、前后端参数拼接不一致导致的验签失败,完全适配线上集群高并发场景。
-
生产极致容错设计:全覆盖空值、格式异常、非法应用、签名不匹配等异常场景,统一标准化JSON错误返回,无原生异常堆栈泄露,兼顾安全性与用户体验;同时区分非法攻击拦截与正常业务异常,不影响合法请求重试机制。
-
轻量化高性能落地:整体方案无复杂依赖、加密运算性能损耗极低,Redis缓存自动过期清理,无内存堆积、无数据冗余,适配大流量接口场景,兼顾安全与接口响应性能。
九、高频面试深度追问(满分答疑)
Q:为什么签名必须结合Timestamp+Nonce,不能单独使用签名防重放?
A:单独签名只能防篡改、防伪造,无法防重放攻击。攻击者可抓取一份合法带签名的报文,在任意时间无限重放请求,签名不会失效。绑定Timestamp实现请求时效性、绑定Nonce实现单次请求唯一性,三者结合才能同时实现防篡改、防伪造、防过期重放、防短时重复重放的全维度防护。
Q:POST JSON请求只校验URL参数签名有什么漏洞?如何解决?
A:存在严重参数篡改漏洞,攻击者可随意修改请求Body内的核心业务参数(金额、订单号、权益数量等),URL参数不变则签名校验通过,成功绕过防护。生产解决方案:对POST请求的JSON Body进行序列化摘要,将Body摘要一并纳入签名拼接规则,实现全参数校验。
Q:HMAC-SHA256相比普通SHA256安全优势是什么?
A:普通SHA256是无密钥单向哈希,参数泄露后可被暴力碰撞、伪造签名;HMAC-SHA256基于密钥加密哈希,密钥仅服务端私密留存,即使请求参数完全泄露,攻击者也无法生成合法签名,彻底杜绝签名伪造,安全性适配金融级业务场景。
Q:线上出现偶尔验签失败是什么原因?如何排查解决?
A:常见原因包含:前后端参数排序不一致、空值参数参与拼接、Body序列化格式不统一、客户端时间偏移过大、Nonce重复使用。解决方案:统一字典升序拼接规则、过滤所有空参数、固定JSON序列化格式、前端同步服务端时间、每次请求刷新全新Timestamp和Nonce。
更多推荐

所有评论(0)