zoukankan      html  css  js  c++  java
  • 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>

    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);  //key: token value: token 时间
            return token;
        }
    
        public synchronize boolean findToken(String tokenKey) {  //从redis查询对应的token   防止没来得及删除 只有一个线程操作 其实redis已经可以防止了
            String token = (String) baseRedisService.getString(tokenKey);
            if (StringUtils.isEmpty(token)) { //要么被被人使用过了 要么没有对应token
                return false;
            }
            // token 获取成功后 删除对应tokenMapstoken
            baseRedisService.delKey(token);
            return true;  //保证每个接口对应的token只能访问一次,保证接口幂等性问题
        }
    
    }

    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获取参数 进行校验

  • 相关阅读:
    使用字体图标完整步骤
    用position:absolute定位小窗口位于版面正中心
    MySql 技术内幕 (第7章 游标)
    MySql 技术内幕 (第5章 联接与集合操作)
    赋值语句作为判断的条件
    发布订阅模式和观察者模式
    关系代数
    数据库关系代数表达式学习
    软考通过分数
    哈希表——线性探测法、链地址法、查找成功、查找不成功的平均长度
  • 原文地址:https://www.cnblogs.com/toov5/p/10312458.html
Copyright © 2011-2022 走看看