接口重复请求如何过滤

wen 网络安全 28

本文目录导读:

接口重复请求如何过滤

  1. 防抖与节流(前端基础方案)
  2. 按钮状态锁(最常用、最可靠)
  3. 请求唯一 ID 去重(后端+前端配合)
  4. 全局请求拦截器(成熟方案)
  5. Token 机制(幂等性方案)
  6. 综合最佳实践(推荐组合)
  7. 注意事项

接口重复请求(重复提交)是前端开发中非常常见的问题,主要发生在网络延迟、用户快速点击或支付/订单等敏感操作时,以下是常见的过滤策略,按推荐程度实现难度排序:

防抖与节流(前端基础方案)

防抖(Debounce)—— 适用于点击提交

原理:用户停止操作后一段时间再执行,如果再次触发则重置计时器。

// 简单防抖实现
function debounce(fn, delay = 300) {
  let timer = null;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
// 使用场景:搜索框输入、表单提交
const submitForm = debounce((data) => {
  api.submitOrder(data);
}, 500);

节流(Throttle)—— 适用于频繁触发

原理:一段时间内只执行一次。

function throttle(fn, delay = 300) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= delay) {
      fn.apply(this, args);
      lastTime = now;
    }
  };
}
// 使用场景:按钮点击、滚动事件
const handleClick = throttle(() => {
  console.log('click');
}, 1000);

按钮状态锁(最常用、最可靠)

原理:点击后立即禁用按钮,请求完成后恢复。

Vue 示例

<template>
  <button :disabled="loading" @click="submit">
    {{ loading ? '提交中...' : '提交' }}
  </button>
</template>
<script>
export default {
  data() {
    return { loading: false };
  },
  methods: {
    async submit() {
      if (this.loading) return;
      this.loading = true;
      try {
        await api.submitOrder(data);
      } finally {
        this.loading = false;
      }
    }
  }
};
</script>

React 示例

function SubmitButton() {
  const [loading, setLoading] = useState(false);
  const handleSubmit = useCallback(async () => {
    if (loading) return;
    setLoading(true);
    try {
      await submitOrder(data);
    } finally {
      setLoading(false);
    }
  }, [loading]);
  return (
    <button disabled={loading} onClick={handleSubmit}>
      {loading ? '提交中...' : '提交'}
    </button>
  );
}

请求唯一 ID 去重(后端+前端配合)

原理:每个请求携带唯一ID,后端保存已处理的ID,相同ID返回已有结果。

// 前端生成 UUID
const requestId = uuidv4();
// 发起请求
const response = await fetch('/api/order', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    requestId,  // 唯一标识
    data: { productId: 123 }
  })
});

后端处理(伪代码)

# Redis 中存储已处理的 requestId
if redis.exists(request_id):
    return {"code": 200, "message": "重复请求", "data": existing_result}
else:
    redis.setex(request_id, 3600, "processing")  # 1小时过期
    # 处理业务逻辑
    result = process_order(data)
    redis.set(request_id, result)
    return result

全局请求拦截器(成熟方案)

使用 axiosfetch 拦截器,自动检测和阻止重复请求。

Axios 请求去重(最推荐)

import axios from 'axios';
import md5 from 'md5';
// 存储正在进行的请求
const pendingRequests = new Map();
// 生成请求唯一标识
function generateRequestKey(config) {
  return md5(
    `${config.method}:${config.url}:${JSON.stringify(config.data || '')}`
  );
}
// 请求拦截器
axios.interceptors.request.use((config) => {
  const key = generateRequestKey(config);
  if (pendingRequests.has(key)) {
    // 取消重复请求
    const cancel = pendingRequests.get(key);
    cancel('Duplicate request cancelled');
    // 或者直接返回 null/抛出错误,不给用户重复发送
    return Promise.reject(new axios.Cancel('重复请求已被阻止'));
  }
  // 创建取消令牌
  const controller = new AbortController();
  config.signal = controller.signal;
  pendingRequests.set(key, controller.abort.bind(controller));
  return config;
});
// 响应拦截器
axios.interceptors.response.use(
  (response) => {
    const key = generateRequestKey(response.config);
    pendingRequests.delete(key);
    return response;
  },
  (error) => {
    if (axios.isCancel(error)) {
      console.warn('请求被取消:', error.message);
    }
    // 确保移除
    const config = error.config;
    if (config) {
      const key = generateRequestKey(config);
      pendingRequests.delete(key);
    }
    return Promise.reject(error);
  }
);
// 使用 axios 发起请求时,3 秒内有相同请求,会被自动取消
axios.post('/api/order', { productId: 123 });

封装成 React Hook

function useRequestDebounce() {
  const requestQueue = useRef(new Map());
  const debouncedRequest = useCallback(async (config) => {
    const key = `${config.method}:${config.url}:${JSON.stringify(config.data)}`;
    if (requestQueue.current.has(key)) {
      console.warn('重复请求已取消:', key);
      return;
    }
    const promise = axios(config).finally(() => {
      requestQueue.current.delete(key);
    });
    requestQueue.current.set(key, promise);
    return promise;
  }, []);
  return { debouncedRequest };
}
// 使用
const { debouncedRequest } = useRequestDebounce();
const handleSubmit = async () => {
  const res = await debouncedRequest({
    method: 'POST',
    url: '/api/order',
    data: { productId: 123 }
  });
};

Token 机制(幂等性方案)

原理:每次操作前从后端获取一个唯一 token,提交时携带该 token,后端验证 token 是否已使用。

// 1. 获取 token
const { token } = await api.getOperationToken();
// 2. 提交时携带 token
await api.submitOrder({
  idempotencyToken: token,
  data: { productId: 123 }
});
// 3. 后端验证
function submitOrder(req) {
  if redis.get(req.idempotencyToken) {
    return { code: 200, message: '已处理' };
  }
  redis.set(req.idempotencyToken, 'used');
  // 处理业务...
}

综合最佳实践(推荐组合)

对于关键业务(支付、下单),建议采用 三层防护

层级 方案 说明
前端 按钮状态锁 + 防抖 立即阻止用户重复点击
前端 Axios 请求去重 防止代码层面重复调用
后端 请求 ID 幂等 终极保障,防止网络重试导致的问题
// 最终代码示例
function handleSubmit() {
  if (this.loading) return; // 按钮锁 1
  this.loading = true; // 按钮锁 2
  this.throttle(async () => {
    try {
      // 请求携带 requestId(幂等)
      await api.submitOrder({
        requestId: generateUUID(),
        data: this.formData
      });
    } finally {
      this.loading = false;
    }
  }, 1000)();
}

注意事项

  1. 不要完全依赖前端:网络波动、页面刷新等场景下,最终兜底应在后端实现幂等
  2. 超时处理:长时间未收到响应应允许用户重试
  3. 用户体验:禁用按钮时要给出明确反馈(加载动画、提示文字)
  4. 异常恢复:如果请求失败,必须释放锁的状态,允许用户再次操作

根据你的场景复杂度,多数情况下选择 按钮状态锁 + 防抖 即可解决 80% 的问题。

抱歉,评论功能暂时关闭!