API接口幂等性框架设计

表单重复提价问题

rpc远程调用时候 发生网络延迟  可能有重试机制

MQ消费者幂等(保证唯一)一样

解决方案: token

令牌 保证唯一的并且是临时的  过一段时间失效

分布式: redis+token

注意在getToken() 这种方法代码一定要上锁  保证只有一个线程执行  否则会造成token不唯一

步骤 调用接口之前生成对应的 token,存放在redis中

调用接口的时候,将该令牌放到请求头中 (获取请求头中的令牌)

接口获取对应的令牌,如果能够获取该令牌 (将当前令牌删除掉),执行该方法业务逻辑

如果获取不到对应的令牌。返回提示“老铁 不要重复提交”

哈哈 如果别人获得了你的token 然后拿去做坏事,采用机器模拟去攻击。这时候我们要用验证码来搞定。

从代码开发者的角度看,如果每次请求都要 获取token  然后进行一统校验。代码冗余啊。如果一百个接口 要写一百次

所以采用AOP的方式进行开发,通过注解方式。

如果过滤器的话,所有接口都进行了校验。

框架开发:

自定义一个注解@  作为标记

如果哪个Controller需要进行token的验证加上注解标记

在执行代码时候AOP通过切面类中 写的 作用接口进行 判断,如果这个接口方法有 自定义的@注解  那么进行校验逻辑

校验结果 要么提示给用户 “请勿提交” 要么通过验证 继续往下执行代码

关于表单重复提交:

在表单有个隐藏域 存放token  使用  getParameter 去获取token 然后通过返回的结果进行校验

注意 获取token的这个代码 也是用AOP去解决,实现。 否则每个Controller类都写这段代码就冗余了。前置通知搞定

注解:

首先pom:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.0.RELEASE</version>
    </parent>
    <dependencies>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!-- mysql 依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- SpringBoot 对lombok 支持 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- SpringBoot web 核心组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>
        <!-- SpringBoot 外部tomcat支持 -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>

        <!-- springboot-log4j -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j</artifactId>
            <version>1.3.8.RELEASE</version>
        </dependency>
        <!-- springboot-aop 技术 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-lang/commons-lang -->
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>
        <dependency>
            <groupId>taglibs</groupId>
            <artifactId>standard</artifactId>
            <version>1.1.2</version>
        </dependency>
    </dependencies>

1、关于Header的token的注解封装

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiIdempotent {
    String value();
}

2、关于表单提交的注解的封装

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiIdempotent {
    String value();
}

AOP:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.itmayeidu.ext.ExtApiIdempotent;
import com.itmayeidu.ext.ExtApiToken;
import com.itmayeidu.utils.ConstantUtils;
import com.itmayeidu.utils.RedisTokenUtils;
import com.itmayeidu.utils.TokenUtils;

@Aspect
@Component
public class ExtApiAopIdempotent {
    @Autowired
    private RedisTokenUtils redisTokenUtils;

    //需要作用的类
    @Pointcut("execution(public * com.itmayiedu.controller.*.*(..))")
    public void rlAop() {
    }

    // 前置通知转发Token参数  进行拦截的逻辑
    @Before("rlAop()")
    public void before(JoinPoint point) {
        //获取并判断类上是否有注解
        MethodSignature signature = (MethodSignature) point.getSignature();//统一的返回值
        ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class);//参数是注解的那个
        if (extApiToken != null) { //如果有注解的情况
            extApiToken();
        }
    }

    // 环绕通知验证参数
    @Around("rlAop()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
        if (extApiIdempotent != null) { //有注解的情况 有注解的说明需要进行token校验
            return extApiIdempotent(proceedingJoinPoint, signature);
        }
        // 放行
        Object proceed = proceedingJoinPoint.proceed(); //放行 正常执行后面(Controller)的业务逻辑
        return proceed;
    }

    // 验证Token  方法的封装
    public Object extApiIdempotent(ProceedingJoinPoint proceedingJoinPoint, MethodSignature signature)
            throws Throwable {
        ExtApiIdempotent extApiIdempotent = signature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
        if (extApiIdempotent == null) {
            // 直接执行程序
            Object proceed = proceedingJoinPoint.proceed();
            return proceed;
        }
        // 代码步骤:
        // 1.获取令牌 存放在请求头中
        HttpServletRequest request = getRequest();
        // value就是获取类型 请求头之类的
        String valueType = extApiIdempotent.value();
        if (StringUtils.isEmpty(valueType)) {
            response("参数错误!");
            return null;
        }
        String token = null;
        if (valueType.equals(ConstantUtils.EXTAPIHEAD)) { //如果存在header中 从头中获取
            token = request.getHeader("token"); //从头中获取
        } else {
            token = request.getParameter("token"); //否则从 请求参数获取
        }
        if (StringUtils.isEmpty(token)) {
            response("参数错误!");
            return null;
        }
        if (!redisTokenUtils.findToken(token)) {
            response("请勿重复提交!");
            return null;
        }
        Object proceed = proceedingJoinPoint.proceed();
        return proceed;
    }

    public void extApiToken() {
        String token = redisTokenUtils.getToken();
        getRequest().setAttribute("token", token);

    }

    public HttpServletRequest getRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        return request;
    }

    public void response(String msg) throws IOException {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = attributes.getResponse();
        response.setHeader("Content-type", "text/html;charset=UTF-8");
        PrintWriter writer = response.getWriter();
        try {
            writer.println(msg);
        } catch (Exception e) {

        } finally {
            writer.close();
        }

    }

}

订单请求接口:

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.itmayeidu.ext.ExtApiIdempotent;
import com.itmayeidu.utils.ConstantUtils;
import com.itmayeidu.utils.RedisTokenUtils;
import com.itmayeidu.utils.TokenUtils;
import com.itmayiedu.entity.OrderEntity;
import com.itmayiedu.mapper.OrderMapper;

@RestController
public class OrderController {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RedisTokenUtils redisTokenUtils;

    // 从redis中获取Token
    @RequestMapping("/redisToken")
    public String RedisToken() {
        return redisTokenUtils.getToken();
    }

    // 验证Token
    @RequestMapping(value = "/addOrderExtApiIdempotent", produces = "application/json; charset=utf-8")
    @ExtApiIdempotent(value = ConstantUtils.EXTAPIHEAD)
    public String addOrderExtApiIdempotent(@RequestBody OrderEntity orderEntity, HttpServletRequest request) {
        int result = orderMapper.addOrder(orderEntity);
        return result > 0 ? "添加成功" : "添加失败" + "";
    }
}

表单提交的请求接口:

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.itmayeidu.ext.ExtApiIdempotent;
import com.itmayeidu.ext.ExtApiToken;
import com.itmayeidu.utils.ConstantUtils;
import com.itmayiedu.entity.OrderEntity;
import com.itmayiedu.mapper.OrderMapper;

@Controller
public class OrderPageController {
    @Autowired
    private OrderMapper orderMapper;

    @RequestMapping("/indexPage")
    @ExtApiToken
    public String indexPage(HttpServletRequest req) {
        return "indexPage";
    }

    @RequestMapping("/addOrderPage")
    @ExtApiIdempotent(value = ConstantUtils.EXTAPIFROM)
    public String addOrder(OrderEntity orderEntity) {
        int addOrder = orderMapper.addOrder(orderEntity);
        return addOrder > 0 ? "success" : "fail";
    }

}

utils:

redis:

import java.util.concurrent.TimeUnit;

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

@Component
public class BaseRedisService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public void setString(String key, Object data, Long timeout) {
        if (data instanceof String) {
            String value = (String) data;
            stringRedisTemplate.opsForValue().set(key, value);
        }
        if (timeout != null) {
            stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
    }

    public Object getString(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    public void delKey(String key) {
        stringRedisTemplate.delete(key);
    }

}

常量:

public interface ConstantUtils {

    static final String EXTAPIHEAD = "head";

    static final String EXTAPIFROM = "from";
}

mvc:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

@Configuration
@EnableWebMvc
@ComponentScan("com.too5.controller")
public class MyMvcConfig {
    @Bean // 出现问题原因 @bean 忘记添加
    public InternalResourceViewResolver viewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
        viewResolver.setViewClass(JstlView.class);
        return viewResolver;
    }

}

redis操作token工具类:

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class RedisTokenUtils {
    private long timeout = 60 * 60;
    @Autowired
    private BaseRedisService baseRedisService;

    // 将token存入在redis
    public String getToken() {
        String token = "token" + System.currentTimeMillis();
        baseRedisService.setString(token, token, timeout);
        return token;
    }

    public boolean findToken(String tokenKey) {
        String token = (String) baseRedisService.getString(tokenKey);
        if (StringUtils.isEmpty(token)) {
            return false;
        }
        // token 获取成功后 删除对应tokenMapstoken
        baseRedisService.delKey(token);
        return true;
    }

}

tokenutils:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang.StringUtils;

public class TokenUtils {

    private static Map<String, Object> tokenMaps = new ConcurrentHashMap<String, Object>();
    // 1.什么Token(令牌) 表示是一个零时不允许有重复相同的值(临时且唯一)
    // 2.使用令牌方式防止Token重复提交。

    // 使用场景:在调用第API接口的时候,需要传递令牌,该Api接口 获取到令牌之后,执行当前业务逻辑,让后把当前的令牌删除掉。
    // 在调用第API接口的时候,需要传递令牌 建议15-2小时
    // 代码步骤:
    // 1.获取令牌
    // 2.判断令牌是否在缓存中有对应的数据
    // 3.如何缓存没有该令牌的话,直接报错(请勿重复提交)
    // 4.如何缓存有该令牌的话,直接执行该业务逻辑
    // 5.执行完业务逻辑之后,直接删除该令牌。

    // 获取令牌
    public static synchronized String getToken() {
        // 如何在分布式场景下使用分布式全局ID实现
        String token = "token" + System.currentTimeMillis();
        // hashMap好处可以附带
        tokenMaps.put(token, token);
        return token;
    }

    // generateToken();

    public static boolean findToken(String tokenKey) {
        // 判断该令牌是否在tokenMap 是否存在
        String token = (String) tokenMaps.get(tokenKey);
        if (StringUtils.isEmpty(token)) {
            return false;
        }
        // token 获取成功后 删除对应tokenMapstoken
        tokenMaps.remove(token);
        return true;
    }
}

实体类:

public class OrderEntity {

    private int id;
    private String orderName;
    private String orderDes;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getOrderName() {
        return orderName;
    }

    public void setOrderName(String orderName) {
        this.orderName = orderName;
    }

    public String getOrderDes() {
        return orderDes;
    }

    public void setOrderDes(String orderDes) {
        this.orderDes = orderDes;
    }

}
public class UserEntity {

    private Long id;
    private String userName;
    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "UserEntity [id=" + id + ", userName=" + userName + ", password=" + password + "]";
    }

}

Mapper:

import org.apache.ibatis.annotations.Insert;

import com.itmayiedu.entity.OrderEntity;

public interface OrderMapper {
    @Insert("insert order_info values (null,#{orderName},#{orderDes})")
    public int addOrder(OrderEntity OrderEntity);
}
public interface UserMapper {

    @Select(" SELECT  * FROM user_info where userName=#{userName} and password=#{password}")
    public UserEntity login(UserEntity userEntity);

    @Insert("insert user_info values (null,#{userName},#{password})")
    public int insertUser(UserEntity userEntity);
}

yml:

spring:
  mvc:
    view:
      # 页面默认前缀目录
      prefix: /WEB-INF/jsp/
      # 响应页面默认后缀
      suffix: .jsp

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
    test-while-idle: true
    test-on-borrow: true
    validation-query: SELECT 1 FROM DUAL
    time-between-eviction-runs-millis: 300000
    min-evictable-idle-time-millis: 1800000
  redis:
    database: 1
    host: 106.15.185.133
    port: 6379
    password: [email protected]
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0
    timeout: 10000
domain:
 name: www.toov5.com
 

启动类:

@MapperScan(basePackages = { "com.tov5.mapper" })
@SpringBootApplication
@ServletComponentScan
public class AppB {

    public static void main(String[] args) {
        SpringApplication.run(AppB.class, args);
    }

}

总结:

核心就是

自定义注解

controller中的方法注解

aop切面类判断对象是否有相应的注解 如果有 从parameter或者header获取参数 进行校验

原文地址:https://www.cnblogs.com/toov5/p/10312458.html

时间: 2024-07-31 15:25:28

API接口幂等性框架设计的相关文章

REST API 自动化测试 利器Rest Assured(API接口自动化测试框架体系)

现在,越来越多的 Web 应用转向了 RESTful 的架构,很多产品和应用暴露给用户的往往就是一组 REST API,这样有一个好处,用户可以根据需要,调用不同的 API,整合出自己的应用出来.从这个角度来讲,Web 开发的成本会越来越低,人们不必再维护自己的信息孤岛,而是使用 REST API 互联互通 那么,作为 REST API 的提供者,如何确保 API 的稳定性与正确性呢?全面系统的测试是必不可少的.Java 程序员常常借助于 JUnit 来测试自己的 REST API,不,应该这样

Python3简易接口自动化测试框架设计与实现 实例2

目录 1.开发环境 2.用到的模块 3.框架设计 ?3.1.流程 3.2.项目结构 5.日志打印 6.接口请求类封装 7.Excel数据读取 7.1.读取配置文件 7.1.编写Excel操作类 8.用例组装 9.用例运行结果校验 10.运行用例 11 .小结 1.开发环境 操作系统:Ubuntu18 开发工具:IDEA+PyCharm插件 Python版本:3.6 2.用到的模块 requests:用于发送请求 xlrd:操作Excel,组织测试用例 smtplib,email:发送测试报告 l

Python Api接口自动化测试框架 代码写用例

公司新来两个妹子一直吐槽这个接口测试用例用excel维护起来十分费脑费事,而且比较low(内心十分赞同但是不能推翻自己),妹子说excel本来就很麻烦的工具,于是偷偷的进行了二次改版. 变更内容如下: 1.代码结构 image.png 2.新增测试报告网页版和版本管理 3.新增用例代码化 一.封装一个获取用例的模块 image.png 用例的写法可以按照yml文件的写法,后缀的文件都可为.conf..config..ini.[]中的是测试用例场景,下面的参数内容对应接口用例参数. 简单介绍下py

从接口自动化测试框架设计到开发(五)--case运行结果统计、发送邮件

1.case运行结果统计 #Run_Test.py# -*- coding: utf-8 -*- # @Author: jiujiu # @Date: 2020-03-04 16:30:31 # @Last Modified time: 2020-03-07 10:05:52 import sys sys.path.append("G:/uni_test") #添加当前过程的目录 import json from base.run_method import RunMethod fro

微信小程序的Web API接口设计及常见接口实现

微信小程序给我们提供了一个很好的开发平台,可以用于展现各种数据和实现丰富的功能,通过小程序的请求Web API 平台获取JSON数据后,可以在小程序界面上进行数据的动态展示.在数据的关键 一环中,我们设计和编写Web API平台是非常重要的,通过这个我们可以实现数据的集中控制和管理,本篇随笔介绍基于Asp.NET MVC的Web API接口层的设计和常见接口代码的展示,以便展示我们常规Web API接口层的接口代码设计.参数的处理等内容. 1.Web API整体性的架构设计 我们整体性的架构设计

Python接口自动化测试框架实战 从设计到开发

第1章 课程介绍(不要错过)本章主要讲解课程的详细安排.课程学习要求.课程面向用户等,让大家很直观的对课程有整体认知! 第2章 接口测试工具Fiddler的运用本章重点讲解如何抓app\web的http\https请求包.如何模拟请求数据.过滤规则及修改响应数据.如何解决无法抓包问题        以及fiddler接口测试,让大家能应用好工具! 第3章 Requests常见方法实战运用本章重点讲解 get\post请求测试.接口测试中结果处理.上传\下载文件接口测试.请求中header及coo

php后台对接ios,安卓,API接口设计和实践完全攻略,涨薪必备技能

2016年12月29日13:45:27 关于接口设计要说的东西很多,可能写一个系列都可以,vsd图都得画很多张,但是由于个人时间和精力有限,所有有些东西后面再补充 说道接口设计第一反应就是restful api 请明白一点,这个只是设计指导思想,也就是设计风格 ,比如你需要遵循这些原则 原则条件REST 指的是一组架构约束条件和原则.满足这些约束条件和原则的应用程序或设计就是 RESTful.Web 应用程序最重要的 REST 原则是,客户端和服务器之间的交互在请求之间是无状态的.从客户端到服务

整合微信小程序的Web API接口层的架构设计

在我前面有很多篇随笔介绍了Web API 接口层的架构设计,以及对微信公众号.企业号.小程序等模块的分类划分.例如在<C#开发微信门户及应用(43)--微信各个项目模块的定义和相互关系>介绍了相关模块的划分,在<基于微信小程序的系统开发准备工作>介绍了Web API的架构设计思路.本篇随笔对之前介绍的架构内容进行统一的调整更新,以便更加方便实际项目的应用开发,以期达到统一.重用.清晰的目的. 1.公众号.企业号.小程序模块的划分 我们知道,目前微信企业应用,分为公众号.企业号(企业

Web API接口设计(学习)

1.在接口定义中确定MVC的GET或者POST方式 由于我们整个Web API平台是基于MVC的基础上进行的API开发,因此整个Web API的接口,在定义的时候,一般需要显示来声明接口是[HttpGet]或者[HttpPost],虽然有些接口也可以不用声明,但是避免出现类似下面的错误信息,显式声明还是有好处的. 请求的资源不支持 http 方法“POST 例如在基类定义的查找对象接口如下所示. /// <summary> /// 查询数据库,检查是否存在指定ID的对象 /// </su