很多场景下,接口需要做防重提交处理,比如下单,用户第一次提交后不能连续提交,从而避免接口被攻击;
1、java 注解介绍
Annotation(注解) 从JDK 1.5开始, Java增加了对元数据(MetaData)的⽀持,也就 是 Annotation(注解)。 注解其实就是代码⾥的特殊标记,它⽤于替代配置⽂件 常⻅的很多 @Override、@Deprecated等
什么是元注解 注解的注解,⽐如当我们需要⾃定义注解时 会需要⼀些元注解
(meta-annotation),如@Target和 @Retention
java内置4种元注解
@Target 表示该注解⽤于什么地⽅
ElementType.CONSTRUCTOR ⽤在构造器
ElementType.FIELD ⽤于描述域-属性上
ElementType.METHOD ⽤在⽅法上
ElementType.TYPE ⽤在类或接⼝上
ElementType.PACKAGE ⽤于描述包
@Retention 表示在什么级别保存该注解信息
RetentionPolicy.SOURCE 保留到源码上
RetentionPolicy.CLASS 保留到字节码上
RetentionPolicy.RUNTIME 保留到虚拟机运⾏时(最多, 可通过反射获取)
@Documented 将此注解包含在 javadoc 中
@Inherited 是否允许⼦类继承⽗类中的注解
@interface ⽤来声明⼀个注解,可以通过default来声明参数的默认值 ⾃定义注解时,⾃动继承了java.lang.annotation.Annotation 接⼝ 通过反射可以获取⾃定义注解
以下代码自定义了一个注解
/**
* 自定义防重提交注解
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
// 设置这个自顶一个注解里面的几个属性,即我们写注解的时候后面括号配置的参数
/**
* 防重提交,支持两种,一个方法参数,一个是令牌
*/
enum Type {PARAM,TOKEN }
/**
* 默认防重提交是方法参数
* @return
*/
Type limitType() default Type.PARAM;
/**
* 加锁过期时间,默认是 5s
* 比如通过redis的key来校验是否重复提交,
* 这个5s就是设置的key的过期时间
* @return
*/
long lockTime() default 5;
}
2、AOP切面编程
注解具体的效果,通过是基于AOP 切面编程来实现;注解标记的位置,作为切面编程的切入点
横切关注点: 对哪些⽅法进⾏拦截,拦截后怎么处理,这些就叫横切关 注点 ⽐如 权限认证、⽇志、事物
通知 Advice :在特定的切⼊点上执⾏的增强处理 做啥? ⽐如你需要记录⽇志,控制事务 ,提前编写好通⽤ 的模块,需要的地⽅直接调⽤ ⽐如防复提交判断逻辑 类型
@Before前置通知 在执⾏⽬标⽅法之前运⾏
@After后置通知 在⽬标⽅法运⾏结束之后
@AfterReturning返回通知 在⽬标⽅法正常返回值后运⾏
@AfterThrowing异常通知 在⽬标⽅法出现异常后运⾏
@Around环绕通知 在⽬标⽅法完成前、后做增强处理 ,环绕通知是最重要的通知类型 ,
像事务,⽇志等都是环绕通知,注意编 程中核⼼是⼀个ProceedingJoinPoint,
需要⼿动执 ⾏ joinPoint.procced()
连接点 JointPoint :要⽤通知的地⽅,业务流程在运⾏过程中需要插⼊切⾯的 具体位置, ⼀般是⽅法的调⽤前后,全部⽅法都可以是连接点
spring 的AOP 执行顺序:
5.0 之后
3、环绕通知中实现防重提交功能
防重提交的⽅式 :
token令牌⽅式
ip+类+⽅法⽅式
/**
* 定义切面类
*/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {
@Autowired
private StringRedisTemplate redisTemplate;
// @Autowired
// private RedissonClient redissonClient;
/**
* 定义切入点
* 方法一: 可以通过pointcut表达式来实现
*
* 方法而: 此处使用自定义注解来实现,即在有repeatsubmit 注解的地方,就是切入点
*/
@Pointcut("@annotation(repeatSubmit)")
public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit){
}
/**
* 环绕通知, 围绕着方法执行
*
* @param joinPoint
* @param
* @return
* @throws Throwable
* @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
*
* 方式一:单用 @Around("execution(* net.cloud.controller.*.*(..))")可以
* 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个)
*
*
* 两种方式
* 方式一:加锁 固定时间内不能重复提交
*
* 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交
*/
@Around("pointCutNoRepeatSubmit(repeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
//用于记录成功或者失败
boolean res = false;
//防重提交类型
String type = repeatSubmit.limitType().name();
if(type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())){
//方式一,参数形式防重提交
long lockTime = repeatSubmit.lockTime();
String ipAdr = CommonUtil.getIpAddr(request);
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
String className = method.getDeclaringClass().getName();
String key ="order-service:repeat_submit:" + CommonUtil.MD5( String.format("%s-%s-%s-%s", ipAdr,className,method,accountNo));
// 加锁 todo
res = redisTemplate.opsForValue().setIfAbsent(key,"1",lockTime, TimeUnit.SECONDS);
// RLock lock = redissonClient.getLock(key);
// // lock.lock(); 这种方式没有返回值
//
// // 尝试加锁,最多等待2s,上锁以后 locktime 以后自动解锁
// res = lock.tryLock(2,lockTime,TimeUnit.SECONDS);
}else {
//方式二,令牌形式防重提交;这种方式的令牌是在之前设置进了header里面 --getOrderToken
String requestToken = request.getHeader("request-token");
if(StringUtils.isBlank(requestToken)){
throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
}
String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY,accountNo,requestToken);
/**
* 提交表单的token key
* 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断
* 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成
*/
res = redisTemplate.delete(key);
}
if(!res){
log.error("请求重复");
return null;
}
log.info("环绕通知执行前");
Object obj = joinPoint.proceed();
log.info("环绕通知执行后");
return obj;
}
}