在日常业务中经常会出现短时间内重复点击提交按钮场景,例如抢票,电商秒杀,各种投票,薅羊毛活动
如果不做表单重复提交过滤,服务器压力很大,还有就是会存在安全问题,导致网站被薅羊毛
提交后按钮禁用,置灰,页面出现遮罩
使用表单提交专属 token,每个 token 只能使用一次
/**
* 防止重复提交注解
* @author oscar
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiRepeatSubmit {
ConstantUtils value();
}
/**
* 生成token注解
* @author oscar
*/
@Target(value = ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiToken {
}
/**
* 定义从哪里取Token的枚举类
* @author oscar
*/
public enum ConstantUtils {
/**
* 从请求体中取token
*/
BOOD,
/**
* 从请求头中取token
*/
HEAD
}
/**
* 响应码枚举类
* @author oscar
*/
public enum ResponseCode {
SUCCESS("000000", "成功"),
// 通用模块 1xxxx
ILLEGAL_ARGUMENT("100000", "参数不合法"),
REPETITIVE_OPERATION("100001", "请勿重复操作"),
;
ResponseCode(String code, String msg) {
this.code = code;
this.msg = msg;
}
private String code;
private String msg;
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
/**
* 重复提交表单拦截器
* @author oscar
*/
@Aspect
@Component
@Slf4j
public class NoRepeatSubmitAop {
/**
* 表单提交 token 字段名
*/
private static final String FORM_TOKEN_NAME = "formToken";
@Autowired
private RedisTokenUtils redisTokenUtils;
/**
* 将token放入请求
*
* @param pjp
* @param nrs
*/
@Before("execution(* com.zhunongyun.toalibaba.commoncode.controller.*Controller.*(..)) && @annotation(nrs)")
public void before(JoinPoint pjp, ApiToken nrs) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
response.setHeader(FORM_TOKEN_NAME, redisTokenUtils.getToken());
}
/**
* 拦截带有重复请求的注解的方法
*
* @param pjp
* @param nrs
*/
@Around("execution(* com.zhunongyun.toalibaba.commoncode.controller.*Controller.*(..)) && @annotation(nrs)")
public Object arround(ProceedingJoinPoint pjp, ApiRepeatSubmit nrs) {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String token = null;
if (nrs.value() == ConstantUtils.BOOD) {
//从 参数 中取Token
token = (String) request.getAttribute(FORM_TOKEN_NAME);
} else if (nrs.value() == ConstantUtils.HEAD) {
//从 请求头 中取Token
token = request.getHeader(FORM_TOKEN_NAME);
}
if (StringUtils.isEmpty(token)) {
log.error("获取token失败");
return ResponseVO.fail(ResponseCode.REPETITIVE_OPERATION, "获取token失败");
}
if (!redisTokenUtils.findToken(token)) {
log.error("重复提交");
return ResponseVO.fail(ResponseCode.REPETITIVE_OPERATION, "重复提交");
}
log.error("正常提交表单");
Object o = pjp.proceed();
return o;
} catch (Throwable e) {
log.error("验证重复提交时出现未知异常:{}", e);
return ResponseVO.fail(ResponseCode.REPETITIVE_OPERATION, "验证重复提交时出现未知异常");
}
}
}
@Data
public class ResponseVO {
private String code;
private String msg;
private Object data;
private ResponseVO() {
}
private ResponseVO(String code, String msg, Object data) {
this.code = code;
this.msg = msg;
this.data = data;
}
private ResponseVO(String code, String msg) {
new ResponseVO(code, msg, null);
}
public static ResponseVO success() {
return new ResponseVO(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg());
}
public static ResponseVO success(Object data) {
return new ResponseVO(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg(), data);
}
public static ResponseVO fail(ResponseCode responseCode, Object data) {
return new ResponseVO(responseCode.getCode(), responseCode.getMsg(), data);
}
public static ResponseVO fail(String code, String msg, Object data) {
return new ResponseVO(code, msg, data);
}
public static ResponseVO fail(String code, String msg) {
return fail(code, msg, null);
}
}
/**
* 解决表单重复提交
* @author oscar
*/
@RestController
@RequestMapping("form")
public class FormRepeatSubmissionController {
/**
* 进入页面
* @return
*/
@ApiToken
@GetMapping("index")
public ResponseVO index(){
return ResponseVO.success();
}
/**
* 提交表单
* @param data
* @return
*/
@ApiRepeatSubmit(ConstantUtils.HEAD)
@PostMapping("add")
public ResponseVO saveData(@RequestBody String data) {
return ResponseVO.success(data);
}
}
获取表单提交需要的 token
第一次提交表单
再次提交表单
使用 JMeter 压测结果正常