1. 程式人生 > >API接口冪等性框架設計

API接口冪等性框架設計

mysql- public 通知 用戶 red des entity ihe cati

表單重復提價問題

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: meitedu.+@
    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接口冪等性框架設計