form表单防止重复提交
4种方案:1、js屏蔽提交按钮(只可限制按钮重复点击)
2、利用Session防止表单重复提交(需配置session分布式存储)
3、使用AOP自定义切入实现(限制了访问频率)
4、数据库增加唯一约束(简单粗暴)
5、利用token防止表单重复提交(目前最佳)1、js屏蔽提交按钮
实现:<script type="text/javascript">
//默认提交状态为false
var commitStatus = false;
function dosubmit(){
if(commitStatus==false){
//提交表单后,讲提交状态改为true
commitStatus = true;
return true;
}else{
return false;
}
}
</script>
<body>
<form action="/path/post" onsubmit="return dosubmit()" method="post">
用户名:<input type="text" name="username">
<input type="submit" value="提交" id="submit">
</form>
</body>2、利用Session防止表单重复提交实现原理:
1、请求页面controller加注解@FormToken(saveToken = true)
2、页面请求时拦截器生成formToken,写入session
3、页面添加<input type="hidden" id="formToken" name="formToken" value="${formToken}">
4、提交请求传递formToken
5、对需要防止重复提交的controller加注解@FormToken(removeToken = true)
6、提交请求时拦截器,若非重复提交,移除session中的formTokenFormToken注解:@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FormToken {
boolean saveToken() default false;
boolean removeToken() default false;
}防重复提交拦截器实现:public class NoRepeatCommitInterceptor extends HandlerInterceptorAdapter {
public NoRepeatCommitInterceptor() {
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
FormToken annotation = method
.getAnnotation(FormToken.class);
if (annotation != null) {
boolean needSaveSession = annotation.saveToken();
if (needSaveSession) {
request.getSession()
.setAttribute(
"formToken",
UUID.randomUUID().toString());
}
boolean needRemoveSession = annotation.removeToken();
if (needRemoveSession) {
if (isRepeatSubmit(request)) {
throw new BizException(ResultEnum.FAIL.getCode(), "不可重复提交,请稍后重试");
}
request.getSession(false).removeAttribute("formToken");
}
}
}
return true;
}
private boolean isRepeatSubmit(HttpServletRequest request) {
String serverToken = (String) request.getSession(false).getAttribute(
"formToken");
if (serverToken == null) {
return true;
}
String clientToken = request.getParameter("formToken");
if (clientToken == null) {
return true;
}
if (!serverToken.equals(clientToken)) {
return true;
}
return false;
}
}3、使用AOP自定义切入实现实现原理:
1、对需要防止重复提交的Controller添加注解@NoRepeatCommit,可设置过期时间
2、提交请求时切面判断:redis setNx key,失败则拦截
key=ip_className_methodName
3、实际是限流:30秒内只允许1次请求NoRepeatCommit注解:@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatCommit {
/**
* 指定时间内不可重复提交,单位秒
*
* @return
*/
int timeout() default 30;
}防重复提交AOP实现:@Aspect
@Component
public class NoRepeatCommitAspect {
private static final Logger log = LoggerFactory.getLogger(NoRepeatCommitAspect.class);
@Resource
private RedisClient adminRedisClient;
@Pointcut("@annotation(com.xxx.util.NoRepeatCommit)")
public void pointcut() {
}
/**
* @param point
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
log.info("进入防止重复提交切面..........");
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String ip = ServletUtil.getIp(request);
//获取注解
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
//目标类、方法
String className = method.getDeclaringClass().getName();
String name = method.getName();
String ipKey = String.format("%s#%s", className, name);
int hashCode = Math.abs(ipKey.hashCode());
String key = String.format("%s_%d", ip, hashCode);
log.info("ipKey={},hashCode={},key={}", ipKey, hashCode, key);
NoRepeatCommit noRepeatCommit = method.getAnnotation(NoRepeatCommit.class);
int timeout = noRepeatCommit.timeout();
if (timeout < 0) {
//过期时间30秒
timeout = 30;
}
final int expire = timeout;
boolean acquireResult = adminRedisClient.execute(key, new JedisAction<Boolean>() {
@Override
public Boolean action(Jedis jedis) {
try {
String setNxResult = jedis.set(key, UUID.randomUUID().toString(), "NX", "EX", expire);
if ("OK".equals(setNxResult)) {
return true;
}
} catch (Exception e) {
log.error("acquireResult error", e);
}
return false;
}
});
if (!acquireResult) {
throw new BizException(ResultEnum.FAIL, "请勿重复提交");
}
//执行方法
Object object = point.proceed();
return object;
}
}4、数据库增加唯一约束
5、利用token防止表单重复提交实现原理
1、请求页面controller加注解@CreateFormToken(timeout = 60*60,keyPrefix = "SEND_MARKET_SMS")
2、页面请求时拦截器生成formToken,写入request.setAttribute("formToken", token);
3、页面添加<input type="hidden" id="formToken" name="formToken" value="${formToken!}">
4、提交请求传递formToken
5、对需要防止重复提交的controller加注解@ConsumeFormToken
6、提交请求时拦截器,先检查formToken是否传递,若无则提示参数错误,再校验redis是否存在key=formToken,若存在则删除,不存在则提醒重复提交
7、若防重复提交业务失败,需恢复formToken(实现ResponseBodyAdvice,拦截带ConsumeFormToken注解的请求,校验返回结果,如失败重写formToken)FormToken注解:@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CreateFormToken {
//token过期时间,单位秒
int timeout() default 3600;
//token自定义前缀
String keyPrefix() default "FORM_TOKEN_PREFIX_";
}
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConsumeFormToken {
}拦截器实现:/**
* 防重复提交拦截器
**/
public class NoRepeatCommitInterceptor extends HandlerInterceptorAdapter {
private static final Logger log = LoggerFactory.getLogger(NoRepeatCommitInterceptor.class);
@Resource
private RedisClient adminRedisClient;
public NoRepeatCommitInterceptor() {
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//创建formToken
CreateFormToken createAnnotation = method
.getAnnotation(CreateFormToken.class);
if (createAnnotation != null) {
//使用UUID以保证唯一
String tokenKey = UUID.randomUUID().toString();
String keyPrefix = createAnnotation.keyPrefix();
if (StringUtils.isNotEmpty(keyPrefix)) {
tokenKey = keyPrefix + tokenKey;
}
int timeout = createAnnotation.timeout();
if (timeout <= 0) {
//过期时间3600秒
timeout = CommonConstant.FORM_TOKEN_DEFAULT_SECOND;
}
String token = tokenKey;
int expire = timeout;
boolean acquireResult = adminRedisClient.execute(token, new JedisAction<Boolean>() {
@Override
public Boolean action(Jedis jedis) {
try {
String setNxResult = jedis.set(token, token, "NX", "EX", expire);
if (CommonConstant.OK.equals(setNxResult)) {
request.setAttribute(CommonConstant.FORM_TOKEN_NAME, token);
return true;
}
} catch (Exception e) {
log.error("acquireResult error", e);
}
return false;
}
});
return acquireResult;
}
//消费formToken
ConsumeFormToken consumeAnnotation = method
.getAnnotation(ConsumeFormToken.class);
if (consumeAnnotation != null) {
String clientToken = request.getParameter(CommonConstant.FORM_TOKEN_NAME);
if (StringUtils.isEmpty(clientToken)) {
throw new BizException(ResultEnum.FAIL.getCode(), "请求参数不合法,formToken不存在");
}
if (adminRedisClient.del(clientToken) <= 0) {
throw new BizException(ResultEnum.FAIL.getCode(), "不可重复提交,请稍后重试");
}
}
}
return true;
}
}恢复formToken实现:
**
* 恢复formToken
**/
@ControllerAdvice
public class FormTokenResponseBodyAdvice implements ResponseBodyAdvice {
private static final Logger log = LoggerFactory.getLogger(NoRepeatCommitInterceptor.class);
@Resource
private RedisClient adminRedisClient;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
Method method = returnType.getMethod();
//消费formToken
ConsumeFormToken consumeAnnotation = method
.getAnnotation(ConsumeFormToken.class);
if (consumeAnnotation != null) {
return true;
}
return false;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
AjaxResult result = (AjaxResult) body;
//返回值成功不处理
if (result.getCode() == ResultEnum.SUCCESS.getCode()) {
return body;
}
//通过ServerHttpRequest的实现类ServletServerHttpRequest 获得HttpServletRequest
ServletServerHttpRequest sshr = (ServletServerHttpRequest) request;
HttpServletRequest servletRequest = sshr.getServletRequest();
String formToken = servletRequest.getParameter(CommonConstant.FORM_TOKEN_NAME);
//无formToken不处理
if (StringUtils.isEmpty(formToken)) {
return body;
}
//恢复formToken
adminRedisClient.execute(formToken, new JedisAction<Boolean>() {
@Override
public Boolean action(Jedis jedis) {
try {
String setNxResult = jedis.set(formToken, formToken, "NX", "EX", CommonConstant.FORM_TOKEN_DEFAULT_SECOND);
if (CommonConstant.OK.equals(setNxResult)) {
return true;
}
} catch (Exception e) {
log.error("recoveryResult error", e);
}
return false;
}
});
return body;
}
}
向上吧,少年
















