springMVC如何防止表單重復(fù)提交詳解
?前言
在系統(tǒng)中,有些接口如果重復(fù)提交,可能會造成臟數(shù)據(jù)或者其他的嚴(yán)重的問題,所以我們一般會對與數(shù)據(jù)庫有交互的接口進(jìn)行重復(fù)處理
- 首先可以在前端做一層控制。當(dāng)前端觸發(fā)操作時,或彈出確認(rèn)界面,或 disable 禁用按鈕等等,但是這并不能徹底解決問題。假設(shè)我們不是從客戶端提交,而是被其他的系統(tǒng)調(diào)用,還會遇到這種問題
- 為了徹底解決問題,還需要在后端對接口做防重處理
一般會引起表單重復(fù)提交的場景
- 在網(wǎng)絡(luò)延遲的情況下讓用戶有時間點擊多次 submit 按鈕導(dǎo)致表單重復(fù)提交
- 表單提交后用戶點擊【刷新】按鈕導(dǎo)致表單重復(fù)提交
- 用戶提交表單后,點擊瀏覽器的【后退】按鈕回退到表單頁面后進(jìn)行再次提交
防止表單重復(fù)提交
單機(jī)
實現(xiàn)的思路步驟
通過當(dāng)前用戶上一次請求的 url 和參數(shù),驗證是否是重復(fù)的請求
- 攔截器攔截請求,將上一次請求的 url 和參數(shù)和這次的對比
- 判斷,是否一致說明重復(fù)提交并記錄日志
代碼實現(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);
}
}
/**
* 驗證同一個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");
// 如果上一個數(shù)據(jù)為null,表示沒有重復(fù)提交
if (preUrlParams == null) {
httpServletRequest.getSession().setAttribute("repeatData", nowUrlParams);
return false;
// 否則,已經(jīng)有過提交了
} 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 {
//.....
}
}
分布式

很顯然在分布式的情況下,使用上述方法已無法防止表單重復(fù)提交了;一者在分布式部署下 session 是不共享的,二者使用 上一次請求的 url 和參數(shù)和這次的對比 已無法對比請求的 url 和參數(shù)了。在此情況下,就可以使用 redisson 的分布式鎖來實現(xiàn)了
實現(xiàn)的思路步驟
- 使用分布式鎖 redisson 來嘗試獲取一把鎖
- 如果成功獲取鎖,再獲取判斷 redis 中的 key 值是否存在(key 值自定義)
- 如果 key 值不存在,則證明不是重復(fù)請求,再給 redis 中存入數(shù)據(jù)(使用 UUID 不重復(fù));反之,則證明是重復(fù)請求進(jìn)行提交
- 最后,釋放分布式鎖
代碼實現(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("請勿重復(fù)提交");
} catch (Exception e) {
return new ResultMap().fail().message("獲取redisson分布式鎖異常");
}
} finally {
// 釋放鎖
rLock.unlock();
}
return null;
}
}
分布式鎖防止表單重復(fù)提交參考:http://www.dbjr.com.cn/article/230608.htm
總結(jié)
到此這篇關(guān)于springMVC如何防止表單重復(fù)提交的文章就介紹到這了,更多相關(guān)springMVC防止表單重復(fù)提交內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java基礎(chǔ)之初始化ArrayList時直接賦值的4種方式總結(jié)
ArrayList是Java中的一個類,它是Java集合框架中的一部分,用于實現(xiàn)動態(tài)數(shù)組,下面這篇文章主要給大家介紹了關(guān)于java基礎(chǔ)之初始化ArrayList時直接賦值的4種方式,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-07-07
在Java中對List進(jìn)行分區(qū)的實現(xiàn)方法
在本文中,我們將說明如何將一個列表拆分為多個給定大小的子列表,也就是說在 Java 中如何對List進(jìn)行分區(qū),文中有詳細(xì)的代碼示例供大家參考,需要的朋友可以參考下2024-04-04
Java中利用POI優(yōu)雅的導(dǎo)出Excel文件詳解
這篇文章主要給大家介紹了關(guān)于Java中如何利用POI優(yōu)雅的導(dǎo)出Excel文件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05
解決Process.getInputStream()阻塞的問題
這篇文章主要介紹了解決Process.getInputStream()阻塞的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06
SpringCloud的@RefreshScope 注解你了解嗎
這篇文章主要介紹了Spring Cloud @RefreshScope 原理及使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-09-09

