开源宝藏:Smart-Admin 重复提交防护的 AOP 切面实现详解
首先,说下重复提交问题,基本上解决方案,核心都是根据URL、参数、token等,有一个唯一值检验是否重复提交。
而下面这个是根据用户id,唯一值进行判定,使用两种缓存方式,redis和caffeine,可以通过配置修改使用那种方式。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
package net.lab1024.sa.common.module.support.repeatsubmit.annoation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标记 需要防止重复提交 的注解<br>
* 单位:毫秒
*
* @Author 1024创新实验室: 胡克
* @Date 2020-11-25 20:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright 1024创新实验室 ( https://1024lab.net )
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RepeatSubmit {
/**
* 重复提交间隔时间/毫秒
*
* @return
*/
int value() default 300;
/**
* 最长间隔30s
*/
int MAX_INTERVAL = 30000;
}
package net.lab1024.sa.common.module.support.repeatsubmit.ticket;
import java.util.function.Function;
/**
* 凭证(用于校验重复提交的东西)
*
* @Author 1024创新实验室: 罗伊
* @Date 2020-11-25 20:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright 1024创新实验室 ( https://1024lab.net )
*/
public abstract class AbstractRepeatSubmitTicket {
private Function<String, String> ticketFunction;
public AbstractRepeatSubmitTicket(Function<String, String> ticketFunction) {
this.ticketFunction = ticketFunction;
}
/**
* 获取凭证
*
* @param ticketToken
* @return
*/
public String getTicket(String ticketToken) {
return this.ticketFunction.apply(ticketToken);
}
/**
* 获取凭证 时间戳
*
* @param ticket
* @return
*/
public abstract Long getTicketTimestamp(String ticket);
/**
* 设置本次请求时间
*
* @param ticket
*/
public abstract void putTicket(String ticket);
/**
* 移除凭证
*
* @param ticket
*/
public abstract void removeTicket(String ticket);
}
import net.lab1024.sa.common.common.constant.StringConst;
import net.lab1024.sa.common.common.util.SmartRequestUtil;
import net.lab1024.sa.common.module.support.repeatsubmit.RepeatSubmitAspect;
import net.lab1024.sa.common.module.support.repeatsubmit.ticket.RepeatSubmitCaffeineTicket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 重复提交配置
*
* @Author 1024创新实验室: 罗伊
* @Date 2021/10/9 18:47
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright 1024创新实验室 ( https://1024lab.net )
*/
@Configuration
public class RepeatSubmitConfig {
@Bean
public RepeatSubmitAspect repeatSubmitAspect() {
RepeatSubmitCaffeineTicket caffeineTicket = new RepeatSubmitCaffeineTicket(this::ticket);
return new RepeatSubmitAspect(caffeineTicket);
}
/**
* 获取指明某个用户的凭证
*
* @return
*/
private String ticket(String servletPath) {
Long userId = SmartRequestUtil.getRequestUserId();
if (null == userId) {
return StringConst.EMPTY;
}
return servletPath + "_" + userId;
}
}
package net.lab1024.sa.common.module.support.repeatsubmit.ticket;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import net.lab1024.sa.common.module.support.repeatsubmit.annoation.RepeatSubmit;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* 凭证(内存实现)
*
* @Author 1024创新实验室: 罗伊
* @Date 2020-11-25 20:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright 1024创新实验室 ( https://1024lab.net )
*/
public class RepeatSubmitCaffeineTicket extends AbstractRepeatSubmitTicket {
/**
* 限制缓存最大数量 超过后先放入的会自动移除
* 默认缓存时间
* 初始大小为:100万
*/
private static Cache<String, Long> cache = Caffeine.newBuilder()
.maximumSize(100 * 10000)
.expireAfterWrite(RepeatSubmit.MAX_INTERVAL, TimeUnit.MILLISECONDS).build();
public RepeatSubmitCaffeineTicket(Function<String, String> ticketFunction) {
super(ticketFunction);
}
@Override
public Long getTicketTimestamp(String ticket) {
return cache.getIfPresent(ticket);
}
@Override
public void putTicket(String ticket) {
cache.put(ticket, System.currentTimeMillis());
}
@Override
public void removeTicket(String ticket) {
cache.invalidate(ticket);
}
}
package net.lab1024.sa.common.module.support.repeatsubmit.ticket;
import net.lab1024.sa.common.module.support.repeatsubmit.annoation.RepeatSubmit;
import org.springframework.data.redis.core.ValueOperations;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* 凭证(redis实现)
*
* @Author 1024创新实验室: 罗伊
* @Date 2020-11-25 20:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright 1024创新实验室 ( https://1024lab.net )
*/
public class RepeatSubmitRedisTicket extends AbstractRepeatSubmitTicket {
private ValueOperations<String, String> redisValueOperations;
public RepeatSubmitRedisTicket(ValueOperations<String, String> redisValueOperations,
Function<String, String> ticketFunction) {
super(ticketFunction);
this.redisValueOperations = redisValueOperations;
}
@Override
public Long getTicketTimestamp(String ticket) {
Long timeStamp = System.currentTimeMillis();
boolean setFlag = redisValueOperations.setIfAbsent(ticket, String.valueOf(timeStamp), RepeatSubmit.MAX_INTERVAL, TimeUnit.MILLISECONDS);
if (!setFlag) {
timeStamp = Long.valueOf(redisValueOperations.get(ticket));
}
return timeStamp;
}
@Override
public void putTicket(String ticket) {
redisValueOperations.getOperations().delete(ticket);
this.getTicketTimestamp(ticket);
}
@Override
public void removeTicket(String ticket) {
redisValueOperations.getOperations().delete(ticket);
}
}
package net.lab1024.sa.common.module.support.repeatsubmit;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.common.common.code.UserErrorCode;
import net.lab1024.sa.common.common.domain.ResponseDTO;
import net.lab1024.sa.common.module.support.repeatsubmit.annoation.RepeatSubmit;
import net.lab1024.sa.common.module.support.repeatsubmit.ticket.AbstractRepeatSubmitTicket;
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.reflect.MethodSignature;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
/**
* 重复提交 aop切口
*
* @Author 1024创新实验室: 胡克
* @Date 2020-11-25 20:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright 1024创新实验室 ( https://1024lab.net )
*/
@Aspect
@Slf4j
public class RepeatSubmitAspect {
private AbstractRepeatSubmitTicket repeatSubmitTicket;
/**
* 获取凭证信息
* rep
*
* @param repeatSubmitTicket
*/
public RepeatSubmitAspect(AbstractRepeatSubmitTicket repeatSubmitTicket) {
this.repeatSubmitTicket = repeatSubmitTicket;
}
/**
* 定义切入点
*
* @param point
* @return
* @throws Throwable
*/
@Around("@annotation(net.lab1024.sa.common.module.support.repeatsubmit.annoation.RepeatSubmit)")
public Object around(ProceedingJoinPoint point) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ticketToken = attributes.getRequest().getServletPath();
String ticket = this.repeatSubmitTicket.getTicket(ticketToken);
if (StringUtils.isEmpty(ticket)) {
return point.proceed();
}
Long timeStamp = this.repeatSubmitTicket.getTicketTimestamp(ticket);
if (timeStamp != null) {
Method method = ((MethodSignature) point.getSignature()).getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 说明注解去掉了
if (annotation != null) {
return point.proceed();
}
int interval = Math.min(annotation.value(), RepeatSubmit.MAX_INTERVAL);
if (System.currentTimeMillis() < timeStamp + interval) {
// 提交频繁
return ResponseDTO.error(UserErrorCode.REPEAT_SUBMIT);
}
}
Object obj = null;
try {
// 先给 ticket 设置在执行中
this.repeatSubmitTicket.putTicket(ticket);
obj = point.proceed();
} catch (Throwable throwable) {
log.error("", throwable);
throw throwable;
} finally {
this.repeatSubmitTicket.removeTicket(ticket);
}
return obj;
}
}
参考链接:https://github.com/1024-lab/smart-admin