Springboot 2.x 中怎么避免重复提交

发布时间:2021-07-24 13:52:23 作者:Leah
来源:亿速云 阅读:263
# SpringBoot 2.x 中怎么避免重复提交

## 前言

在Web应用开发中,重复提交是一个常见但危害严重的问题。当用户因网络延迟、误操作或恶意行为导致同一请求被多次提交时,可能引发数据重复插入、订单重复创建、资源重复扣减等业务异常。SpringBoot 2.x作为当前主流的Java Web开发框架,提供了多种解决方案来应对这一挑战。本文将深入探讨6种实用方案,涵盖从基础到高级的完整防御体系。

## 一、重复提交的典型场景与危害

### 1.1 常见触发场景
- **网络延迟**:用户点击提交后未及时响应导致重复点击
- **页面回退**:提交成功后按浏览器返回按钮再次提交
- **恶意攻击**:使用工具自动化重复提交关键业务请求
- **程序缺陷**:前端未做防重复点击控制

### 1.2 业务危害分析
- **数据一致性破坏**:重复生成相同业务数据
- **资源异常消耗**:多次扣减库存/余额
- **系统性能下降**:无效请求占用服务器资源
- **用户体验受损**:重复提示或异常页面

## 二、前端基础防护方案

### 2.1 按钮状态控制(基础防御)
```javascript
// Vue示例
<template>
  <button @click="submit" :disabled="isSubmitting">
    {{ isSubmitting ? '提交中...' : '提交' }}
  </button>
</template>

<script>
export default {
  data() {
    return {
      isSubmitting: false
    }
  },
  methods: {
    async submit() {
      if(this.isSubmitting) return;
      
      this.isSubmitting = true;
      try {
        await axios.post('/api/submit', formData);
      } finally {
        this.isSubmitting = false;
      }
    }
  }
}
</script>

2.2 请求拦截器方案

// Axios全局配置
const pendingRequests = new Map();

axios.interceptors.request.use(config => {
  const requestKey = `${config.method}-${config.url}`;
  if (pendingRequests.has(requestKey)) {
    return Promise.reject(new Error('重复请求已阻止'));
  }
  pendingRequests.set(requestKey, true);
  return config;
});

axios.interceptors.response.use(response => {
  const requestKey = `${response.config.method}-${response.config.url}`;
  pendingRequests.delete(requestKey);
  return response;
});

三、服务端Token机制(推荐方案)

3.1 实现原理

  1. 服务端生成唯一Token并存储
  2. Token随表单返回前端
  3. 提交时携带Token进行验证
  4. 验证后立即失效Token

3.2 SpringBoot实现代码

Token生成端点

@RestController
public class TokenController {

    @GetMapping("/token")
    public String getToken(HttpSession session) {
        String token = UUID.randomUUID().toString();
        session.setAttribute("submit_token", token);
        return token;
    }
}

拦截器验证

public class TokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response,
                           Object handler) throws Exception {
        
        if ("POST".equalsIgnoreCase(request.getMethod())) {
            String clientToken = request.getParameter("submit_token");
            String serverToken = (String) request.getSession()
                                      .getAttribute("submit_token");
            
            if (serverToken == null || !serverToken.equals(clientToken)) {
                response.sendError(HttpStatus.BAD_REQUEST.value(), 
                                 "无效或重复的提交令牌");
                return false;
            }
            
            // 验证成功后立即清除
            request.getSession().removeAttribute("submit_token");
        }
        return true;
    }
}

注册拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenInterceptor())
                .addPathPatterns("/submit");
    }
}

四、分布式锁方案(集群环境)

4.1 Redisson实现方案

@RestController
public class OrderController {

    @Autowired
    private RedissonClient redissonClient;

    @PostMapping("/createOrder")
    public String createOrder(@RequestBody Order order, 
                            HttpServletRequest request) {
        
        String userId = getCurrentUserId();
        String lockKey = "order:submit:" + userId;
        
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁,等待3秒,锁定10秒自动释放
            if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
                return processOrder(order);
            } else {
                throw new RuntimeException("操作太频繁,请稍后重试");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("系统繁忙");
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

4.2 自定义注解+切面

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
    String key() default "";
    int expire() default 5; // 秒
}

@Aspect
@Component
public class DuplicateSubmitAspect {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Around("@annotation(prevent)")
    public Object checkDuplicate(ProceedingJoinPoint pjp, 
                               PreventDuplicateSubmit prevent) throws Throwable {
        
        String requestKey = buildRequestKey(pjp, prevent);
        
        if (redisTemplate.opsForValue().setIfAbsent(requestKey, "1")) {
            redisTemplate.expire(requestKey, prevent.expire(), TimeUnit.SECONDS);
            return pjp.proceed();
        } else {
            throw new RuntimeException("请勿重复提交");
        }
    }
    
    private String buildRequestKey(ProceedingJoinPoint pjp, 
                                 PreventDuplicateSubmit prevent) {
        // 构建基于用户+方法参数的唯一key
        return "submit:lock:" + 
               (prevent.key().isEmpty() ? 
                pjp.getSignature().toLongString() : prevent.key());
    }
}

五、数据库唯一约束(终极保障)

5.1 幂等表设计

CREATE TABLE `idempotent_record` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `business_type` varchar(32) NOT NULL COMMENT '业务类型',
  `business_key` varchar(64) NOT NULL COMMENT '业务唯一键',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_type_key` (`business_type`,`business_key`)
) ENGINE=InnoDB COMMENT='幂等记录表';

5.2 JPA实现示例

@Service
@Transactional
public class OrderService {

    @Autowired
    private IdempotentRecordRepository recordRepository;
    
    public Order createOrder(OrderDTO dto) {
        // 检查幂等记录
        if (recordRepository.existsByBusinessTypeAndBusinessKey(
            "order_create", dto.getOrderNo())) {
            throw new BusinessException("订单已存在");
        }
        
        // 业务处理
        Order order = convertToOrder(dto);
        orderRepository.save(order);
        
        // 记录幂等
        IdempotentRecord record = new IdempotentRecord();
        record.setBusinessType("order_create");
        record.setBusinessKey(dto.getOrderNo());
        recordRepository.save(record);
        
        return order;
    }
}

六、请求指纹校验(高级方案)

6.1 请求特征提取

public class RequestFingerprintUtil {

    public static String generate(HttpServletRequest request) {
        StringJoiner joiner = new StringJoiner("|");
        
        // 基础信息
        joiner.add(request.getMethod())
              .add(request.getRequestURI());
        
        // 用户标识
        String userId = getCurrentUserId();
        if (userId != null) {
            joiner.add(userId);
        }
        
        // 参数指纹
        Map<String, String[]> params = request.getParameterMap();
        params.entrySet().stream()
              .sorted(Map.Entry.comparingByKey())
              .forEach(e -> {
                  String value = Arrays.stream(e.getValue())
                                     .sorted()
                                     .collect(Collectors.joining(","));
                  joiner.add(e.getKey() + "=" + value);
              });
        
        // 请求体指纹(需缓存读取)
        String cachedBody = (String) request.getAttribute("cachedRequestBody");
        if (cachedBody != null) {
            joiner.add("body=" + DigestUtils.md5Hex(cachedBody));
        }
        
        return DigestUtils.md5Hex(joiner.toString());
    }
}

6.2 配合Guava Cache实现

@Configuration
public class CacheConfig {

    @Bean
    public Cache<String, Boolean> requestFingerprintCache() {
        return CacheBuilder.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .build();
    }
}

@RestControllerAdvice
public class DuplicateSubmitAdvice {

    @Autowired
    private Cache<String, Boolean> fingerprintCache;

    @ModelAttribute
    public void checkFingerprint(HttpServletRequest request) {
        if ("POST".equalsIgnoreCase(request.getMethod())) {
            String fingerprint = RequestFingerprintUtil.generate(request);
            if (fingerprintCache.getIfPresent(fingerprint) != null) {
                throw new DuplicateSubmitException();
            }
            fingerprintCache.put(fingerprint, true);
        }
    }
}

七、方案对比与选型建议

方案 适用场景 优点 缺点
前端控制 简单表单提交 实现简单 可绕过,防护弱
Token机制 常规Web应用 可靠性高,用户体验好 需前后端配合
分布式锁 高并发分布式系统 强一致性保证 性能开销较大
数据库唯一约束 核心交易场景 绝对可靠 增加数据库压力
请求指纹 RESTful API接口 灵活精准 实现复杂度高

选型建议: 1. 对于管理后台等系统:Token机制+前端控制 2. 电商交易系统:分布式锁+数据库唯一约束 3. 开放API平台:请求指纹+限流机制

结语

在SpringBoot 2.x项目中构建完整的防重复提交体系,需要根据实际业务场景组合多种方案。建议从以下维度进行设计: 1. 防御层次:前端防护+服务端校验+持久层保障 2. 业务关键性:非核心业务可采用轻量级方案,核心业务需强校验 3. 系统架构:单体应用与分布式系统选择不同技术方案

通过合理的方案选择和组合实现,可以有效避免重复提交带来的各类业务问题,提升系统健壮性和用户体验。 “`

推荐阅读:
  1. SpringBoot 2 要升级吗?
  2. 如何在springboot中操作静态资源文件

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

springboot

上一篇:PHP代码重构方法的示例分析

下一篇:vim中四种模式及模式切换的示例分析

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》