import java.lang.annotation.*;
/**
* 自定义防重提交
* @author
* @date 2023年9月6日11:19:13
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 默认防重提交,是方法参数
* @return
*/
Type limitType() default Type.PARAM;
/**
* 加锁过期时间,默认是5秒
* @return
*/
long lockTime() default 5;
/**
* 规定周期内限制次数
*/
int maxCount() default 1;
/**
* 触发限制时的消息提示
*/
String msg() default "操作频率过高";
/**
* 防重提交,支持两种,一个是方法参数,一个是令牌
*/
enum Type {
PARAM(1, "方法参数"), TOKEN(2, "令牌");
private int id;
private String name;
Type() {
}
Type(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return this.name;
}
}
/**
* 是否需要登录
* @return
*/
boolean needLogin() default false;
}
import cn.hutool.extra.servlet.ServletUtil;
import com.qihoo.mssosservice.annotation.RepeatSubmit;
import com.qihoo.mssosservice.constants.Constants;
import com.qihoo.mssosservice.model.resp.RestfulResult;
import com.redxun.common.utils.ContextUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
/**
* 基于环绕通知实现限流锁
* @author
* @date 2023年9月6日17:11:10
*/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 定义 @Pointcut注解表达式, 通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法
* 在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点
*
* 方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这)
* 方式二:execution:一般用于指定方法的执行
*/
@Pointcut("@annotation(repeatSubmit)")
public void pointCutNoRepeatSubmit(RepeatSubmit repeatSubmit) {
}
/**
* 环绕通知, 围绕着方法执行
*
* @param joinPoint
* @param repeatSubmit
* @return
* @throws Throwable
* @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
*
* 方式一:单用 @Around("execution(* cn.mss.management.center.controller.*.*(..))")可以
* 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个)
*
*
* 两种方式
* 方式一:加锁 固定时间内不能重复提交
*
* 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交
*/
@Around("pointCutNoRepeatSubmit(repeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String accountNo = ObjectUtils.isEmpty(ContextUtil.getCurrentUser()) ? "admin" : ContextUtil.getCurrentUser().getAccount();
//用于记录成功或者失败
boolean res = false;
// 超时时间
long lockTime = repeatSubmit.lockTime();
// 最大次数
int maxCount = repeatSubmit.maxCount();
// 异常信息
String msg = repeatSubmit.msg();
//防重提交类型
String type = repeatSubmit.limitType().name();
// 是否需要登录
boolean needLogin = repeatSubmit.needLogin();
if (needLogin) {
String authorization = request.getHeader("Authorization");
if (StringUtils.isBlank(authorization )) {
authorization = request.getParameter("Authorization");
}
if (StringUtils.isBlank(authorization)) {
return RestfulResult.getFailResult("没有登录");
}
}
if (type.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
//方式一,参数形式防重提交
String ipAddr = ServletUtil.getClientIP(request, null);
String requestURI = request.getRequestURI();
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
String className = method.getDeclaringClass().getName();
String key = Constants.SUBMIT_ORDER_REPEAT_SUBMIT+ DigestUtils.md5DigestAsHex(String.format("%s-%s-%s-%s-%s",ipAddr,requestURI,className,method,accountNo).getBytes(StandardCharsets.UTF_8));
String count = redisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(count)) {
//在规定周期内第一次访问,存入redis
// redisTemplate.opsForValue().increment(key);
// redisTemplate.expire(key, lockTime, TimeUnit.SECONDS);
//加锁
res = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);
} else {
if (Integer.valueOf(count) > maxCount) {
//超出访问限制次数
return RestfulResult.getFailResult(msg);
} else {
redisTemplate.opsForValue().increment(key);
}
}
} else if (type.equalsIgnoreCase(RepeatSubmit.Type.TOKEN.name())) {
//方式二,令牌形式防重提交
String authorization = request.getHeader("Authorization");
if (StringUtils.isBlank(authorization )) {
authorization = request.getParameter("Authorization");
}
if (StringUtils.isBlank(authorization)) {
return RestfulResult.getFailResult("没有登录");
}
String key = String.format(Constants.SUBMIT_ORDER_REPEAT_TOKEN_KEY, authorization, accountNo);
String count = redisTemplate.opsForValue().get(key);
if (StringUtils.isBlank(count)) {
//在规定周期内第一次访问,存入redis
//加锁
res = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);
} else {
if (Integer.valueOf(count) > maxCount) {
//超出访问限制次数
return RestfulResult.getFailResult(msg);
} else {
redisTemplate.opsForValue().increment(key);
}
}
/**
* 提交表单的token key
* 方式一:不用lua脚本获取再判断,之前是因为 key组成是 os-service:submit:accountNo, value是对应的token,所以需要先获取值,再判断
* 方式二:可以直接key是 os-service:submit:accountNo:token,然后直接删除成功则完成
*/
// res = redisTemplate.delete(key);
} else {
return RestfulResult.getFailResult(type+":未定义的类型!");
}
if (!res) {
log.error("请求重复提交");
return RestfulResult.getFailResult("请求重复提交");
}
log.info("环绕通知执行前");
Object obj = joinPoint.proceed();
log.info("环绕通知执行后");
return obj;
}
}
/**
* 1. 使用默认
**/
@RepeatSubmit
@GetMapping("/getAllURL")
public RestfulResult getAllURL() {
// todo
return RestfulResult.getSuccessResult(result);
}
/**
* 1.替换默认值
**/
@RepeatSubmit(limitType =RepeatSubmit.Type.PARAM,lockTime=10,maxCount=2,msg = "test",needLogin = true)
@GetMapping("/getAllURL")
public RestfulResult getAllURL() {
// todo
return RestfulResult.getSuccessResult(result);
}