场景:对于一些未登录的用户向系统提交表单信息时,不能拿到token来唯一标识用户,而用户恶意重复点击的情况。
方案:在表单中用自定义@Tag注解标记需要一个唯一的表单数据(比如:手机号,身份证等),使用AOP在切面中获取标记的属性值,组装放入redis并设置过期用户重复提交该条记录就不进行数据库判断,直接从redis获取数据返会重复提交,以次减少服务器和数据库压力。
代码:
注解NoRepeat :
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeat {
// 默认30s
long time() default 30L;
}
注解Tag :
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Tag {
}
切面:
@Aspect
@Slf4j
@Configuration
public class NoRepeatAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private static final String DUPLICATE_COMMIT = "DUPLICATE_COMMIT:";
/**
* 切点
*/
@Pointcut("@annotation(cn.zk.common.annatation.NoRepeat)")
public void pointcut() {
}
@Around("pointcut()&&@annotation(nrp)")
public Object around(ProceedingJoinPoint joinPoint, NoRepeat nrp) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Object proceed = null;
StringBuffer cachePrefix = null;
try {
// 获取需要缓存的key
cachePrefix = getTagString(joinPoint);
String result = redisTemplate.opsForValue().get(DUPLICATE_COMMIT+cachePrefix.toString());
if (!StringUtils.isEmpty(result)) {
log.info("用户重复点击!==>key:{}", cachePrefix.toString());
proceed = Response.failed(BizError.SYS_TIP, "请勿重复点击!");
} else {
proceed = joinPoint.proceed();
}
} catch (Throwable throwable) {
throwable.printStackTrace();
log.error("重复点击切面错误==>key:{},err:[}", cachePrefix.toString(), throwable.getMessage());
proceed = Response.failed(BizError.SYS_TIP, "系统错误!");
} finally {
if (!Objects.isNull(cachePrefix)) {
redisTemplate.opsForValue().set(DUPLICATE_COMMIT+cachePrefix.toString(),
sdf.format(new Date()), nrp.time(), TimeUnit.SECONDS);
}
}
return proceed;
}
/**
* 获取参数中有Tag注解字段 并组装成redis的key
*
* @param joinPoint
* @return
* @throws IllegalAccessException
*/
private StringBuffer getTagString(ProceedingJoinPoint joinPoint) throws IllegalAccessException {
// 获取参数
Object[] params = joinPoint.getArgs();
if (params.length == 0) {
return null;
}
//获取方法,此处可将signature强转为MethodSignature
// API:获取连接点处的签名(用于跟踪或记录应用程序以获取有关连接点的反射信息)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//参数注解,1维是参数,2维是注解
StringBuffer cachePrefix = new StringBuffer();
// API:返回一个 Annotations 数组,1维是参数,2维是注解
Annotation[][] annotations = method.getParameterAnnotations();
for (int i = 0; i < annotations.length; i++) {
Object param = params[i];
Class<?> clz = param.getClass();
// 扫描参数中的所有字段
// API:返回一个 Field 对象数组,反映由此 Class 对象表示的类或接口声明的所有字段。
// 这包括公共、受保护、默认(包)访问和私有字段,但不包括继承的字段
for (Field field : clz.getDeclaredFields()) {
field.setAccessible(true);
if (field.isAnnotationPresent(Tag.class)) {
Tag tag = field.getAnnotation(Tag.class);
// API:返回指定对象上此字段表示的字段的值。如果该值具有原始类型,则该值会自动包装在对象中。
Object value = field.get(param);
if(!Objects.isNull(value)) {
cachePrefix.append(String.valueOf(value) + "-");
}
}
}
}
return cachePrefix;
}
}
使用方式:
表单对象
接口:
验证:
第一次提交:
30秒内重复提交:
redis: