接口安全设计:防重放攻击、参数校验与敏感信息脱敏 接口安全设计防重放攻击、参数校验与敏感信息脱敏一、一次简单的 HTTP 请求为什么需要三道防线一个后端接口完成功能开发只是第一步上线前还要回答三个安全层面的问题。这个请求会不会被人截获后换个时间重放传入的参数是否可能超出业务边界导致异常返回的数据里有没有把不该暴露的信息一起带出去了这三个问题分别对应防重放攻击、参数校验和敏感信息脱敏。它们不像 XSS、SQL 注入那样为人熟知但在实际工作中造成的风险同样不容忽视。如果不做重放防御攻击者抓包后重放你的关键接口可能导致重复扣款。如果参数校验偷懒一个超长的输入可能击穿下游的存储系统。如果脱敏不严格用户手机号明文落到日志里数据安全审计一定过不了。二、防重放攻击不是签名就够了所谓的重放攻击就是攻击者拦截了一次合法的 API 请求然后在之后的某个时间点原样重发。如果接口不做重放防御同样的请求会被重复执行产生非预期的副作用。最常见的防御方案是签名 时间戳 Nonce一次性随机数的组合拳。签名用 HMAC-SHA256 对请求参数做摘要附加到请求头。服务端用同样的密钥计算签名并比对防止参数被篡改。时间戳请求中携带客户端时间戳服务端拒绝 5 分钟之前的请求。这能阻止攻击者把请求囤积起来很久之后再发。Nonce每次请求生成一个全局唯一的随机字符串服务端在有效时间窗口内记录已处理过的 Nonce相同 Nonce 的请求直接拒绝。时间戳过期时间和 Nonce 存储是重放防御中的两个关键参数。过期时间越短越安全但如果客户端和服务端时钟偏差过大会导致合法请求被误杀。Nonce 存储需要一个合理的 TTL通常等于过期时间到期自动清理防止内存无限增长。/** * 防重放攻击拦截器 * * 设计要点 * 1. 签名校验防止参数篡改 * 2. 时间戳窗口防止延期重放 * 3. Nonce 去重防止同时间内重复请求 * 4. Nonce 自动过期清理避免内存泄漏 */ Component public class AntiReplayInterceptor implements HandlerInterceptor { // 时间戳有效期5 分钟 private static final long TIMESTAMP_WINDOW_MS 5 * 60 * 1000; // Nonce 最大缓存数防止内存溢出 private static final int MAX_NONCE_SIZE 100000; // 存储已使用的 Noncekeynonce, value过期时间戳 // 使用 ConcurrentHashMap 而不是 Redis 是为了降低外部依赖 // 分布式环境下需要用 Redis 替代 private final ConcurrentHashMapString, Long usedNonces new ConcurrentHashMap(); Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String signature request.getHeader(X-Signature); String timestamp request.getHeader(X-Timestamp); String nonce request.getHeader(X-Nonce); // 第一步参数完整性校验 if (signature null || timestamp null || nonce null) { response.setStatus(403); response.getWriter().write(缺少安全校验参数); return false; } // 第二步时间戳校验 long requestTime; try { requestTime Long.parseLong(timestamp); } catch (NumberFormatException e) { response.setStatus(403); response.getWriter().write(时间戳格式错误); return false; } long now System.currentTimeMillis(); if (Math.abs(now - requestTime) TIMESTAMP_WINDOW_MS) { response.setStatus(403); response.getWriter().write(请求已过期); return false; } // 第三步Nonce 去重 if (usedNonces.putIfAbsent(nonce, now) ! null) { response.setStatus(403); response.getWriter().write(请求已被处理); return false; } // 第四步签名校验 String expectedSign computeSignature(request, timestamp, nonce); if (!expectedSign.equals(signature)) { // 签名不匹配删除已记录的 Nonce usedNonces.remove(nonce); response.setStatus(403); response.getWriter().write(签名校验失败); return false; } // 第五步定期清理过期 Nonce cleanExpiredNonces(now); return true; } /** * 清理过期 Nonce * * 全量扫描在大 Nonce 量时有性能开销 * 生产环境建议改为定时后台任务 */ private void cleanExpiredNonces(long now) { if (usedNonces.size() MAX_NONCE_SIZE / 2) { usedNonces.entrySet().removeIf( entry - (now - entry.getValue()) TIMESTAMP_WINDOW_MS ); } } private String computeSignature( HttpServletRequest request, String timestamp, String nonce) { // 拼接密钥 时间戳 Nonce 请求体 → HMAC-SHA256 // 具体实现省略... return ; } }这个实现适用于单机服务。分布式环境下Nonce 存储必须换成 Redis 等共享存储否则不同节点间 Nonce 不共享重放攻击绕得过去。三、参数校验三层过滤逐级收紧参数校验看起来简单——不就是写几个if判断吗但实践中乱写校验的危害和没写差不多。参数校验应该分三层进行格式校验手机号是不是 11 位数字邮箱有没有 这些检查独立于任何业务逻辑纯属格式层面的约束。范围校验页码是不是负数金额是不是超大数组长度有没有上限这一层防止参数在合法格式下超出业务边界。业务规则校验这个订单 ID 在系统中存在吗这个用户的积分够不够这一层依赖数据库状态属于最深层的校验。三层校验的顺序不能乱。必须先做格式校验——它是纯 CPU 计算几乎无代价。再做范围校验——也是本地判断不产生外部依赖。最后才做业务规则校验——这一步通常需要查数据库或调下游服务成本最高。如果前两层就能过滤掉的坏请求就不该让它们走到第三层去消耗数据库连接。具体实现可以用 JSR-303 Bean Validation 规范NotNull、Min、Pattern等注解处理格式和范围校验在 Service 层再写显式的业务规则校验。注解方式的好处是校验规则紧贴在数据模型上不分散在业务代码各处。四、响应脱敏不该出去的一个字都别带脱敏和加密不是一回事。加密是把数据变成不可读的形式后存储或传输有密钥的人可以解密还原。脱敏是把敏感字段的部分字符替换为掩码符号如138****5678即使在内部日志和监控中暴露了攻击者也拿不到完整信息。常见的脱敏对象包括手机号保留前 3 后 4、身份证号保留前 6 后 4、银行卡号保留后 4 位、邮箱保留首字符和域名。脱敏的实现应该在序列化层面统一处理如 Jackson 的自定义 Serializer而不是在每个业务代码里手动脱敏——手动的方式一定会遗漏。脱敏带来的一个连带问题是开发排查时看不到完整数据影响效率。解决方案是为不同环境设置不同脱敏级别。生产环境严格脱敏测试环境和本地开发环境保留明文。这个级别由配置中心下发动态生效。五、总结接口安全的这三道防线——防重放、参数校验、响应脱敏——各自解决的问题不同但都指向一个共同目标让系统对不可信的外部输入保持稳健。防重放保护的是请求的不可复用性参数校验保护的是系统的边界不被击穿脱敏保护的是数据的泄露面。它们都是安全防御体系中最基础也最容易实现的部分没什么高深的原理但缺了其中任何一道系统的安全短板就会暴露。