系列学习互联网安全架构第 2 篇 —— 防止表单重复提交方案(防止 API 接口幂等设计)

 

表单重复提交,是我们开发中经常遇到的一个问题。最简单的做法是让前端调用者去处理,但是一旦项目庞大,用户量增加的时候,或者网络延迟的情况,多次点击提交的情况,或者补偿重发接口等等,这种方式显然就不靠谱了。

防止 API 接口幂等设计

防止 API 接口幂等,就是为了解决 API 接口重复提交的问题。接下来模拟表单重复提交的场景。

 

先下载本次案例的脚手架代码:https://pan.baidu.com/s/1dLFNDaoGt60iRNne2vOXLQ  提取码:tygb

 

创建表:t_order

CREATE TABLE `t_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `product_name` varchar(255) DEFAULT NULL COMMENT '产品名称',
  `price` decimal(10,2) DEFAULT NULL COMMENT '价格',
  `order_no` varchar(64) DEFAULT NULL COMMENT '订单号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

脚手架代码主要按照常规的做法进行订单的提交。

启动服务,浏览器地址输入:http://127.0.0.1/

系列学习互联网安全架构第 2 篇 —— 防止表单重复提交方案(防止 API 接口幂等设计)_第1张图片

系列学习互联网安全架构第 2 篇 —— 防止表单重复提交方案(防止 API 接口幂等设计)_第2张图片

去数据库查看:

这是正常情况的提交。然后我们继续刷新页面。

系列学习互联网安全架构第 2 篇 —— 防止表单重复提交方案(防止 API 接口幂等设计)_第3张图片

又提交了一次表单,数据库查看:

系列学习互联网安全架构第 2 篇 —— 防止表单重复提交方案(防止 API 接口幂等设计)_第4张图片

如何解决表单重复提交?

解决思路:前端在提交表单前,先调用后台接口获取唯一的、临时的 token(后台生成 token,并存入缓存中,如 Redis,可以设置有效期为30分钟),拿到 token 并存入到头部信息 header,然后再提交表单。后台接收到表单提交的请求,先判断头部的 token 是否在缓存中,如果在缓存中,则继续处理业务逻辑(处理完业务后,必须删除掉缓存中的 token)。如果不存在缓存,说明无效或者重复提交。

OK,我们先创建生成 token 的工具类:

封装的 Redis 操作类:

package com.study.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * @author biandan
 * @description 封装的 Redis 操作类
 * @signature 让天下没有难写的代码
 * @create 2021-06-02 上午 11:46
 */
@Component
public class MyRedisTemplate {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //设置 String 对象
    public Boolean setString(String key,Object data,Long timeout){
        if(data instanceof String){
            if(null != timeout){
                stringRedisTemplate.opsForValue().set(key,(String)data,timeout, TimeUnit.SECONDS);
            }else{
                stringRedisTemplate.opsForValue().set(key,(String)data);
            }
            return true;
        }else{
            return false;
        }
    }

    //获取 String 对象
    public Object getString(String key){
        return stringRedisTemplate.opsForValue().get(key);
    }

    //删除某个 key
    public void delKey(String key){
        stringRedisTemplate.delete(key);
    }

}

token 工具类:

package com.study.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Objects;
import java.util.UUID;

/**
 * @author biandan
 * @description 临时 token 的工具类
 * @signature 让天下没有难写的代码
 * @create 2021-06-02 上午 11:44
 */
@Component
public class TokenUtil {

    @Autowired
    private MyRedisTemplate myRedisTemplate;

    private static final Long TIMEOUT = 60 * 30L;

    //生成 token
    public String getToken() {
        String token = "token_" + UUID.randomUUID().toString().replaceAll("-","");
        myRedisTemplate.setString(token, token, TIMEOUT);
        return token;
    }

    //判断是否有 token
    public Boolean findToken(String tokenKey) {
        if(Objects.nonNull(myRedisTemplate.getString(tokenKey))){
            String token = (String)myRedisTemplate.getString(tokenKey);
            if(!StringUtils.isEmpty(token)){
                return true;
            }
        }
        return false;
    }

    //删除某个 key
    public void deleteKey(String key){
        myRedisTemplate.delKey(key);
    }

}

修改 OrderController,增加 getToken 接口,增加从 header 获取 token 参数,以及提交订单成功后,删除 token。

package com.study.controller;

import com.study.entity.OrderEntity;
import com.study.service.OrderService;
import com.study.util.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

/**
 * @author biandan
 * @description
 * @signature 让天下没有难写的代码
 * @create 2021-06-01 下午 11:19
 */
@Controller
public class OrderController {

    private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyyMMddHHmmss");

    @Autowired
    private OrderService orderService;

    @Autowired
    private TokenUtil tokenUtil;

    //新增订单
    @RequestMapping(value = "/addOrder",method = RequestMethod.POST)
    public ModelAndView index(HttpServletRequest request) {
        ModelAndView view = new ModelAndView();
        String productName = request.getParameter("productName");
        Float price = Float.parseFloat(request.getParameter("price"));

        String token = request.getHeader("token");
        if(StringUtils.isEmpty(token) || !tokenUtil.findToken(token)){
            view.setViewName("fail");
            return view;
        }

        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setProductName(productName);
        orderEntity.setPrice(price);
        String orderNo = SDF.format(new Date())+ new Random().nextInt(1000);
        orderEntity.setOrderNo(orderNo);
        if(orderService.addOrder(orderEntity) > 0){
            view.setViewName("success");
            view.addObject("orderNo",orderNo);
            //删除 token
            tokenUtil.deleteKey(token);
        }else {
            view.setViewName("fail");
        }
        return view;
    }

    //获取token
    @RequestMapping(value = "/getToken",method = RequestMethod.GET)
    @ResponseBody
    public String getToken(){
        String token = tokenUtil.getToken();
        return token;
    }
}

重启服务,并启动 Redis,可以查看博客:https://blog.csdn.net/BiandanLoveyou/article/details/116422555

这次使用 postman 测试。

先获取 token

系列学习互联网安全架构第 2 篇 —— 防止表单重复提交方案(防止 API 接口幂等设计)_第5张图片

然后把 token 放到 header 中,请求新增订单接口。(注意把参数放到 Params 中)

系列学习互联网安全架构第 2 篇 —— 防止表单重复提交方案(防止 API 接口幂等设计)_第6张图片

如果我们点击再次提交,提示我们订单已经重复。

系列学习互联网安全架构第 2 篇 —— 防止表单重复提交方案(防止 API 接口幂等设计)_第7张图片

 

OK,解决表单重复提交的思路和方案大致为上面所说的。

但是有一个问题,如果每次请求都要判断是否有 token,代码就显得臃肿。我们在下一篇博客尝试着封装成一个注解,只需要在方法上增加注解,就可以自动判断 token 是否有效。

 

本篇博客代码:https://pan.baidu.com/s/1HkGWnhE_lsP3iZ2K5kMq3A   提取码:qs55

 

你可能感兴趣的:(互联网安全架构,防止表单重复提交,API,幂等性设计)