【架构技术必学篇】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@Componentpublic 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;
@RestControllerpublic 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;
@Controllerpublic 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"; }
}
常量:
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;
@Componentpublic 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: 10000domain: name: www.toov5.com
启动类:
@MapperScan(basePackages = { "com.tov5.mapper" })@SpringBootApplication@ServletComponentScanpublic class AppB {
public static void main(String[] args) { SpringApplication.run(AppB.class, args); }
}
总结:
核心就是
自定义注解
controller中的方法注解
aop切面类判断对象是否有相应的注解 如果有 从parameter或者header获取参数 进行校验