1. 程式人生 > >網際網路API介面冪等設計

網際網路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