有效防止重復提交的六個方法

重復提交是開發中經常遇到的問題。用戶可能因為頁面響應慢連續點擊提交按鈕,或者網絡延遲時反復重試。這些情況會導致數據重復、業務混亂,比如生成重復訂單、多次扣款等問題。
防止重復提交需要前后端配合。前端主要提升用戶體驗,后端才是真正的保障。
前端防止重復提交
前端方法能防止用戶誤操作,但不能完全依賴,因為可以通過工具繞過前端驗證。
1. 按鈕禁用
提交后立即禁用按鈕,這是最直接的方法。
functionsubmitForm() {
const btn = document.getElementById('submitBtn');
// 如果按鈕已禁用,直接返回
if (btn.disabled) return;
// 禁用按鈕并改變文字
btn.disabled = true;
btn.textContent = '提交中...';
// 發送請求
fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
// 處理響應
})
.catch(error => {
console.error('Error:', error);
})
.finally(() => {
// 無論成功失敗,都重新啟用按鈕
btn.disabled = false;
btn.textContent = '提交';
});
}2. 防抖函數
控制按鈕在指定時間內只能點擊一次。
functiondebounce(func, wait) {
let timeout;
returnfunction() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// 使用示例
const submitForm = debounce(function() {
// 實際的提交邏輯
}, 1000); // 1秒內只能點擊一次3. 請求攔截
記錄正在處理的請求,發現重復請求直接攔截。
classRequestManager{
constructor() {
this.pendingRequests = newMap();
}
generateKey(config) {
return`${config.method}-${config.url}-${JSON.stringify(config.data)}`;
}
addRequest(config) {
const key = this.generateKey(config);
if (this.pendingRequests.has(key)) {
returnfalse;
}
this.pendingRequests.set(key, true);
returntrue;
}
removeRequest(config) {
const key = this.generateKey(config);
this.pendingRequests.delete(key);
}
}
// 在axios攔截器中使用
const requestManager = new RequestManager();
axios.interceptors.request.use(config => {
if (!requestManager.addRequest(config)) {
returnPromise.reject(newError('請求已處理中'));
}
return config;
});
axios.interceptors.response.use(response => {
requestManager.removeRequest(response.config);
return response;
}, error => {
if (error.config) {
requestManager.removeRequest(error.config);
}
returnPromise.reject(error);
});前端方法的優點是提升用戶體驗,缺點是可以被繞過。因此后端驗證是必須的。
后端防止重復提交
4. Token令牌機制
這是比較傳統但有效的方法,適合表單提交場景。
工作流程:
- 用戶訪問頁面時,后端生成唯一Token
- Token隨頁面返回給前端
- 提交表單時攜帶Token
- 后端驗證Token有效性
- 驗證成功后立即刪除Token
Java實現示例:
@Component
publicclassTokenService{
// 生成Token
publicString createToken(HttpServletRequest request) {
String token = UUID.randomUUID().toString();
// 存儲到Session中
request.getSession().setAttribute("FORM_TOKEN", token);
return token;
}
// 驗證Token
publicboolean verifyToken(HttpServletRequest request) {
String clientToken = request.getParameter("token");
if (clientToken == null) {
returnfalse;
}
HttpSession session = request.getSession();
String serverToken = (String) session.getAttribute("FORM_TOKEN");
if (serverToken == null || !serverToken.equals(clientToken)) {
returnfalse;
}
// 驗證成功后立即刪除
session.removeAttribute("FORM_TOKEN");
returntrue;
}
}前端表單:
<formaction="/submit"method="post">
<inputtype="hidden"name="token"value="${token}">
<!-- 其他表單字段 -->
<buttontype="submit">提交</button>
</form>5. AOP + Redis方案
適合分布式系統,利用Redis實現分布式鎖。
定義注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public@interface PreventDuplicate {
intexpire()default 5; // 鎖定時間,默認5秒
String key()default ""; // 自定義鎖key
}實現切面:
@Aspect
@Component
publicclassDuplicateSubmitAspect{
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(preventDuplicate)")
publicObject checkDuplicate(ProceedingJoinPoint joinPoint,
PreventDuplicate preventDuplicate) throws Throwable {
HttpServletRequest request = getRequest();
String lockKey = buildLockKey(request, preventDuplicate);
int expireTime = preventDuplicate.expire();
// 嘗試加鎖
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(expireTime));
if (!success) {
thrownewRuntimeException("請勿重復提交");
}
try {
return joinPoint.proceed(); // 執行原方法
} finally {
// 根據業務需求決定是否立即刪除鎖
// redisTemplate.delete(lockKey);
}
}
privateString buildLockKey(HttpServletRequest request,
PreventDuplicate preventDuplicate) {
String userId = getUserId(request); // 獲取用戶ID
String uri = request.getRequestURI();
String params = request.getQueryString() != null ?
request.getQueryString() : "";
return"submit:lock:" + userId + ":" + uri + ":" +
DigestUtils.md5DigestAsHex(params.getBytes());
}
private HttpServletRequest getRequest() {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) attributes;
return sra.getRequest();
}
}使用方式:
@PostMapping("/order/create")
@PreventDuplicate(expire = 10)
public Result createOrder(@RequestBody OrderDTO order) {
// 業務邏輯
returnResult.success("訂單創建成功");
}6. 數據庫唯一約束
對于有唯一性要求的業務,可以在數據庫層面保障。
例如訂單表:
CREATETABLE orders (
idBIGINT PRIMARY KEY,
order_no VARCHAR(64) UNIQUE, -- 訂單號唯一約束
user_id BIGINT,
amount DECIMAL(10,2),
create_time DATETIME
);在業務代碼中處理:
@Service
publicclassOrderService{
public Result createOrder(OrderDTO order) {
try {
// 嘗試插入訂單
orderMapper.insert(order);
return Result.success("創建成功");
} catch (DuplicateKeyException e) {
// 捕獲唯一約束異常
log.warn("重復訂單: {}", order.getOrderNo());
return Result.error("訂單已存在");
}
}
}方案對比
方案 | 適用場景 | 優點 | 缺點 |
按鈕禁用 | 所有前端表單 | 用戶體驗好 | 可被繞過 |
Token機制 | 表單提交 | 安全可靠 | 分布式環境需要共享Session |
AOP+Redis | 分布式系統 | 無侵入,靈活 | 依賴Redis |
數據庫約束 | 有唯一性要求 | 絕對可靠 | 只能防止最終重復 |
實踐建議
- 前后端結合使用:前端防止誤操作,后端保障數據安全
- 合理設置超時時間:一般5-10秒足夠,避免影響正常操作
- 友好提示用戶:不要直接報錯,提示"操作進行中"或"請勿重復提交"
- 記錄重復提交:監控重復提交情況,幫助優化系統
- 考慮冪等性:重要業務要實現冪等接口
防止重復提交是系統穩定性的基礎保障。選擇方案時要根據實際業務需求,有時候需要多種方案組合使用,才能達到最好的效果。
最重要的是,不要完全依賴前端的防護,后端必須要有相應的驗證機制。這樣才能確保系統的數據安全和業務穩定。





























