API接口幂等性框架设计

时间:2023-12-16 17:39:08

表单重复提价问题

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获取参数 进行校验