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