Java利用注解、Redis做防重复提交和限流
使用场景
- 用户网络慢,电脑卡,一直点击保存,修改按钮无返回信息,会导致多个请求去保存、修改
- 开放接口、或加密接口频繁访问,会导致程序压力大,可能被他人写脚本一直请求接口
解决方案
- 前端js提交后禁止按钮,返回结果后解禁(前端不严谨,点击速度快,也可重复提交)
- 在java中添加自定义防重复提交注解 @RepeatSubmit ,利用AOP切入,其次用Redis临时存入唯一信息。开放接口把请求的IP、请求路径、请求的电脑User-Agent拼接为唯一key,未开发接口按照使用场景,组装为唯一key
- 等等…
实现案例
- 进入相关依赖 pom.xml
<!-- redis缓存依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring aop依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
- 定义 @RepeatSubmit 注解
package com.base.sbc.config.aspect.annotation;
/**
* @author lizan
* @date 2023-03-13 16:12
*/
import java.lang.annotation.*;
/**
* @author lizan
* @date 2023-03-13 16:16
* 自定义防重提交注解
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 防重提交,支持两种,一个方法参数,一个是令牌
*/
enum Type {PARAM,TOKEN }
/**
* 默认防重提交是方法参数
*/
Type limitType() default Type.PARAM;
/**
* 加锁过期时间,默认是 5s
* 比如通过redis的key来校验是否重复提交,
* 这个5s就是设置的key的过期时间
*/
long lockTime() default 5;
}
- 定义AOP切面类:RepeatSubmitAspect,现在定义两种重复提交或限流,一种:获取用户电脑信息、获取请求IP地址、获取请求Url ,第二种:获取请求里的token、获取请求IP地址。如不符合场景,可在repeatSubmit环绕通知方法中重写。注(方法中使用获取IP工具类、常量类,CommonConstant为常量,可直接去创建,获取IP工具类放在尾部)
package com.base.sbc.config.aspect;
import com.base.sbc.config.aspect.annotation.RepeatSubmit;
import com.base.sbc.config.common.ApiResult;
import com.base.sbc.config.constant.CommonConstant;
import com.base.sbc.config.redis.RedisUtils;
import com.base.sbc.config.utils.IpAdrressUtil;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 javax.servlet.http.HttpServletRequest;
/**
* 防止重复提交AOP
* @author lizan
* @date 2023-03-13 16:16
*/
@Aspect
@Component
public class RepeatSubmitAspect {
/** 日志对象 */
protected Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private RedisUtils redisUtils;
/**
* 定义切入点
*/
@Pointcut("@annotation(repeatSubmit)")
public void pointNoRepeatSubmit(RepeatSubmit repeatSubmit) {
}
/**
* 环绕通知, 围绕着方法执行
* @param joinPoint 连接点
* @param repeatSubmit 重复提交注解
* @return 结果集
* @throws Throwable 异常处理
*/
@Around(value = "pointNoRepeatSubmit(repeatSubmit)", argNames = "joinPoint,repeatSubmit")
public Object repeatSubmit(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
logger.info("-----------防止重复提交开始----------");
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
// 这里是唯一标识 根据情况而定
StringBuilder key = new StringBuilder();
// 防重提交类型
String limitType = repeatSubmit.limitType().name();
// 根据防重提交类型处理 默认防重提交是方法参数
if (limitType.equalsIgnoreCase(RepeatSubmit.Type.PARAM.name())) {
// 获取用户电脑信息
key.append(request.getHeader(CommonConstant.USER_AGENT));
// 获取请求IP地址
key.append(IpAdrressUtil.getIpAdrress(request));
key.append("-");
// 获取请求Url
key.append(request.getRequestURI());
} else {
// 获取请求里的token
String authorization = request.getHeader(CommonConstant.AUTHORIZATION);
key.append(authorization);
key.append("-");
// 获取请求Url
key.append(request.getRequestURI());
}
logger.info("防止重复提交Key:{}",key.toString());
// 如果缓存中有这个IP地址,URL视为重复提交
if (!redisUtils.hasKey(key.toString())) {
logger.info("防止重复提交设置中,{} 秒内不可反复提交",repeatSubmit.lockTime());
//通过,执行下一步
Object o = joinPoint.proceed();
//然后存入redis 并且设置5s倒计时
redisUtils.set(key.toString(), key.toString(), repeatSubmit.lockTime());
logger.info("----------防止重复提交设置结束----------");
//返回结果
return o;
}
String repeatMsg = "请勿重复提交或者操作过于频繁! 请在" +repeatSubmit.lockTime() + "秒后重试";
logger.info(repeatMsg);
return ApiResult.error(repeatMsg,
CommonConstant.SC_INTERNAL_SERVER_ERROR_500);
}
}
- 获取IP地址工具类 IpAdrressUtil
package com.base.sbc.config.utils;
import javax.servlet.http.HttpServletRequest;
/**
* 获取IP地址工具类
* @author lizan
* @date 2023-03-13 16:16
*/
public class IpAdrressUtil {
/**
* 获取Ip地址
* @param request
* @return
*/
public static String getIpAdrress(HttpServletRequest request) {
String xip = request.getHeader("X-Real-IP");
String xFor = request.getHeader("X-Forwarded-For");
if(StringUtils.isNotEmpty(xFor) && !"unKnown".equalsIgnoreCase(xFor)){
//多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = xFor.indexOf(",");
if(index != -1){
return xFor.substring(0,index);
}else{
return xFor;
}
}
xFor = xip;
if(StringUtils.isNotEmpty(xFor) && !"unKnown".equalsIgnoreCase(xFor)){
return xFor;
}
if (StringUtils.isBlank(xFor) || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(xFor) || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(xFor) || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isBlank(xFor) || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isBlank(xFor) || "unknown".equalsIgnoreCase(xFor)) {
xFor = request.getRemoteAddr();
}
return xFor;
}
}
- 在访问接口中添加 @RepeatSubmit ,并进行测试
@GetMapping("/test")
@RepeatSubmit(lockTime = 3L, limitType = RepeatSubmit.Type.PARAM)
public ApiResult test(@RequestParam("userCompany") String userCompany,
@RequestParam("id") String id) throws Exception {
return selectSuccess("访问成功");
}
- 结果
第一次访问结果
第二次访问