互联网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=localhost
# 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.yz.entity

spring.datasource.url=jdbc:mysql://localhost:3306/test01
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.UUID;

/**
 * 生成token,放入redis中
 * Created by yz on 2018/7/29.
 */
@Component
public class RedisToken {

    @Autowired
    private BaseRedisService baseRedisService;

    private static final long TOKENTIME = 60*60;

    public String getToken(){
        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;
    }
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 集成封装redis
 * Created by yz on 2018/7/29.
 */
@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);
    }
}
import com.yz.entity.User;
import com.yz.service.UserService;
import com.yz.utils.RedisToken;
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 javax.servlet.http.HttpServletRequest;

/**
 * 处理rpc调用请求
 * Created by yz on 2018/7/29.
 */
@RestController
public class UserController {

    @Autowired
    private RedisToken redisToken;

    @Autowired
    private UserService userService;

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

    @RequestMapping(value = "/addUser")
    public String addOrder(User user, HttpServletRequest request){
        // 获取请求头中的token令牌
        String token = request.getHeader("token");
        if(StringUtils.isEmpty(token)){
            return "参数错误";
        }
        // 校验token
        boolean isToken = redisToken.checkToken(token);
        if(!isToken){
            return "请勿重复提交!";
        }
        // 业务逻辑
        int result = userService.addUser(user);
        return result >0 ? "添加成功" : "添加失败";
    }

}
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.yz.mapper")
@SpringBootApplication
public class YzApplication {

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

测试效果:

获取token

请求接口

再次请求:

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

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

/**
 * 解决接口幂等性问题,支持网络延迟和表单提交
 * Created by yz on 2018/7/29.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckToken {
    // 区分请求来源
    String type();
}
import com.yz.annotation.CheckToken;
import com.yz.utils.ConstantUtils;
import com.yz.utils.RedisToken;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 接口幂等切面
 * Created by yz on 2018/7/29.
 */
@Aspect
@Component
public class ExtApiAopIdempotent {

    @Autowired
    private RedisToken redisToken;

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

    // 环绕通知拦截所有访问
    @Around("rlAop()")
    public Object doBefore(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 判断方法上是否有加ExtApiAopIdempotent注解
        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
        CheckToken declaredAnnotation = methodSignature.getMethod().getDeclaredAnnotation(CheckToken.class);
        if(declaredAnnotation != null){
            String type = declaredAnnotation.type();
            String token = null;
            HttpServletRequest request = getRequest();
            if(type.equals(ConstantUtils.EXTAPIHEAD)){
                // 获取请求头中的token令牌
                token = request.getHeader("token");
            }else{
                // 从表单中获取token
                token = request.getParameter("token");
            }
            if(StringUtils.isEmpty(token)){
                return "参数错误";
            }
            // 校验token
            boolean isToken = redisToken.checkToken(token);
            if(!isToken){
                return "请勿重复提交!";
            }
        }
        // 放行
        Object proceed = proceedingJoinPoint.proceed();
        return proceed;
    }

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

}

controller使用@CheckToken注解:

import com.yz.annotation.CheckToken;
import com.yz.entity.User;
import com.yz.service.UserService;
import com.yz.utils.ConstantUtils;
import com.yz.utils.RedisToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * 处理rpc调用请求
 * Created by yz on 2018/7/29.
 */
@RestController
public class UserController {

    @Autowired
    private RedisToken redisToken;

    @Autowired
    private UserService userService;

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

    // 使用CheckToken注解方式保证请求幂等性
    @RequestMapping(value = "/addUser")
    @CheckToken(type = ConstantUtils.EXTAPIHEAD)
    public String addOrder(User user, HttpServletRequest request){
        // 业务逻辑
        int result = userService.addUser(user);
        return result >0 ? "添加成功" : "添加失败";
    }

}

执行效果:

2.表单重复提交

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
spring.http.encoding.force=true
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8

index.jsp

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Insert title here</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/addUserForPage" method="post">
    <input type="hidden" id="token" name="token" value="${token}">
        name: <input id="name" name="name" />
    <p>
        age:  <input id="age" name="age" />
    <p>
        <input type="submit" value="submit" />
</form>
</body>
</html>
import com.yz.annotation.CheckToken;
import com.yz.entity.User;
import com.yz.service.UserService;
import com.yz.utils.ConstantUtils;
import com.yz.utils.RedisToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/**
 * 处理表单提交请求
 * Created by yz on 2018/7/29.
 */
@Controller
public class UserPageController {

    @Autowired
    private RedisToken redisToken;

    @Autowired
    private UserService userService;

    /**
     * 页面跳转
     * @param req
     * @return
     */
    @RequestMapping("/indexPage")
    public String indexPage(HttpServletRequest req){
        req.setAttribute("token",redisToken.getToken());
        return "index";
    }

    // 使用CheckToken注解方式保证请求幂等性
    @RequestMapping(value = "/addUserForPage")
    @CheckToken(type = ConstantUtils.EXTAPIFROM)
    @ResponseBody
    public String addOrder(User user, HttpServletRequest request){
        // 业务逻辑
        int result = userService.addUser(user);
        return result >0 ? "添加成功" : "添加失败";
    }
}

自定义注解生成Token,将 req.setAttribute("token",redisToken.getToken()); 放在AOP中,减少代码冗余:

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

/**
 * 自定义注解生成Token
 * Created by yz on 2018/7/29.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CreatToken {
}
// 切入点,拦截所有请求
@Pointcut("execution(public * com.yz.controller.*.*(..))")
public void rlAop(){}

// 前置通知,生成Token
@Before("rlAop()")
public void before(JoinPoint point){
    MethodSignature signature = (MethodSignature) point.getSignature();
    CreatToken declaredAnnotation = signature.getMethod().getDeclaredAnnotation(CreatToken.class);
    if(declaredAnnotation != null){
        getRequest().setAttribute("token",redisToken.getToken());
    }
}
import com.yz.annotation.CheckToken;
import com.yz.annotation.CreatToken;
import com.yz.entity.User;
import com.yz.service.UserService;
import com.yz.utils.ConstantUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/**
 * 处理表单提交请求
 * Created by yz on 2018/7/29.
 */
@Controller
public class UserPageController {

    @Autowired
    private UserService userService;

    /**
     * 页面跳转,使用自定义注解生成token,传递到跳转页面中
     * @param req
     * @return
     */
    @RequestMapping("/indexPage")
    @CreatToken
    public String indexPage(HttpServletRequest req){
        //req.setAttribute("token",redisToken.getToken());
        return "index";
    }

    // 使用CheckToken注解方式保证请求幂等性
    @RequestMapping(value = "/addUserForPage")
    @CheckToken(type = ConstantUtils.EXTAPIFROM)
    @ResponseBody
    public String addOrder(User user, HttpServletRequest request){
        // 业务逻辑
        int result = userService.addUser(user);
        return result >0 ? "添加成功" : "添加失败";
    }
}

请求页面的时候,AOP注解会将创建好的token传入到页面中:

代码下载地址:https://github.com/yangzeng1211/api_idempotent.git

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值