API接口幂等性框架设计

Posted toov5

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了API接口幂等性框架设计相关的知识,希望对你有一定的参考价值。

  表单重复提价问题

  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>

 

1、关于Header的token的注解封装

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

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);
        return token;
    }

    public boolean findToken(String tokenKey) {
        String token = (String) baseRedisService.getString(tokenKey);
        if (StringUtils.isEmpty(token)) {
            return false;
        }
        // token 获取成功后 删除对应tokenMapstoken
        baseRedisService.delKey(token);
        return true;
    }

}

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: [email protected]
    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获取参数 进行校验

 

以上是关于API接口幂等性框架设计的主要内容,如果未能解决你的问题,请参考以下文章

接口设计之幂等性

基于SpringBoot实现Api接口幂等性的几种方式

笔记:Jersey REST API 设计

聊聊接口幂等性设计

聊聊接口幂等性设计

支付接口的幂等性设计