表单重复提价问题

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>

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); //key: token value: token 时间
return token;
} public synchronize boolean findToken(String tokenKey) { //从redis查询对应的token 防止没来得及删除 只有一个线程操作 其实redis已经可以防止了
String token = (String) baseRedisService.getString(tokenKey);
if (StringUtils.isEmpty(token)) { //要么被被人使用过了 要么没有对应token
return false;
}
// token 获取成功后 删除对应tokenMapstoken
baseRedisService.delKey(token);
return true; //保证每个接口对应的token只能访问一次,保证接口幂等性问题
} }

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: meitedu.+@
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获取参数 进行校验

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

  1. 防盗链&CSRF&API接口幂等性设计

    防盗链技术 CSRF(模拟请求) 分析防止伪造Token请求攻击 互联网API接口幂等性设计 忘记密码漏洞分析 1.Http请求防盗链 什么是防盗链 比如A网站有一张图片,被B网站直接通过img标签属 ...

  2. Python3简易接口自动化测试框架设计与实现(中)

    目录 7.Excel数据读取 7.1.读取配置文件 7.1.编写Excel操作类 8.用例组装 9.用例运行结果校验 10.运行用例 11 .小结 上一篇:Python3简易接口自动化测试框架设计与实 ...

  3. Api接口幂等设计

    1,Api接口幂等设计,也就是要保证数据的唯一性,不允许有重复. 例如:rpc 远程调用,因为网络延迟,出现了调用了2次的情况. 表单连续点击,出现了重复提交. 接口暴露之后,会被模拟请求工具(Jem ...

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

    现在,越来越多的 Web 应用转向了 RESTful 的架构,很多产品和应用暴露给用户的往往就是一组 REST API,这样有一个好处,用户可以根据需要,调用不同的 API,整合出自己的应用出来.从这 ...

  5. Python3简易接口自动化测试框架设计与实现(上)

    目录 1.开发环境 2.用到的模块 3.框架设计 3.1.流程 3.2.项目结构 5.日志打印 6.接口请求类封装 接口开发请参考:使用Django开发简单接口:文章增删改查 1.开发环境 操作系统: ...

  6. 多测师讲解 _接口自动化框架设计_高级讲师肖sir

    背景:因为把传入接口参数.组建测试用例.执行测试用例和发送报告,都放入一个.py文件对于接口的使用非常不灵活就需要数据和接口业务进行分离让代码之间的 耦合性降低.和实现接口的分层管理,所以需要对代码进 ...

  7. API接口幂等性设计

    目录 幂等性场景 解决方案 幂等性场景 网络延迟导致多次重复提交. 表单重复提交. 解决方案 每次提交都使用一个Token,Token保证临时且唯一即可 token生成规则(单机应用):token+U ...

  8. python 做接口自动化测试框架设计

    1,明确什么叫自动化测试,什么叫接口自动化测试,如何设计接口测试用例,已登录为例 自动化测试:解放人力来自动完成规定的测试. 自动化测试分层模型:UI层,不论WEB端还是移动端,都是基于页面元素的识别 ...

  9. Python Api接口自动化测试框架 excel篇

    工作原理: 测试用例在excel上编辑,使用第三方库xlrd,读取表格sheet和内容,sheetName对应模块名,Jenkins集成服务发现服务moduleName查找对应表单,运用第三方库req ...

随机推荐

  1. BF算法 + KMP算法

    准备: 字符串比大小:比的就是字符串里每个字符的ASCII码的大小.(其实这样的比较没有多大的意义,我们关心的是字符串是否相等,即匹配等) 字符串的存储结构:同线性表(顺序存储+链式存储) 顺序存储结 ...

  2. mysql连接远程数据库的用法

    mysql -u root -p 等这些常用的参数外,你知道多少?来测试一下吧 一,mysql命令行参数 Usage: mysql [OPTIONS] [database]   //命令方式 -?,  ...

  3. 【转】 JS实现HTML标签转义及反转义

    原文地址:http://blog.600km.xyz/2015/12/15/js-encode-html-tags/ 简单说一下业务场景,前台用户通过input输入内容,在离开焦点时,将内容在div中 ...

  4. 170223、Tomcat部署时war和war exploded区别以及平时踩得坑

    war和war exploded的区别 在使用IDEA开发项目的时候,部署Tomcat的时候通常会出现下边的情况: 是选择war还是war exploded 这里首先看一下他们两个的区别: war模式 ...

  5. Powershell Exchange Server UP Time

    Server up time Get-ExchangeServer | where{$_.name -like'wendy*'} | %{ if(Test-Connection $_.name -Co ...

  6. Android REST webservice 类

    App与后台交互,后台使用的是Jersey RESTful 服务.在APP端使用Android 内部集成的HttpClient接口,无需引入第三方jar包, import org.apache.htt ...

  7. String trim 坑 对于ascii码为160的去不掉

    大家在使用string   的trim去除空格的时候,要注意一个坑呀,对于ascii码为160的去不掉 import java.util.Arrays; /** * Created by bjchen ...

  8. django中的setting全局变量的导入

    需求:在py文件中导入settings.py中的变量BASE_DIR settings.py文件 import os # Build paths inside the project like thi ...

  9. Keras之函数式(Functional)模型

    函数式(Functional)模型 我们起初将Functional一词译作泛型,想要表达该类模型能够表达任意张量映射的含义,但表达的不是很精确,在Keras2里我们将这个词改移为“函数式”,函数式模型 ...

  10. Nordic Blue Tooth

    一 . nordic BLE4.0 1.开发nordic的应用需要安装支持keil的pack库和插件 2.nordic的SDK很完整,实例涵盖了几乎所有的应用 https://www.nordicse ...