自定义注解 RepeatSubmit
, 用于防止表单重复提交
package com.goodsoft.shrk.common.annotation;
import java.lang.annotation.*;
/**
* @author YXY
* 自定义注解防止表单重复提交
*
* @Inherited
* 1.类继承关系中@Inherited的作用:类继承关系中,子类会继承父类使用的注解中被@Inherited修饰的注解
* 2.接口继承关系中@Inherited的作用:接口继承关系中,子接口不会继承父接口中的任何注解,不管父接口中使用的注解有没有被@Inherited修饰
* 3.类实现接口关系中@Inherited的作用:类实现接口时不会继承任何接口中定义的注解
* @Documented
* 标注生成javadoc的时候是否会被记录。
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
}
使用方式:
在需要拦截的接口上添加 @RepeatSubmit()
注解
自定义拦截器
package com.goodsoft.shrk.common.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.goodsoft.shrk.common.annotation.RepeatSubmit;
import com.goodsoft.shrk.common.dto.ReturnData;
import com.goodsoft.shrk.common.util.ServletUtils;
//import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* @author YXY
* 防止重复提交拦截器
*/
@Component
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter
{
/**
* preHandle方法都会在Controller方法调用之前执行,当preHandler中返回值为false时整个请求结束
* postHandle方法 在请求完成后执行,会在DispatcherServlet进行视图的渲染之前执行
* afterCompletion方法将在整个请求完成之后,也就是DispatcherServlet渲染了视图执行
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
if (handler instanceof HandlerMethod)
{
// HandlerMethod 封装 方法参数,方法返回值和方法注释
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
if (annotation != null)
{
if (this.isRepeatSubmit(request))
{
ReturnData result = ReturnData.error("不允许重复提交,请稍后再试");
//将字符串渲染到客户端
ServletUtils.renderString(response, JSONObject.toJSONString(result));
return false;
}
}
return true;
}
else
{
return super.preHandle(request, response, handler);
}
}
/**
* 验证是否重复提交由子类实现具体的防重复提交的规则
* @param request
* @return
* @throws Exception
*/
public abstract boolean isRepeatSubmit(HttpServletRequest request);
}
这里有个小问题,就是HandlerMethod的导包问题,
如果导入的是import org.springframework.messaging.handler.HandlerMethod;
这个包下的,
会导致 handler instanceof HandlerMethod
一直为false ,
应该导入:import org.springframework.web.method.HandlerMethod;
这里的 ReturnData
是我们项目封装的统一返回数据结果,需要替换为自己项目的
package com.goodsoft.shrk.common.interceptor;
import com.alibaba.fastjson.JSONObject;
import com.goodsoft.shrk.common.constant.Constants;
import com.goodsoft.shrk.common.util.HttpHelper;
import com.goodsoft.shrk.common.util.StringUtils;
import com.goodsoft.shrk.component.cache.CommonCache;
import com.goodsoft.shrk.component.filter.RepeatedlyRequestWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author YXY
* 判断请求url和数据是否和上一次相同,
* 如果和上次相同,则是重复提交表单。 有效时间为10秒内
*/
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
public final String REPEAT_PARAMS = "repeatParams";
public final String REPEAT_TIME = "repeatTime";
@Autowired
private CommonCache commonCache;
/**
* 间隔时间,单位:秒 默认10秒
*
* 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
*/
private int intervalTime = 10;
public void setIntervalTime(int intervalTime)
{
this.intervalTime = intervalTime;
}
/**
* @SuppressWarnings("unchecked"): 告诉编译器忽略 unchecked 警告信息
* @param request
* @return
*/
@SuppressWarnings("unchecked")
@Override
public boolean isRepeatSubmit(HttpServletRequest request)
{
String nowParams = "";
if (request instanceof RepeatedlyRequestWrapper)
{
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = HttpHelper.getBodyString(repeatedlyRequest);
}
// body参数为空,获取Parameter的数据
if (StringUtils.isEmpty(nowParams))
{
nowParams = JSONObject.toJSONString(request.getParameterMap());
}
Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
// 请求地址(作为存放cache的key值)
String url = request.getRequestURI();
// 唯一值(没有消息头则使用请求地址)
String submitKey = request.getHeader( "token");
if (StringUtils.isEmpty(submitKey))
{
submitKey = url;
}
// 唯一标识(指定key + 消息头)
String cacheRepeatKey = Constants.REPEAT_SUBMIT_KEY + submitKey;
Object sessionObj = commonCache.getVal(cacheRepeatKey);
if (sessionObj != null)
{
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url))
{
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap))
{
return true;
}
}
}
Map<String, Object> cacheMap = new HashMap<String, Object>();
cacheMap.put(url, nowDataMap);
commonCache.set(cacheRepeatKey, cacheMap, intervalTime, TimeUnit.SECONDS);
return false;
}
/**
* 判断参数是否相同
*/
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
{
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams);
}
/**
* 判断两次间隔时间
*/
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap)
{
long time1 = (Long) nowMap.get(REPEAT_TIME);
long time2 = (Long) preMap.get(REPEAT_TIME);
if ((time1 - time2) < (this.intervalTime * 1000))
{
return true;
}
return false;
}
}
CommonCache
是操作redis缓存用 ,这里替换成自己系统中的操作redis存取方法即可
commonCache.getVal()
commonCache.set()
/**
* 根据key获取缓存中的val
*
* @param key 键 不能为null
* @return 缓存中的值
*/
public <T> T getVal(String key) {
return (T) redisTemplate.opsForValue().get(key);
}
/**
* 缓存放入并设置过期时间
*
* @param key 键
* @param value 值
* @param time 时间(毫秒) time要大于0 如果time小于等于0 将设置无限期
* @param unit 默认毫秒
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time, TimeUnit unit) {
try {
if (time > 0 && unit != null) {
redisTemplate.opsForValue().set(key, value, time, unit);
return true;
}
} catch (Exception e) {
LOG.error("redis缓存放入并设置过期时间", e);
}
return false;
}
HttpHelper工具类
package com.goodsoft.shrk.common.util;
import javax.servlet.ServletRequest;
import java.io.*;
import java.nio.charset.Charset;
/**
* @author YXY
*/
public class HttpHelper {
/** * 获取post请求中的Body * * @param request * @return */
public static String getBodyString(ServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
//读取流并将流写出去,避免数据流中断;
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
//添加自定义的信息到请求体中;
public static String appendCustomMsgToReqBody(String newReqBodyStr) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
String newReqBody = null;
try {
//通过字符串构造输入流;
inputStream = String2InputStream(newReqBodyStr);
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//返回字符串;
newReqBody = sb.toString();
return newReqBody;
}
//将字符串转化为输入流;
public static InputStream String2InputStream(String str) {
ByteArrayInputStream stream = null;
try {
stream = new ByteArrayInputStream(str.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return stream;
}
}
Constants 常量
package com.goodsoft.shrk.common.constant;
/**
* 通用常量信息
*
* @author ruoyi
*/
public class Constants
{
/**
* UTF-8 字符集
*/
public static final String UTF8 = "UTF-8";
/**
* GBK 字符集
*/
public static final String GBK = "GBK";
/**
* 防重提交 redis key
*/
public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";
/**
* 限流 redis key
*/
public static final String RATE_LIMIT_KEY = "rate_limit:";
}
RepeatedlyRequestWrapper过滤器
package com.goodsoft.shrk.component.filter;
import cn.hutool.core.io.IoUtil;
import com.goodsoft.shrk.common.constant.Constants;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
/**
* 构建可重复读取inputStream的request
*
* @author ruoyi
*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
super(request);
request.setCharacterEncoding(Constants.UTF8);
response.setCharacterEncoding(Constants.UTF8);
body = IoUtil.readUtf8(request.getInputStream()).getBytes(StandardCharsets.UTF_8);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public int available() throws IOException {
return body.length;
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
这里需要导入 hutool
工具包
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.6</version>
</dependency>
注册RepeatSubmitInterceptor
拦截器,需要继承WebMvcConfigurerAdapter
类
需要重写addInterceptors
方法,这里是对根目录"/"
进行拦截,可以指定拦截url请求目录
package com.goodsoft.shrk.common.config;
import com.goodsoft.shrk.common.interceptor.RepeatSubmitInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@Slf4j
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
}
}