互联网API接口幂等设计

等性概念:保证唯一的意思  如何防止接口不能重复提交===保证接口幂等性

接口幂等产生原因:1.rpc调用时网络延迟(重试发送请求) 2.表单重复提交

解决思路:redis+token,使用Tonken令牌,保证临时且唯一,将token放入redis中,并设置过期时间

如何使用Token 解决幂等性,步骤:
1.在调接口之前生成对应的令牌(Token),存放在Redis
2.调用接口的时候,将该令牌放入请求头中 | 表单隐藏域中
3.接口获取对应的令牌,如果能够获取该令牌(将当前令牌删除掉)就直接执行该访问的业务逻辑
4.接口获取对应的令牌,如果获取不到该令牌,直接返回请勿重复提交

代码部分,使用AOP自定义注解方式对Token进行验证. 防止表单重复提交中,使用AOP注解方式生成Token
1.rpc调用时网络延迟(重试发送请求)
pom.xml

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 引入redis的依赖包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.28</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.36</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>

</dependencies>


application.properties配置文件

# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=5000

mybatis.configuration.map-underscore-to-camel-case=true
mybatis.mapper-locations=mybatis/**/*Mapper.xml
mybatis.type-aliases-package=com.example.entity

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=123qwe
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

 生成tokean类

package com.example.uuid;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import sun.misc.BASE64Encoder;

/**
* 生成tokean放在redis中
*
* @author Administrator
*
*/
@Component
public class RedisToken {

@Autowired
private BaseRedisService baseRedisService;

private static final long TOKENTIME = 60 * 60;

public String getToken() {
String token = (System.currentTimeMillis() + new Random().nextInt(999999999)) + "";
try {
MessageDigest md = MessageDigest.getInstance("md5");
byte md5[] = md.digest(token.getBytes());
BASE64Encoder encoder = new BASE64Encoder();
String tokenValue = encoder.encode(md5);
baseRedisService.setString(tokenValue, tokenValue, TOKENTIME);
return tokenValue;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}

public String createToken() {
String token = "token" + UUID.randomUUID();
baseRedisService.setString(token, token, TOKENTIME);
return token;
}

public boolean checkToken(String tokenKey) {
String tokenValue = baseRedisService.getString(tokenKey);
if (StringUtils.isEmpty(tokenValue)) {
return false;
}
// 保证每个接口对应的token只能访问一次,保证接口幂等性问题
baseRedisService.delKey(tokenKey);
return true;
}

}

 

 

package com.example.uuid;

import java.util.concurrent.TimeUnit;

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

/**
* 集成封装redis
*
* @author Administrator
*
*/
@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 String getString(String key) {
return stringRedisTemplate.opsForValue().get(key);
}

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

}

 

package com.example.controller;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.em.ExtApiIdempotent;
import com.example.entity.User;
import com.example.utils.ConstantUtils;
import com.example.uuid.BaseRedisService;
import com.example.uuid.RedisToken;

/**
* 处理rpc调用请求
*
* @author Administrator
*
*/

@RestController
public class UserController {

@Autowired
private RedisToken redisToken;

@RequestMapping(value = "/getToken")
public String getToken() {
return redisToken.getToken();
}

@RequestMapping(value = "/addUser")
public String addOrder(User user, HttpServletRequest request) {
// 获取请求头中的token令牌
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
return "请求参数错误";
}

boolean isToken = redisToken.checkToken(token);
if (!isToken) {
System.out.println("请不要重新提交");
return "请勿重复提交!";
} else {
// 业务逻辑处理
System.out.println("校验成功,处理业务逻辑");
return "添加成功";
}

}

}

程序启动类:

@SpringBootApplication
@MapperScan("com.example.*")
public class SpringbootTokenApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTokenApplication.class, args);
}

}

将代码改造成AOP注解方式实现

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();
}

 

 

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 ExtApiToken {

}

public interface ConstantUtils {
static final String EXTAPIHEAD = "head";

static final String EXTAPIFROM = "from";
}

 

package com.example.aop;

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

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

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.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.example.em.ExtApiIdempotent;
import com.example.em.ExtApiToken;
import com.example.utils.ConstantUtils;
import com.example.uuid.RedisToken;

/**
* 接口幂等切面
// 1.获取令牌 存放在请求头中
// 2.判断令牌是否在缓存中有对应的令牌
// 3.如何缓存没有该令牌的话,直接报错(请勿重复提交)
// 4.如何缓存有该令牌的话,直接执行该业务逻辑
// 5.执行完业务逻辑之后,直接删除该令牌。

*/
@Aspect
@Component
public class ExtApiAopIdempotent {

@Autowired
private RedisToken redisToken;

// 切入点,拦截所有请求
@Pointcut("execution(public * com.example.controller.*.*(..))")
public void rlAop(){}


// 环绕通知拦截所有访问
@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) {
return extApiIdempotent(proceedingJoinPoint, signature);
}
// 放行
Object proceed = proceedingJoinPoint.proceed();
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();
String valueType = extApiIdempotent.value();
if (StringUtils.isEmpty(valueType)) {
response("参数错误!");
return null;
}
String token = null;
if (valueType.equals(ConstantUtils.EXTAPIHEAD)) {
token = request.getHeader("token");
} else {
token = request.getParameter("token");
}
if (StringUtils.isEmpty(token)) {
response("参数错误!");
return null;
}
if (!redisToken.checkToken(token)) {
response("请勿重复提交!");
return null;
}
Object proceed = proceedingJoinPoint.proceed();
return proceed;
}

public void extApiToken() {
String token = redisToken.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.print(msg);
} finally {
writer.close();
}
}


}

 

本文参考:https://blog.csdn.net/yz2015/article/details/81269320

posted @ 2019-12-26 16:46  逍遥游jJ2EE  阅读(386)  评论(0编辑  收藏  举报