網際網路API介面冪等設計
阿新 • • 發佈:2018-11-08
冪等性概念:保證唯一的意思 如何防止介面不能重複提交===保證介面冪等性
介面冪等產生原因: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傳入到頁面中: