互联网安全架构(4)-纯手互联网API接口幂等框架

纯手写API互联网幂等框架

  • 表单重复提交
  • rpc远程调用的时候网络发生延迟的时候,可能会有重试机制
    针对上面的情况,我们就要设计程序的幂等性(幂等性:保证接口唯一,数据的唯一性,不重复)

幂等性

  • 幂等性就是保证数据唯一的意思,保证幂等性就是防止了接口不能重复提交
  • API接口幂等性的问题解决的核心在于服务器的防御,而不是客户端的防御
  • 如何保证API接口的幂等性:

(1)使用Token(令牌),该Token是惟一的,且并不是持久化的,一般是临时的,是15分钟-120分钟(token的生成只用保证临时且唯一就可以了

注意:分布式情况下,如果使用时间戳作为token,是有一定问题的,分布式情况下的时间戳可能会相同。可以加上锁去解决,不是很推荐使用时间戳作为token(即使加锁了也有可能重复)

(2)token+redis,将生成的token放到redis中,设置其在redis中的过期时间,就可以做到临时性(一般设置为3分钟左右即可)

如何调用Token解决幂等性
  1. 在调用接口之前生成对应的令牌(Token)
  2. 将生成的令牌存放在redis中(redis天生线程安全)
  3. 调用接口的时候将Token放在请求头中
  4. 接口从redis中获取对应的令牌
  5. 如果可以获取该令牌执行业务逻辑,并将令牌从redis中删除
  6. 如果获取不到该令牌就直接返回错误提示
    如果有人恶意模拟token调用接口访问,可以使用验证码进行校验

自定义注解实现幂等性

  1. 防止API接口封装
  2. 防止表单重复提交
    (1)表单的防御重复提交也是采用token的方式(也可以做重定向进行解决,这里是用token的方式解决)
    (2)表单提交其实是A页面提交数据到B页面展示的过程
    (3)在第一次访问A页面的时候添加一个隐藏域,该隐藏域存后台生成的token(第一次进入到A页面的时候生成Token,可以放到Aop的前置通知),当我们A页面点击提交的时候,后台校验该token是否存在于redis中,如果存在继续执行正常逻辑,否则就是重复提交。
  3. 代码实现:
  • 架构演示
    互联网安全架构(4)-纯手互联网API接口幂等框架_第1张图片
  • pom依赖

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.xiyougroupId>
    <artifactId>extannotationartifactId>
    <version>1.0-SNAPSHOTversion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.0.0.RELEASEversion>
    parent>
    <dependencies>

        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>1.1.1version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>

        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-tomcatartifactId>
        dependency>
        
        <dependency>
            <groupId>org.apache.tomcat.embedgroupId>
            <artifactId>tomcat-embed-jasperartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-log4jartifactId>
            <version>1.3.8.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-aopartifactId>
        dependency>
        
        <dependency>
            <groupId>commons-langgroupId>
            <artifactId>commons-langartifactId>
            <version>2.6version>
        dependency>
        
        <dependency>
            <groupId>org.apache.httpcomponentsgroupId>
            <artifactId>httpclientartifactId>
        dependency>
        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.47version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>jstlartifactId>
        dependency>
        <dependency>
            <groupId>taglibsgroupId>
            <artifactId>standardartifactId>
            <version>1.1.2version>
        dependency>
    dependencies>

project>
  • 配置文件yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3307/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: 127.0.0.1
    port: 6379
    password:
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0
    timeout: 10000

domain:
  name: www.xiyou.com

server:
  port: 8889

  • 启动类
package com.xiyou;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

  • 自定义注解(实现幂等性接口)
package com.xiyou.ext;

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

/**
 * 自定义注解,目的是放置接口的重复提交,接口的幂等性和表单的重复提交
 * @author
 */
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiIdempotent {
    /**
     * 定义类型,表示该token是来自请求头还是来自表单
     * @return
     */
    String type();
}

  • 自定义注解(实现自动生成token放到请求中)
package com.xiyou.ext;

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

/**
 * 该注解的作用是生成token,转发到页面进行展示
 * @author
 */
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExtApiToken {
}

  • 工具类

(1)注解常量

package com.xiyou.utils;

/**
 * 规定常量
 */
public interface ConstantUtils {
    /**
     * 表示该token是来自请求头
     */
    public static String EXTAPIHEAD = "head";
    /**
     * 表示该token是来自form表单
     */
    public static String EXTAPIFROM = "form";
}

(2)redis的相关工具类(设置过期时间,查找redis是否存在token)

package com.xiyou.utils;

import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
 * redis的相关工具类
 */
@Component
public class RedisTokenUtils {

    /**
     * 设置过期时间
     */
    private long timeOut = 3600L;

    /**
     * 自己设计的redis库
     */
    private BaseRedisService baseRedisService;

    public String getToken(){
        String token = "token_" + UUID.randomUUID().toString().replaceAll("-", "");
        // 将token放到redis中
        baseRedisService.setString(token, token, timeOut);
        // 返回生成的token
        return token;
    }

    /**
     * 查找需要的token是否在redis中,如果在可以继续执行(并且删除存在redis中的key),如果不在表示重复提交
     * @param tokenKey
     * @return
     */
    public boolean findToken (String tokenKey) {
        // 尝试从redis中获取值
        String token = (String) baseRedisService.getString(tokenKey);
        if (StringUtils.isBlank(token)) {
            // 如果当前token为空
            return false;
        } else {
            // 存在redis中,删除key
            baseRedisService.delKey(tokenKey);
            return true;
        }
    }

}

(3)redis的常用方法

package com.xiyou.utils;

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
 */
@Component
public class BaseRedisService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public void setString(String key, Object data, Long timeout){
        if (data instanceof String) {
            // value的类型是String类型
            String value = (String) data;
            stringRedisTemplate.opsForValue().set(key, value);
        }

        if (timeout != null) {
            // 设置其过期时间
            stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
        }
    }

    /**
     * 得到key对应的值
     * @param key
     * @return
     */
    public Object getString(String key) {
        return this.stringRedisTemplate.opsForValue().get(key);
    }

    /**
     * 删除key对应的value
     * @param key
     */
    public void delKey (String key) {
        this.stringRedisTemplate.delete(key);
    }
}

  • aop实现自定义注解的功能(重要
package com.xiyou.aop;

import com.xiyou.ext.ExtApiIdempotent;
import com.xiyou.ext.ExtApiToken;
import com.xiyou.utils.ConstantUtils;
import com.xiyou.utils.RedisTokenUtils;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 切面类
 * @author
 */
@Aspect
// 必须加注解,不加注解扫描不上这个包
@Component
public class ExtApiIdempotentAop {

    @Autowired
    private RedisTokenUtils redisTokenUtils;

    /**
     * 自定义切点
     */
    @Pointcut("execution(public * com.xiyou.controller.*.*(..))")
    public void rlAop() {

    }

    /**
     * 前置通知: 这里用来处理自动生成token的注解,有ExtApiToken注解的类,需要自动生成token放到request中
     */
    @Before("rlAop()")
    public void before(JoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 判断当前调用的方法上面是否有注解
        ExtApiToken extApiToken = signature.getMethod().getDeclaredAnnotation(ExtApiToken.class);
        if (extApiToken != null) {
            // 表示当前方法上面存在该注解
            // 生成request对象,并且生成token,将token放到请求头中
            getRequest().setAttribute("token", redisTokenUtils.getToken());
        }
    }

    /**
     * 环绕通知: 这里用来处理ExtApiIdempotent注解。该注解的作用是保证接口的幂等性
     * @param proceedingJoinPoint
     * @return
     * @throws Throwable
     */
    @Around("rlAop()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // 判断当前方法上是否有ExtApiIdempotent注解,有注解就表示了必须保证幂等性
        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
        ExtApiIdempotent declaredAnnotation = methodSignature.getMethod().getDeclaredAnnotation(ExtApiIdempotent.class);
        if (declaredAnnotation != null) {
            // 如果当前类上有这个注解,表示需要判断其token是否存在
            // 得到其配置的type属性,type属性表示了token是请求头中的还是表单中的
            String type = declaredAnnotation.type();
            String token = null;
            // 得到request对象
            HttpServletRequest request = getRequest();
            if (type.equals(ConstantUtils.EXTAPIHEAD)) {
                // 如果类型请求头,表示token是来自请求头中的
                token = request.getHeader("token");
            } else {
                // 如果不是请求头,则认为其是表单提交,即使什么没有写
                token = request.getParameter("token");
            }

            if (StringUtils.isBlank(token)) {
                // 如果当前的token是空,表示错误
                return "参数错误,没有token";
            }

            // 判断能否从redis中取到key为当前token的记录
            // 能取到记录返回的是true,且删除redis中存在的key(当前token)
            boolean isToken = redisTokenUtils.findToken(token);
            if (!isToken) {
                // 表示不可以取到,则表示重复提交
                response("请勿重复提交");
                return null;
            }
        }
        // 放行程序,开始执行
        Object proceed = proceedingJoinPoint.proceed();
        return proceed;
    }

    /**
     * 得到当前的请求对象
     * @return
     */
    public HttpServletRequest getRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        return request;
    }

    /**
     * 得到响应对象,往浏览器上写数据
     * @param msg
     * @throws IOException
     */
    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();
        }
    }
}

你可能感兴趣的:(蚂蚁课堂的视频笔记)