redis + 拦截器 :防止数据重复提交
1.项目用到,不是核心
我们干系统开发,不免要考虑一个点,数据的重复提交。
我想我们之前如果要校验数据重复提交要求,会怎么干?会在业务层,对数据库操作,查询数据是否存在,存在就禁止插入数据; 但是吧,我们每次crud操作都会连接一次数据库,也就是占用内存,那么在项目中大量crud操作面前,我们通过这种方式来实现数据的重复提交,显然不大可取。因此我们采用通过 redis + 拦截器来实现防止数据重复提交。来分担数据库连接的压力。
数据重复提交有啥坏处?
- 数据完整性:如果用户在短时间内多次提交相同的表单,可能会导致数据重复或产生不一致的数据。
- 用户体验:如果用户不小心重复提交了表单,而系统没有进行相应的处理,用户可能会收到错误或重复的信息,这会影响用户体验。
- 性能考虑:大量的重复提交可能会对服务器造成不必要的负担,影响系统的性能。
- 安全考虑:在某些场景下,重复提交可能会被用于发起攻击,如DoS攻击。
我们要考虑一个事情,就是我们要验证数据的重复提交: 首先第一次提交的数据肯定是要被存储的,当而第二次往后,每次提交数据都会与之前的数据产生比对从而验证数据重复提交,但是通常情况下我们不仅要对提交数据重复性校验,还有前后提交时间差的校验。
下面,就有我通过redis + 拦截器来实现如何防止数据重复提交。
思路: 我们对需要验证重复提交的数据,加上自定义注解限制提交时间段,然后在拦截器中读取第一次提交内容和时间点存储到redi中,当第二次提交时,会拿到新的数据和时间点与存储到redis对比。如果提交2次时间段小于限制提交时间段(拦截器拿到自定义注解的值),就算重复提交。
项目依赖
<dependencies><!--boot-web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.11</version><exclusions><exclusion><groupId>cn.hutool</groupId><artifactId>hutool-json</artifactId></exclusion></exclusions></dependency><!--fastJson2--><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.19.graal</version></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--commons-pools连接池,lettuce没有内置的数据库连接池所以要用第三方的 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><!--boot-test--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.6.13</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
application.yml
主要是redis的配置。
spring:# redis 配置redis:# 地址host: 192.168.233.131# 端口,默认为6379port: 6379# 数据库索引database: 0# 密码password:# 连接超时时间timeout: 10slettuce:pool:# 连接池中的最小空闲连接min-idle: 0# 连接池中的最大空闲连接max-idle: 8# 连接池的最大数据库连接数max-active: 8# #连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms
FastJson2JsonRedisSerializer
主要负责对存入redis的key、value进行序列化。
/*** Redis使用FastJson序列化** @author jzm*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);private Class<T> clazz;public FastJson2JsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null){return new byte[0];}return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes == null || bytes.length <= 0){return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);}
}
RedisConfig
redis相关配置。定义通过redisTemplate,设置到redis中的key、value的序列化方式。
/*** redis配置** @author jzm*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{@Bean@SuppressWarnings(value = {"unchecked", "rawtypes"})// 设置key、value的序列化方式public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}@Beanpublic DefaultRedisScript<Long> limitScript(){DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(limitScriptText());redisScript.setResultType(Long.class);return redisScript;}/*** 限流脚本*/private String limitScriptText(){return "local key = KEYS[1]\n" +"local count = tonumber(ARGV[1])\n" +"local time = tonumber(ARGV[2])\n" +"local current = redis.call('get', key);\n" +"if current and tonumber(current) > count then\n" +" return tonumber(current);\n" +"end\n" +"current = redis.call('incr', key)\n" +"if tonumber(current) == 1 then\n" +" redis.call('expire', key, time)\n" +"end\n" +"return tonumber(current);";}
}
WebAppConfig
web mvc的相关配置。这里主要是注册自定义拦截器。
/*** web 配置** @author: jzm* @date: 2024-01-25 11:30**/@Configuration
public class WebAppConfig implements WebMvcConfigurer
{@Autowiredprivate SameUrlDataInterceptor sameUrlDataInterceptor;// 注册拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry){// 可添加多个registry.addInterceptor(sameUrlDataInterceptor).addPathPatterns("/**");}
}
FilterConfig
过滤器配置。注册自定义过滤器。
/*** 过滤器配置** @author: jzm* @date: 2024-01-26 08:53**/@Configuration
public class FilterConfig
{@Autowiredprivate RepeatableFilter repeatableFilter;@SuppressWarnings({"rawtypes", "unchecked"})@Beanpublic FilterRegistrationBean someFilterRegistration(){FilterRegistrationBean registration = new FilterRegistrationBean();registration.setFilter(repeatableFilter);registration.addUrlPatterns("/*");registration.setName("repeatableFilter");registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);return registration;}
}
项目用到的常量类
常量类。直接CV。
/*** 缓存的key 常量** @author jzm*/
public class CacheConstants
{/*** 登录用户 redis key*/public static final String LOGIN_TOKEN_KEY = "login_tokens:";/*** 验证码 redis key*/public static final String CAPTCHA_CODE_KEY = "captcha_codes:";/*** 参数管理 cache key*/public static final String SYS_CONFIG_KEY = "sys_config:";/*** 字典管理 cache key*/public static final String SYS_DICT_KEY = "sys_dict:";/*** 防重提交 redis key*/public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";/*** 限流 redis key*/public static final String RATE_LIMIT_KEY = "rate_limit:";/*** 登录账户密码错误次数 redis key*/public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
}
/*** 通用常量信息** @author jzm*/
public class Constants
{/*** UTF-8 字符集*/public static final String UTF8 = "UTF-8";/*** GBK 字符集*/public static final String GBK = "GBK";/*** www主域*/public static final String WWW = "www.";/*** http请求*/public static final String HTTP = "http://";/*** https请求*/public static final String HTTPS = "https://";/*** 通用成功标识*/public static final String SUCCESS = "0";/*** 通用失败标识*/public static final String FAIL = "1";/*** 登录成功*/public static final String LOGIN_SUCCESS = "Success";/*** 注销*/public static final String LOGOUT = "Logout";/*** 注册*/public static final String REGISTER = "Register";/*** 登录失败*/public static final String LOGIN_FAIL = "Error";/*** 所有权限标识*/public static final String ALL_PERMISSION = "*:*:*";/*** 管理员角色权限标识*/public static final String SUPER_ADMIN = "admin";/*** 角色权限分隔符*/public static final String ROLE_DELIMETER = ",";/*** 权限标识分隔符*/public static final String PERMISSION_DELIMETER = ",";/*** 验证码有效期(分钟)*/public static final Integer CAPTCHA_EXPIRATION = 2;/*** 令牌*/public static final String TOKEN = "token";/*** 令牌前缀*/public static final String TOKEN_PREFIX = "Bearer ";/*** 令牌前缀*/public static final String LOGIN_USER_KEY = "login_user_key";/*** 用户ID*/public static final String JWT_USERID = "userid";/*** 用户名称*/public static final String JWT_USERNAME = "sub";/*** 用户头像*/public static final String JWT_AVATAR = "avatar";/*** 创建时间*/public static final String JWT_CREATED = "created";/*** 用户权限*/public static final String JWT_AUTHORITIES = "authorities";/*** 资源映射路径 前缀*/public static final String RESOURCE_PREFIX = "/profile";/*** RMI 远程方法调用*/public static final String LOOKUP_RMI = "rmi:";/*** LDAP 远程方法调用*/public static final String LOOKUP_LDAP = "ldap:";/*** LDAPS 远程方法调用*/public static final String LOOKUP_LDAPS = "ldaps:";/*** 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全)*/public static final String[] JSON_WHITELIST_STR = {"org.springframework", "com.ruoyi"};/*** 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)*/public static final String[] JOB_WHITELIST_STR = {"com.ruoyi"};/*** 定时任务违规的字符*/public static final String[] JOB_ERROR_STR = {"java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml","org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config"};
}
/*** 返回状态码* * @author jzm*/
public class HttpStatus
{/*** 操作成功*/public static final int SUCCESS = 200;/*** 对象创建成功*/public static final int CREATED = 201;/*** 请求已经被接受*/public static final int ACCEPTED = 202;/*** 操作已经执行成功,但是没有返回数据*/public static final int NO_CONTENT = 204;/*** 资源已被移除*/public static final int MOVED_PERM = 301;/*** 重定向*/public static final int SEE_OTHER = 303;/*** 资源没有被修改*/public static final int NOT_MODIFIED = 304;/*** 参数列表错误(缺少,格式不匹配)*/public static final int BAD_REQUEST = 400;/*** 未授权*/public static final int UNAUTHORIZED = 401;/*** 访问受限,授权过期*/public static final int FORBIDDEN = 403;/*** 资源,服务未找到*/public static final int NOT_FOUND = 404;/*** 不允许的http方法*/public static final int BAD_METHOD = 405;/*** 资源冲突,或者资源被锁*/public static final int CONFLICT = 409;/*** 不支持的数据,媒体类型*/public static final int UNSUPPORTED_TYPE = 415;/*** 系统内部错误*/public static final int ERROR = 500;/*** 接口未实现*/public static final int NOT_IMPLEMENTED = 501;/*** 系统警告消息*/public static final int WARN = 601;
}
项目用到的工具类
也是直接。CV。
RedisCache
/*** redis 工具类** @author jzm**/
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache
{@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值* @param timeout 时间* @param timeUnit 时间颗粒度*/public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit){redisTemplate.opsForValue().set(key, value, timeout, timeUnit);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout){return expire(key, timeout, TimeUnit.SECONDS);}/*** 设置有效时间** @param key Redis键* @param timeout 超时时间* @param unit 时间单位* @return true=设置成功;false=设置失败*/public boolean expire(final String key, final long timeout, final TimeUnit unit){return redisTemplate.expire(key, timeout, unit);}/*** 获取有效时间** @param key Redis键* @return 有效时间*/public long getExpire(final String key){return redisTemplate.getExpire(key);}/*** 判断 key是否存在** @param key 键* @return true 存在 false不存在*/public Boolean hasKey(String key){return redisTemplate.hasKey(key);}/*** 获得缓存的基本对象。** @param key 缓存键值* @return 缓存键值对应的数据*/public <T> T getCacheObject(final String key){ValueOperations<String, T> operation = redisTemplate.opsForValue();return operation.get(key);}/*** 删除单个对象** @param key*/public boolean deleteObject(final String key){return redisTemplate.delete(key);}/*** 删除集合对象** @param collection 多个对象* @return*/public boolean deleteObject(final Collection collection){return redisTemplate.delete(collection) > 0;}/*** 缓存List数据** @param key 缓存的键值* @param dataList 待缓存的List数据* @return 缓存的对象*/public <T> long setCacheList(final String key, final List<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象** @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** @param key 缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet){BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 缓存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap){if (dataMap != null){redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据** @param key Redis键* @param hKey Hash键* @param value 值*/public <T> void setCacheMapValue(final String key, final String hKey, final T value){redisTemplate.opsForHash().put(key, hKey, value);}/*** 获取Hash中的数据** @param key Redis键* @param hKey Hash键* @return Hash中的对象*/public <T> T getCacheMapValue(final String key, final String hKey){HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 获取多个Hash中的数据** @param key Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 删除Hash中的某条数据** @param key Redis键* @param hKey Hash键* @return 是否成功*/public boolean deleteCacheMapValue(final String key, final String hKey){return redisTemplate.opsForHash().delete(key, hKey) > 0;}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern){return redisTemplate.keys(pattern);}
}
HttpHelper
主要是为了读取http请求体的数据。
/*** 通用http工具封装** @author ruoyi*/
public class HttpHelper
{private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class);public static 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){LOGGER.warn("getBodyString出现问题!");} finally{if (reader != null){try{reader.close();} catch (IOException e){LOGGER.error("Exceptions:", e.getMessage());}}}return sb.toString();}
}
StringUtils
/*** 字符串工具类** @author jzm*/
public class StringUtils extends StrUtil
{/*** 空字符串*/private static final String NULLSTR = "";/*** 下划线*/private static final char SEPARATOR = '_';/*** 获取参数不为空值** @param value defaultValue 要判断的value* @return value 返回值*/public static <T> T nvl(T value, T defaultValue){return value != null ? value : defaultValue;}/*** * 判断一个Collection是否为空, 包含List,Set,Queue** @param coll 要判断的Collection* @return true:为空 false:非空*/public static boolean isEmpty(Collection<?> coll){return isNull(coll) || coll.isEmpty();}/*** * 判断一个Collection是否非空,包含List,Set,Queue** @param coll 要判断的Collection* @return true:非空 false:空*/public static boolean isNotEmpty(Collection<?> coll){return !isEmpty(coll);}/*** * 判断一个对象数组是否为空** @param objects 要判断的对象数组* * @return true:为空 false:非空*/public static boolean isEmpty(Object[] objects){return isNull(objects) || (objects.length == 0);}/*** * 判断一个对象数组是否非空** @param objects 要判断的对象数组* @return true:非空 false:空*/public static boolean isNotEmpty(Object[] objects){return !isEmpty(objects);}/*** * 判断一个Map是否为空** @param map 要判断的Map* @return true:为空 false:非空*/public static boolean isEmpty(Map<?, ?> map){return isNull(map) || map.isEmpty();}/*** * 判断一个Map是否为空** @param map 要判断的Map* @return true:非空 false:空*/public static boolean isNotEmpty(Map<?, ?> map){return !isEmpty(map);}/*** * 判断一个字符串是否为空串** @param str String* @return true:为空 false:非空*/public static boolean isEmpty(String str){return isNull(str) || NULLSTR.equals(str.trim());}/*** * 判断一个字符串是否为非空串** @param str String* @return true:非空串 false:空串*/public static boolean isNotEmpty(String str){return !isEmpty(str);}/*** * 判断一个对象是否为空** @param object Object* @return true:为空 false:非空*/public static boolean isNull(Object object){return object == null;}/*** * 判断一个对象是否非空** @param object Object* @return true:非空 false:空*/public static boolean isNotNull(Object object){return !isNull(object);}public static boolean inStringIgnoreCase(String str, String... strs){if (str != null && strs != null){for (String s : strs){if (str.equalsIgnoreCase(s)){return true;}}}return false;}
}
ServletUtils
客户端工具类
/*** 客户端工具类** @author Jzm*/
public class ServletUtils
{/*** 获取String参数*/public static String getParameter(String name){return getRequest().getParameter(name);}/*** 获取String参数*/public static String getParameter(String name, String defaultValue){return Convert.toStr(getRequest().getParameter(name), defaultValue);}/*** 获取Integer参数*/public static Integer getParameterToInt(String name){return Convert.toInt(getRequest().getParameter(name));}/*** 获取Integer参数*/public static Integer getParameterToInt(String name, Integer defaultValue){return Convert.toInt(getRequest().getParameter(name), defaultValue);}/*** 获取Boolean参数*/public static Boolean getParameterToBool(String name){return Convert.toBool(getRequest().getParameter(name));}/*** 获取Boolean参数*/public static Boolean getParameterToBool(String name, Boolean defaultValue){return Convert.toBool(getRequest().getParameter(name), defaultValue);}/*** 获得所有请求参数** @param request 请求对象{@link ServletRequest}* @return Map*/public static Map<String, String[]> getParams(ServletRequest request){final Map<String, String[]> map = request.getParameterMap();return Collections.unmodifiableMap(map);}/*** 获得所有请求参数** @param request 请求对象{@link ServletRequest}* @return Map*/public static Map<String, String> getParamMap(ServletRequest request){Map<String, String> params = new HashMap<>();for (Map.Entry<String, String[]> entry : getParams(request).entrySet()){params.put(entry.getKey(), StringUtils.join(",", entry.getValue()));}return params;}/*** 获取request*/public static HttpServletRequest getRequest(){return getRequestAttributes().getRequest();}/*** 获取response*/public static HttpServletResponse getResponse(){return getRequestAttributes().getResponse();}/*** 获取session*/public static HttpSession getSession(){return getRequest().getSession();}public static ServletRequestAttributes getRequestAttributes(){RequestAttributes attributes = RequestContextHolder.getRequestAttributes();return (ServletRequestAttributes) attributes;}/*** 将字符串渲染到客户端** @param response 渲染对象* @param string 待渲染的字符串*/public static void renderString(HttpServletResponse response, String string){try{response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(string);} catch (IOException e){e.printStackTrace();}}/*** 是否是Ajax异步请求** @param request*/public static boolean isAjaxRequest(HttpServletRequest request){String accept = request.getHeader("accept");if (accept != null && accept.contains("application/json")){return true;}String xRequestedWith = request.getHeader("X-Requested-With");if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")){return true;}String uri = request.getRequestURI();if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")){return true;}String ajax = request.getParameter("__ajax");return StringUtils.inStringIgnoreCase(ajax, "json", "xml");}/*** 内容编码** @param str 内容* @return 编码后的内容*/public static String urlEncode(String str){try{return URLEncoder.encode(str, Constants.UTF8);} catch (UnsupportedEncodingException e){return StringUtils.EMPTY;}}/*** 内容解码** @param str 内容* @return 解码后的内容*/public static String urlDecode(String str){try{return URLDecoder.decode(str, Constants.UTF8);} catch (UnsupportedEncodingException e){return StringUtils.EMPTY;}}
}
项目用到的模型
AjaxResult: 公共响应类
/*** 操作消息提醒** @author jzm*/
public class AjaxResult extends HashMap<String, Object>
{private static final long serialVersionUID = 1L;/*** 状态码*/public static final String CODE_TAG = "code";/*** 返回内容*/public static final String MSG_TAG = "msg";/*** 数据对象*/public static final String DATA_TAG = "data";/*** 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。*/public AjaxResult(){}/*** 初始化一个新创建的 AjaxResult 对象** @param code 状态码* @param msg 返回内容*/public AjaxResult(int code, String msg){super.put(CODE_TAG, code);super.put(MSG_TAG, msg);}/*** 初始化一个新创建的 AjaxResult 对象** @param code 状态码* @param msg 返回内容* @param data 数据对象*/public AjaxResult(int code, String msg, Object data){super.put(CODE_TAG, code);super.put(MSG_TAG, msg);if (StringUtils.isNotNull(data)){super.put(DATA_TAG, data);}}/*** 返回成功消息** @return 成功消息*/public static AjaxResult success(){return AjaxResult.success("操作成功");}/*** 返回成功数据** @return 成功消息*/public static AjaxResult success(Object data){return AjaxResult.success("操作成功", data);}/*** 返回成功消息** @param msg 返回内容* @return 成功消息*/public static AjaxResult success(String msg){return AjaxResult.success(msg, null);}/*** 返回成功消息** @param msg 返回内容* @param data 数据对象* @return 成功消息*/public static AjaxResult success(String msg, Object data){return new AjaxResult(HttpStatus.SUCCESS, msg, data);}/*** 返回警告消息** @param msg 返回内容* @return 警告消息*/public static AjaxResult warn(String msg){return AjaxResult.warn(msg, null);}/*** 返回警告消息** @param msg 返回内容* @param data 数据对象* @return 警告消息*/public static AjaxResult warn(String msg, Object data){return new AjaxResult(HttpStatus.WARN, msg, data);}/*** 返回错误消息** @return 错误消息*/public static AjaxResult error(){return AjaxResult.error("操作失败");}/*** 返回错误消息** @param msg 返回内容* @return 错误消息*/public static AjaxResult error(String msg){return AjaxResult.error(msg, null);}/*** 返回错误消息** @param msg 返回内容* @param data 数据对象* @return 错误消息*/public static AjaxResult error(String msg, Object data){return new AjaxResult(HttpStatus.ERROR, msg, data);}/*** 返回错误消息** @param code 状态码* @param msg 返回内容* @return 错误消息*/public static AjaxResult error(int code, String msg){return new AjaxResult(code, msg, null);}/*** 是否为成功消息** @return 结果*/public boolean isSuccess(){return Objects.equals(HttpStatus.SUCCESS, this.get(CODE_TAG));}/*** 是否为警告消息** @return 结果*/public boolean isWarn(){return Objects.equals(HttpStatus.WARN, this.get(CODE_TAG));}/*** 是否为错误消息** @return 结果*/public boolean isError(){return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG));}/*** 方便链式调用** @param key 键* @param value 值* @return 数据对象*/@Overridepublic AjaxResult put(String key, Object value){super.put(key, value);return this;}
}
2.核心
RepeatSubmit
重复提交注解。主要用来设置前后提交数据时间差,至少要大于的时间差的上限。
/*** 自定义注解防止表单重复提交** @author jzm*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{/*** 间隔时间(ms),小于此时间视为重复提交*/public int interval() default 5000;/*** 提示消息*/public String message() default "不允许重复提交,请稍候再试";
}
RepeatSubmitInterceptor
我们重复提交拦截器的抽象类。我们主要把 preHandle()方法给实现了,但是具体判断是否重复提交的逻辑交给子类来实现。好处是,灵活度高,代码可读性强。当我们有其他相似功能拦截器需要实现时,也只需要继承该类即可。
/*** 拦截器** @author: jzm* @date: 2024-01-24 21:20**/public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception{if (handler instanceof HandlerMethod){HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class); // 能拿到处理方法if (repeatSubmit != null){if (this.isRepeatSubmit(request, repeatSubmit)) // 我们只有加了这个注解才表示限制重复提交{AjaxResult result = AjaxResult.error(repeatSubmit.message());ServletUtils.renderString(response, JSONUtil.toJsonStr(result));return false;}}return true;} else{return true;}}/*** 验证是否重复提交由子类实现具体的防重复提交的规则** @param request 请求信息* @param annotation 防重复注解参数* @return 结果* @throws Exception*/public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}
SameUrlDataInterceptor
我们要具体判断数据是否重复提交的子类。最后,将这个注入spring容器里面,然后我们在webmvc里面进行配置就可以正常使用了。
/*** 判断请求url和数据是否和上一次相同,* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。** @author jzm*/
// 我们使用拦截器,防止重复提交
// 现在我们知道,为什么不用面向切面了? 切面需要拦截controller里面的方法,但是若依controller分布比较分散
// 用拦截器,会拦截controller的映射接口
// 首先,我们知道 Handler能够获得映射为方法的Method
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{@AutowiredRedisCache redisCache;public final String header = "Authorization";public final String REPEAT_PARAMS = "repeatParams";public final String REPEAT_TIME = "repeatTime";@Overridepublic boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation){String nowParams = "";// 拿请求body里面的内容// 拷贝副本--给拦截器读取 clone拷贝不显示,对于引用对象,是引用拷贝...,//if (request instanceof RequestReaderHttpServletRequestWrapper){RequestReaderHttpServletRequestWrapper requestWrapper = (RequestReaderHttpServletRequestWrapper) request;nowParams = HttpHelper.getBodyString(requestWrapper);}// body参数为空,获取Parameter的数据if (StringUtils.isEmpty(nowParams)){nowParams = JSONUtil.toJsonStr(request.getParameterMap());}// 当前数据映射,提交参数、提交时间Map<String, Object> nowDataMap = new HashMap<String, Object>();nowDataMap.put(REPEAT_PARAMS, nowParams);nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());// 请求地址(作为存放cache的key值)String uri = request.getRequestURI();// 唯一值(没有消息头则使用请求地址)String submitKey = StringUtils.trimToEmpty(request.getHeader(header));// 唯一标识(指定key + url + 消息头)String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + uri + submitKey;// 如果 == null,代表提交过Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);if (sessionObj != null){Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;if (sessionMap.containsKey(uri)){Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(uri);// 两次提交内容一致 && 提交时间间隔差 < 要求时间段if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())){return true;}}}HashMap<String, Object> cacheMap = new HashMap<>();cacheMap.put(uri, 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;}
}
RepeatedlyRequestWrapper
关于为什么要这个东西呢?我们post请求,拦截器要预先读取HtppServletRequest里面的body的数据,是通过io的方式,都知道io读取完毕之后,之前的数据是变为null的,但是,当我么后面的接口来委派的时候,也是通过io读取body。这时候bodu里面是null的。那么鸡儿就会报io错。
因此我们需要这个类建立复制流。
/*** 将请求包装,用来建立复制流** @author jzm*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{private final byte[] body;public RepeatedlyRequestWrapper(HttpServletRequest request) throws IOException{super(request);body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));}@Overridepublic BufferedReader getReader() throws IOException{return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException{final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream(){@Overridepublic int read() throws IOException{return bais.read();}@Overridepublic boolean isFinished(){return false;}@Overridepublic boolean isReady(){return false;}@Overridepublic void setReadListener(ReadListener readListener){}};}
}
RepeatableFilter
一般情况下,还用到Filterl来对reques来进行包装成wrapper。然后传递到拦截器。
/*** Repeatable 过滤器** @author jzm*/
@Component
public class RepeatableFilter implements Filter
{@Overridepublic void init(FilterConfig filterConfig) throws ServletException{}@Override// 我们下面对于包装,前提是application/jsonpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException{ServletRequest requestWrapper = null;if (request instanceof HttpServletRequest&& StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)){requestWrapper = new RequestReaderHttpServletRequestWrapper((HttpServletRequest) request);}if (null == requestWrapper){chain.doFilter(request, response);} else{chain.doFilter(requestWrapper, response);}}@Overridepublic void destroy(){}
}
3.测试
测试用到的to
/*** 测试to** @author: jzm* @date: 2024-01-25 14:26**/public class TestTo
{public TestTo(){}public String getName(){return name;}public void setName(String name){this.name = name;}public Integer getAge(){return age;}public void setAge(Integer age){this.age = age;}private String name;private Integer age;}
我们新建controller用来测试。分别对get路径参数、post请求体中的数据进行来校验。
*** 测试控制器** @author: jzm* @date: 2024-01-25 11:10**/@RestController
@ResponseBody
public class BaseController
{private Logger log = LoggerFactory.getLogger(BaseController.class);@RequestMapping(value = "/get/test", method = {RequestMethod.GET})@RepeatSubmit(interval = 10 * 1000, message = "对不起您重复提交get请求!")public AjaxResult getTest(@RequestParam("name") String name, @RequestParam("age") Integer age){String res = "get_test:" + name + age;log.info(res);return AjaxResult.success(res);}@RequestMapping(value = "/post/test", method = {RequestMethod.POST})@RepeatSubmit(interval = 10 * 1000, message = "对不起重复提交post请求")public AjaxResult postTest(@RequestBody TestTo testTo){String res = "post_test" + testTo.getName() + testTo.getAge();log.info(res);return AjaxResult.success(res);}
}
我们启动项目,利用Apifox 来进行测试:
这时候打开Resp:免费Redis图形化界面(RESP)下载地址和连接步骤_resp下载-CSDN博客
发现数据是成功存入的,剩余7s过期,在10s之内,也就是数据没过期之前,在发送一次。

因此,确实数据重复提交了。

post请求测试。

相关文章:
redis + 拦截器 :防止数据重复提交
1.项目用到,不是核心 我们干系统开发,不免要考虑一个点,数据的重复提交。 我想我们之前如果要校验数据重复提交要求,会怎么干?会在业务层,对数据库操作,查询数据是否存在,存在就禁止插入数据; 但是吧,我们每次crud操作都会连接…...
如何进行H.265视频播放器EasyPlayer.js的中性化设置?
H5无插件流媒体播放器EasyPlayer属于一款高效、精炼、稳定且免费的流媒体播放器,可支持多种流媒体协议播放,可支持H.264与H.265编码格式,性能稳定、播放流畅,能支持WebSocket-FLV、HTTP-FLV,HLS(m3u8&#…...
Ubuntu22.04安装4090显卡驱动
1、安装完Ubuntu系统,打完所有补丁后再进行后续操作 2、下载系统所需要的版本的NV显卡驱动,本次由于使用CUDA12.1,故选用的驱动版本为NVIDIA-Linux-x86_64-530.41.03.run 3、卸载NV驱动(只是保险起见,并不是一定会卸…...
YOLOv8优化策略:注意力涨点系列篇 | 一种轻量级的加强通道信息和空间信息提取能力的MLCA注意力
🚀🚀🚀本文改进:一种轻量级的加强通道信息和空间信息提取能力 MLCA注意力 🚀🚀🚀在YOLOv8中如何使用 1)作为注意力机制使用;2)与c2f结合使用; 🚀🚀🚀YOLOv8改进专栏:http://t.csdnimg.cn/hGhVK 学姐带你学习YOLOv8,从入门到创新,轻轻松松搞定科研…...
【新书推荐】2.5节 有符号整数和无符号整数
本节内容:整数的编码规则。 ■数据的编码规则:计算机的二进制数对于计算机本身而言仅仅表示0和1。人们按照不同的编码规则赋予二进制数不同的含义。整数的编码规则分为有符号整数和无符号整数。 ■数据的存储规则:x86计算机以字节为单位&…...
RT-Thread: 串口操作、增加串口、串口函数
说明:本文记录RT-Thread添加串口的步骤和串口的使用。 1.新增串口 官方链接:https://www.rt-thread.org/document/site/rtthread-studio/drivers/uart/v4.0.2/rtthread-studio-uart-v4.0.2/ 新增串口只需要在 board.h 文件中定义相关串口的宏定…...
自然语言处理的新突破:如何推动语音助手和机器翻译的进步
一、语音助手方面的进展 语音助手作为人机交互的重要入口之一,其性能的提升离不开自然语言处理技术的进步。基于深度学习的语音识别和语义理解技术,使得语音助手可以更准确地分析用户意图,提供个性化服务。 语音识别精度的持续提高 语音识别是语音助手的基础。随着深度神经网…...
vue3 + jeecgBoot 获取项目IP地址
封装的useGlobSetting 函数 引入并使用 import { useGlobSetting } from //hooks/setting;const glob useGlobSetting();console.log(glob.uploadUrl) //http://192.168.105.57:7900/bs-axfd...
Java Server-Sent Events通信
Server-Sent Events特点与优势 后端可以向前端发送信息,类似于websocket,但是websocket是双向通信,但是sse为单向通信,服务器只能向客户端发送文本信息,效率比websocket高。 单向通信:SSE只支持服务器到客…...
[蓝桥杯]真题讲解:冶炼金属(暴力+二分)
蓝桥杯真题视频讲解:冶炼金属(暴力做法与二分做法) 一、视频讲解二、暴力代码三、正解代码 一、视频讲解 视频讲解 二、暴力代码 //暴力代码 #include<bits/stdc.h> #define endl \n #define deb(x) cout << #x << &qu…...
Fastbee开源物联网项目RoadMap
架构优化 代码简化业务&协议解耦关键组件支持横向拓展网络协议支持横向拓展,包括:mqtt broker,tcp,coap,udp,sip等协议插件化编码脚本化业务代码模版化消息总线 功能优化 网关/子网关:上线,绑定,拓扑࿰…...
Linux文件管理技术实践
shell shell的种类(了解) shell是用于和Linux内核进行交互的一个程序,他的功能和window系统下的cmd是一样的。而且shell的种类也有很多常见的有c shell、bash shell、Korn shell等等。而本文就是使用Linux最常见的bash shell对Linux常见指令展开探讨。 内置shell…...
Python如何按指定列的空值删除行?
目录 1、按指定列的空值删除行2、滑动窗口按指定列的值填充最前面的缺失值 1、按指定列的空值删除行 数据准备: df pd.DataFrame({C1: [1, 2, 3, 4], C2: [A, np.NaN, C, D], C3: [V1, V2, V3, np.NaN]}) print(df.to_string()) C1 C2 C3 0 1 A V1 1 …...
【云原生】Docker的镜像创建
目录 1.基于现有镜像创建 (1)首先启动一个镜像,在容器里做修改 编辑(2)然后将修改后的容器提交为新的镜像,需要使用该容器的 ID 号创建新镜像 实验 2.基于本地模板创建 3&am…...
大语言模型推理提速:TensorRT-LLM 高性能推理实践
作者:顾静 TensorRT-LLM 如何提升 LLM 模型推理效率 大型语言模型(Large language models,LLM)是基于大量数据进行预训练的超大型深度学习模型。底层转换器是一组神经网络,这些神经网络由具有 self-attention 的编码器和解码器组…...
全面理解“张量”概念
1. 多重视角看“张量” 张量(Tensor)是一个多维数组的概念,在不同的学科领域中有不同的应用和解释: 物理学中的张量: 在物理学中,张量是一个几何对象,用来表示在不同坐标系下变换具有特定规律的…...
MacOS X 安装免费的 LaTex 环境
最近把工作终端一步步迁移到Mac上来了,搭了个 Latex的环境,跟windows上一样好用。 首先,如果是 intel 芯片的 macOS,那么可以使用组合1, 如果是 M1、M2 或 M3 芯片或者 intel 芯片的 Mac book,则应该使用…...
深入Amazon S3:实战指南
Amazon S3(Simple Storage Service)是AWS(Amazon Web Services)提供的一项强大的云存储服务,广泛用于存储和检索各种类型的数据。本篇实战指南将深入介绍如何在实际项目中充分利用Amazon S3的功能,包括存储桶的创建、对象的管理、权限控制、版本控制、日志记录等方面的实…...
Ansible自动化运维(三)Playbook 模式详解
👨🎓博主简介 🏅云计算领域优质创作者 🏅华为云开发者社区专家博主 🏅阿里云开发者社区专家博主 💊交流社区:运维交流社区 欢迎大家的加入! 🐋 希望大家多多支…...
LCS板子加逆向搜索
LCS 题面翻译 题目描述: 给定一个字符串 s s s 和一个字符串 t t t ,输出 s s s 和 t t t 的最长公共子序列。 输入格式: 两行,第一行输入 s s s ,第二行输入 t t t 。 输出格式: 输出 s s s…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
基于服务器使用 apt 安装、配置 Nginx
🧾 一、查看可安装的 Nginx 版本 首先,你可以运行以下命令查看可用版本: apt-cache madison nginx-core输出示例: nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...
理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件,用于在原生应用中加载 HTML 页面: 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...
Pinocchio 库详解及其在足式机器人上的应用
Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库,专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性,并提供了一个通用的框架&…...
中医有效性探讨
文章目录 西医是如何发展到以生物化学为药理基础的现代医学?传统医学奠基期(远古 - 17 世纪)近代医学转型期(17 世纪 - 19 世纪末)现代医学成熟期(20世纪至今) 中医的源远流长和一脉相承远古至…...
