表单重复提价问题

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. manacher算法处理最长的回文子串(二)

    在上篇<manacher算法处理最长的回文子串(一)>解释了manacher算法的原理,接着给该算法,该程序在leetcode的最长回文子串中通过.首先manacher算法维护3个变量.一 ...

  2. Web的本质以及第一个Django实例.

       Web框架的本质:    所有的Web应用本质上就是一个socket服务器, 而用户的浏览器就是一个socket客户端. import socket sk = socket.socket() s ...

  3. git GUI 入门

    一:安装一个git 及gui 二:配置gui及线上的git链接 在Git Gui中,选择Remote->add添加远程服务器,远程服务器信息有两种填写方式,填写https地址或ssh地址,对应g ...

  4. 【BZOJ4896】[Thu Summer Camp2016]补退选 Trie树

    [BZOJ4896][Thu Summer Camp2016]补退选 Description X是T大的一名老师,每年他都要教授许多学生基础的C++知识.在T大,每个学生在每学期的开学前都需要选课,每 ...

  5. iOS Search bar 输入空字符串也可以搜索

    Search bar delegate - (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar { UITextField *sea ...

  6. Windows(7)上那些好用的软件及优化技巧(原创)

    *本文为原创内容,转载请注明作者和出处:www.cnblogs.com/wang1024 软件篇 注:以下软件在百度直接搜索软件名均可找到官网,直接官网下载即可 大众的软件哪个好: 杀毒软件专题 基于 ...

  7. 【Python之路】第十二篇--JavaScript

    JavaScript 历史 1992年Nombas开发出C-minus-minus(C--)的嵌入式脚本语言(最初绑定在CEnvi软件中).后将其改名ScriptEase.(客户端执行的语言) Net ...

  8. Java RTTI and Reflection

    Reference: Java编程思想 java 反射(Reflect) Java系列笔记(2) - Java RTTI和反射机制 Java Reflection in Action, 有空再补 -- ...

  9. Hidden String---hdu5311(字符串处理)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5311 题意:从给出的串 s 中找到3个子串然后把他们连在一起问是否能够成anniversary #in ...

  10. 《COM本质论》COM是一个更好的C++心得分享

    昨天看了<COM本质论>的第一章"COM是一个更好的C++",认为非常有必要做一些笔记,于是整理成这篇文章.我相信你值得拥有. 这篇文章主要讲的内容是:一个实现了高速查 ...