springMVC如何防止表單重復(fù)提交詳解
?前言
在系統(tǒng)中,有些接口如果重復(fù)提交,可能會(huì)造成臟數(shù)據(jù)或者其他的嚴(yán)重的問(wèn)題,所以我們一般會(huì)對(duì)與數(shù)據(jù)庫(kù)有交互的接口進(jìn)行重復(fù)處理
- 首先可以在前端做一層控制。當(dāng)前端觸發(fā)操作時(shí),或彈出確認(rèn)界面,或 disable 禁用按鈕等等,但是這并不能徹底解決問(wèn)題。假設(shè)我們不是從客戶端提交,而是被其他的系統(tǒng)調(diào)用,還會(huì)遇到這種問(wèn)題
- 為了徹底解決問(wèn)題,還需要在后端對(duì)接口做防重處理
一般會(huì)引起表單重復(fù)提交的場(chǎng)景
- 在網(wǎng)絡(luò)延遲的情況下讓用戶有時(shí)間點(diǎn)擊多次 submit 按鈕導(dǎo)致表單重復(fù)提交
- 表單提交后用戶點(diǎn)擊【刷新】按鈕導(dǎo)致表單重復(fù)提交
- 用戶提交表單后,點(diǎn)擊瀏覽器的【后退】按鈕回退到表單頁(yè)面后進(jìn)行再次提交
防止表單重復(fù)提交
單機(jī)
實(shí)現(xiàn)的思路步驟
通過(guò)當(dāng)前用戶上一次請(qǐng)求的 url 和參數(shù),驗(yàn)證是否是重復(fù)的請(qǐng)求
- 攔截器攔截請(qǐng)求,將上一次請(qǐng)求的 url 和參數(shù)和這次的對(duì)比
- 判斷,是否一致說(shuō)明重復(fù)提交并記錄日志
代碼實(shí)現(xiàn)
創(chuàng)建自定義注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SameUrlData { }
創(chuàng)建自定義攔截器
public class SameUrlDataInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); // 是否有 SameUrlData 注解 SameUrlData annotation = method.getAnnotation(SameUrlData.class); if (annotation != null) { // 如果重復(fù)相同數(shù)據(jù) if (repeatDataValidator(request)) { response.sendRedirect("/error/409"); return false; } else { return true; } } return true; } else { return super.preHandle(request, response, handler); } } /** * 驗(yàn)證同一個(gè)url數(shù)據(jù)是否相同提交,相同返回true */ private boolean repeatDataValidator(HttpServletRequest httpServletRequest) { String params = JsonMapper.toJsonString(httpServletRequest.getParameterMap()); String url = httpServletRequest.getRequestURI(); Map<String, String> map = new HashMap<>(); map.put(url, params); String nowUrlParams = map.toString(); Object preUrlParams = httpServletRequest.getSession().getAttribute("repeatData"); // 如果上一個(gè)數(shù)據(jù)為null,表示沒(méi)有重復(fù)提交 if (preUrlParams == null) { httpServletRequest.getSession().setAttribute("repeatData", nowUrlParams); return false; // 否則,已經(jīng)有過(guò)提交了 } else { // 如果上次url+數(shù)據(jù) 和 本次url+數(shù)據(jù)相同,則表示重復(fù)添加數(shù)據(jù) if (preUrlParams.toString().equals(nowUrlParams)) { return true; // 如果上次 url+數(shù)據(jù) 和 本次url加數(shù)據(jù)不同,則不是重復(fù)提交 } else { httpServletRequest.getSession().setAttribute("repeatData", nowUrlParams); return false; } } } }
將自定義攔截器添加到容器
<mvc:interceptor> <mvc:mapping path="/**"/> <bean class="com.chinagdn.base.common.interceptor.SameUrlDataInterceptor"/> </mvc:interceptor>
controller 層
@Controller public class TestController { // 在 controller 層使用 @SameUrlData 注解即可 @SameUrlData @RequestMapping(value = "save", method = RequestMethod.POST) public String save(@Valid LoginUser loginUser, Errors errors, RedirectAttributes redirectAttributes, Model model) throws Exception { //..... } }
分布式
很顯然在分布式的情況下,使用上述方法已無(wú)法防止表單重復(fù)提交了;一者在分布式部署下 session 是不共享的,二者使用 上一次請(qǐng)求的 url 和參數(shù)和這次的對(duì)比 已無(wú)法對(duì)比請(qǐng)求的 url 和參數(shù)了。在此情況下,就可以使用 redisson 的分布式鎖來(lái)實(shí)現(xiàn)了
實(shí)現(xiàn)的思路步驟
- 使用分布式鎖 redisson 來(lái)嘗試獲取一把鎖
- 如果成功獲取鎖,再獲取判斷 redis 中的 key 值是否存在(key 值自定義)
- 如果 key 值不存在,則證明不是重復(fù)請(qǐng)求,再給 redis 中存入數(shù)據(jù)(使用 UUID 不重復(fù));反之,則證明是重復(fù)請(qǐng)求進(jìn)行提交
- 最后,釋放分布式鎖
代碼實(shí)現(xiàn)
Maven 依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.11.0</version> </dependency>
配置文件配置 redis 信息
# redis 的配置 spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password= spring.redis.database=0 spring.redis.timeout=2000 spring.redis.lettuce.pool.max-active=10 spring.redis.lettuce.pool.max-wait=2 spring.redis.lettuce.pool.min-idle=5 spring.redis.lettuce.pool.max-idle=10
RedissonClient 配置類
@Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient client = Redisson.create(config); return client; } }
controller 層,偽代碼如下
@Controller public class TestController { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private RedissonClient redissonClient; @PostMapping(path="/registerLock") @ResponseBody public ResultMap registerLock(UserDto userDto) { RLock rLock = redissonClient.getLock(userDto.getUserName()); // redis中的key值 String key = userDto.getUserName() + "-redissonLock"; boolean resultLock = rLock.tryLock(30, 10, TimeUnit.SECONDS); if (resultLock) { try { // 如果不存在key if (!stringRedisTemplate.hasKey(key)) { // 給redis中存入數(shù)據(jù) stringRedisTemplate.opsForValue().set(key,UUID.randomUUID().toString(),10L,TimeUnit.SECONDS); // .....................你的業(yè)務(wù) } // 如果存在,則提示不可重復(fù)提交 return new ResultMap().fail().message("請(qǐng)勿重復(fù)提交"); } catch (Exception e) { return new ResultMap().fail().message("獲取redisson分布式鎖異常"); } } finally { // 釋放鎖 rLock.unlock(); } return null; } }
分布式鎖防止表單重復(fù)提交參考:http://chabaoo.cn/article/230608.htm
總結(jié)
到此這篇關(guān)于springMVC如何防止表單重復(fù)提交的文章就介紹到這了,更多相關(guān)springMVC防止表單重復(fù)提交內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java基礎(chǔ)之初始化ArrayList時(shí)直接賦值的4種方式總結(jié)
ArrayList是Java中的一個(gè)類,它是Java集合框架中的一部分,用于實(shí)現(xiàn)動(dòng)態(tài)數(shù)組,下面這篇文章主要給大家介紹了關(guān)于java基礎(chǔ)之初始化ArrayList時(shí)直接賦值的4種方式,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07在Java中對(duì)List進(jìn)行分區(qū)的實(shí)現(xiàn)方法
在本文中,我們將說(shuō)明如何將一個(gè)列表拆分為多個(gè)給定大小的子列表,也就是說(shuō)在 Java 中如何對(duì)List進(jìn)行分區(qū),文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下2024-04-04Java中利用POI優(yōu)雅的導(dǎo)出Excel文件詳解
這篇文章主要給大家介紹了關(guān)于Java中如何利用POI優(yōu)雅的導(dǎo)出Excel文件的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05ReentrantLock實(shí)現(xiàn)原理詳解
本文將對(duì)ReentrantLock實(shí)現(xiàn)原理進(jìn)行詳細(xì)的介紹,具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-02-02解決Process.getInputStream()阻塞的問(wèn)題
這篇文章主要介紹了解決Process.getInputStream()阻塞的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06SpringCloud的@RefreshScope 注解你了解嗎
這篇文章主要介紹了Spring Cloud @RefreshScope 原理及使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-09-09