ruoyi-vue-plus RepeatSubmit源码解读 实现防重复提交
查询和删除是天然的幂等操作,故一般不建议使用。在有更新和写入的操作时,建议使用,例如订单提交,修改订单状态。幂等性: 多次执行的结果和一次执行成功的结果对资源的作用是相同的,这里的资源一般指的是数据库的数据。同一个用户的统一操作, 在规定的时间内 只能做一次相同数据的请求,防止重复提交,超时重试等一些问题。interval 间隔时间 默认单位是msmessage为默认返回信息。使用 拦截器 + 过
·
RepeatSubmit作用
同一个用户的统一操作, 在规定的时间内 只能做一次相同数据的请求,防止重复提交,超时重试等一些问题
RepeatSubmit使用注意事项
查询和删除是天然的幂等操作,故一般不建议使用。在有更新和写入的操作时,建议使用,例如订单提交,修改订单状态。
幂等性: 多次执行的结果和一次执行成功的结果对资源的作用是相同的,这里的资源一般指的是数据库的数据。
使用
interval 间隔时间 默认单位是ms message 为默认返回信息
@RepeatSubmit(interval = 10000,message = "不能重复提交")
解决方案
程序控制和资源(数据)控制
ruoyi 脚手架的实现
使用 拦截器 + 过滤器 实现的
- 拦截器 : 对 带有RepeatSubmit 接口进行重复性校验,将 ( key标识 + 请求url + base64(用户请求数据)) 作为key,时间戳 + 请求数据 组成map 作为value, 当然了 key 可以自行进行修改 。
- 过滤器: 对请求进行过滤,将请求转化为可重复读取inputStream的request。
具体代码如下(本人已修改):
package com.example.interceptor;
import java.lang.annotation.*;
/**
* 自定义注解防止表单重复提交
* 建议查询和删除不要使用此注解。
* @author ruoyi
*
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
public int interval() default 5000;
/**
* 提示消息
*/
public String message() default "不允许重复提交,请稍候再试";
}
- 定义拦截器
package com.example.interceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* 防止重复提交拦截器
*
* @author ruoyi
*/
@Component
@Slf4j
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
if (handler instanceof HandlerMethod)
{
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
if (annotation != null)
{
if (this.isRepeatSubmit(request, annotation))
{
log.info("重复提交了!!!可自行定义返回格式");
// 重复提交了
// AjaxResult ajaxResult = AjaxResult.error(annotation.message());
// ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
return false;
}
}
return true;
}
else
{
return true;
}
}
/**
* 验证是否重复提交由子类实现具体的防重复提交的规则
*
* @param request 请求信息
* @param annotation 防重复注解参数
* @return 结果
* @throws Exception
*/
public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}
- 具体实现机制
可自定义key
package com.example.interceptor.impl;
import com.alibaba.fastjson2.JSON;
import com.example.interceptor.RepeatSubmit;
import com.example.interceptor.RepeatSubmitInterceptor;
import com.example.interceptor.RepeatedlyRequestWrapper;
import com.example.util.redis.RedisCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import sun.misc.BASE64Encoder;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 判断请求url和数据是否和上一次相同,
* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
*
* @author ruoyi
*/
@Slf4j
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
public final String REPEAT_PARAMS = "repeatParams";
public final String REPEAT_TIME = "repeatTime";
/**
* 防重提交 redis key
*/
public final String REPEAT_SUBMIT_KEY = "repeat_submit:";
// 令牌自定义标识
@Value("${token.header}")
private String header;
@Autowired
private RedisCache redisCache;
@SuppressWarnings("unchecked")
@Override
public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation)
{
BASE64Encoder encoder = new BASE64Encoder();
String nowParams = "";
if (request instanceof RepeatedlyRequestWrapper)
{
// 注意 一定要使用过滤器 将 request 转换为 RepeatedlyRequestWrapper 不然无法获取请求参数
RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
nowParams = getBodyString(repeatedlyRequest);
}
// body参数为空,获取Parameter的数据 nowParams
if ("".equals(nowParams.trim()))
{
nowParams = JSON.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 str = request.getHeader(header);
// 唯一值(没有消息头则使用请求地址)
String submitKey = str == null ? "" : str.trim();
// 可以获取 不同的 IP地址 或者 mac地址 进行 主机识别 加上此注解的 通过id查询可能会受影响 所以建议查询和删除不要使用此注解即可。
// 使用 BASE64 对 submitKey 和 请求数据 加密
String dataEncryption = encoder.encode((submitKey+nowParams).getBytes(StandardCharsets.UTF_8));
// 唯一标识(指定key + url + 请求数据 )
String cacheRepeatKey = REPEAT_SUBMIT_KEY + url + dataEncryption;
Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
// 判断 redis 中 有无重复的key
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, annotation.interval()))
{
return true;
}
}
}
Map<String, Object> cacheMap = new HashMap<String, Object>();
cacheMap.put(url, nowDataMap);
redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
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, int interval)
{
long time1 = (Long) nowMap.get(REPEAT_TIME);
long time2 = (Long) preMap.get(REPEAT_TIME);
if ((time1 - time2) < interval)
{
return true;
}
return false;
}
public String getBodyString(ServletRequest request)
{
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try (InputStream inputStream = request.getInputStream())
{
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line = "";
while ((line = reader.readLine()) != null)
{
sb.append(line);
}
}
catch (IOException e)
{
log.warn("getBodyString出现问题!");
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (IOException e)
{
log.error("关闭文件错误");
}
}
}
return sb.toString();
}
}
- 可重复读取inputStream的request
package com.example.interceptor;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* 构建可重复读取inputStream的request
*
* @author ruoyi
*/
@Slf4j
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException
{
super(request);
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
body = getBodyString(request).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)
{
}
};
}
public String getBodyString(ServletRequest request)
{
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try (InputStream inputStream = request.getInputStream())
{
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line = "";
while ((line = reader.readLine()) != null)
{
sb.append(line);
}
}
catch (IOException e)
{
log.warn("getBodyString出现问题!");
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (IOException e)
{
log.error("关闭文件错误");
}
}
}
return sb.toString();
}
}
- 将拦截器注册进入系统配置中
package com.example.interceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 通用配置
*
* @author ruoyi
*/
@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;
/**
* 自定义拦截规则
*/
@Override
public void addInterceptors(InterceptorRegistry registry)
{
// 把重复校验添加到系统配置中
// 重复校验拦截所有请求 自行判断是否重复校验 有注解则校验 没有则不校验
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
}
/**
* 跨域配置
*/
@Bean
public CorsFilter corsFilter()
{
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置访问源地址
config.addAllowedOriginPattern("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 有效期 1800秒
config.setMaxAge(1800L);
// 添加映射路径,拦截一切请求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 返回新的CorsFilter
return new CorsFilter(source);
}
}
- 定义过滤器
package com.example.filter;
import com.example.interceptor.RepeatedlyRequestWrapper;
import org.springframework.http.MediaType;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Repeatable 过滤器
*
* @author ruoyi
*/
public class RepeatableFilter implements Filter
{
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
ServletRequest requestWrapper = null;
if (request instanceof HttpServletRequest
&& startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE))
{
requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
}
if (null == requestWrapper)
{
chain.doFilter(request, response);
}
else
{
chain.doFilter(requestWrapper, response);
}
}
public static boolean startsWithIgnoreCase(CharSequence str, CharSequence prefix) {
return startsWith(str, prefix);
}
private static boolean startsWith(CharSequence str, CharSequence prefix) {
if (str != null && prefix != null) {
int preLen = prefix.length();
return preLen <= str.length() && regionMatches(str, prefix, preLen);
} else {
return str == prefix;
}
}
static boolean regionMatches(CharSequence cs, CharSequence substring, int length) {
if (cs instanceof String && substring instanceof String) {
return ((String)cs).regionMatches(true, 0, (String)substring, 0, length);
} else {
int index1 = 0;
int index2 = 0;
int tmpLen = length;
int srcLen = cs.length();
int otherLen = substring.length();
if (length >= 0) {
if (srcLen >= length && otherLen >= length) {
while(tmpLen-- > 0) {
char c1 = cs.charAt(index1++);
char c2 = substring.charAt(index2++);
if (c1 != c2) {
char u1 = Character.toUpperCase(c1);
char u2 = Character.toUpperCase(c2);
if (u1 != u2 && Character.toLowerCase(u1) != Character.toLowerCase(u2)) {
return false;
}
}
}
return true;
} else {
return false;
}
} else {
return false;
}
}
}
}
- 将过滤器注册进入系统中
package com.example.filter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Filter配置
*
* @author ruoyi
*/
@Configuration
public class FilterConfig
{
@SuppressWarnings({ "rawtypes", "unchecked" })
@Bean
public FilterRegistrationBean someFilterRegistration()
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RepeatableFilter());
registration.addUrlPatterns("/*");
registration.setName("repeatableFilter");
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
return registration;
}
}
更多推荐
已为社区贡献1条内容
所有评论(0)