赞
踩
表单重复提交会造成数据重复,增加服务器负载,严重甚至会造成服务器宕机等情况,有效防止表单重复提交有一定的必要性。
常见的防止表单重复提交解决方案有以下几种:
一、通过一个标识来控制表单提交之后,再次提交会直接返回处理
示例:
- <html>
- <head>
- <title>防止表单重复提交</title>
- </head>
- <body>
- <form action="/path/post" onsubmit="return dosubmit()" method="post">
- <input type="submit" value="提交" id="submit">
- </form>
- <script type="text/javascript">
- //默认提交状态为false
- let isCommitted = false;
- function dosubmit(){
- if(isCommitted == false){
- //提交表单后,讲提交状态改为true
- commitStatus = true;
- //返回true,让表单正常提交
- return true;
- }else{
- return false;
- }
- }
- </script>
- </body>
- </html>
示例:
- <html>
- <head>
- <title>防止表单重复提交</title>
- </head>
- <body>
- <form action="/path/post" onsubmit="return dosubmit()" method="post">
- <input type="submit" value="提交" id="submit">
- </form>
- <script type="text/javascript">
- function dosubmit() {
- //获取表单提交按钮
- Var btnSubmit = documen.getElementById("sumit");
- //将表单提交按钮设置为不可用,可以避免用户再次点击提交按钮进行提交
- btnSubmit.disabled = "disabled";
- //返回true让表单可以提交
- return true;
- }
- </script>
- </body>
- </html>
注意:通过js代码,当用户点击提交按钮后,屏蔽提交按钮使用户无法点击提交按钮或点击无效,从而实现防止表单重复提交。
在创建数据库建表的时候在ID字段添加主键约束,用户名、邮箱、电话等字段加唯一性约束,以确保数据库只可以添加一条数据。数据库加唯一性约束sql:
alter table tableName_xxx add unique key uniq_xxx(field1, field2)
服务器及时捕捉插入数据异常:
- try {
- xxxMapper.insert(user);
- } catch (DuplicateKeyException e) {
- logger.error("user already exist");
- }
注意:通过数据库加唯一键约束能有效避免数据库重复插入相同数据。但无法阻止恶意用户重复提交表单(攻击网站),服务器大量执行sql插入语句,增加服务器和数据库负荷。
四、利用Session+token防止表单重复提交(建议)
原理
服务器返回表单页面时,会先生成一个token保存于session,并把该toen传给表单页面。当表单提交时会带上token,服务器拦截器Interceptor会拦截该请求,拦截器判断session保存的token和表单提交token是否一致:若不一致或session的token为空或表单未携带token则不通过;首次提交表单时session的token与表单携带的token一致走正常流程,然后拦截器内会删除session保存的token。当再次提交表单时由于session的token为空则不通过。从而实现了防止表单重复提交。
步骤
在服务器端生成一个唯一的token(令牌),同时在当前用户的Session域中保存这个token。
将token发送到客户端的form表单中,在form表单中使用隐藏域来存储这个token,表单提交的时候连同这个token一起提交到服务器端。
在服务器端判断客户端提交上来的token与服务器端生成的token是否一致:如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单;如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的token。
示例
第一步:定义防止重复提交注解
在打开页面方法上,设置createToken()为true,此时拦截器会在Session中保存一个token,同时需要在页面中添加<input type="hidden" name="token" th:value="${session.token}">,保存方法需要验证重复提交的,设置removeToken为true,此时会在拦截器中验证是否重复提交
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface Resubmit {
- /**
- * 创建Token
- * @return
- */
- boolean createToken() default false;
-
- /**
- * 移除Token
- * @return
- */
- boolean removeToken() default false;
- }
- @Slf4j
- public class ResubmitInterceptor 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();
-
- Resubmit annotation = method.getAnnotation(Resubmit.class);
- if (annotation != null) {
- boolean saveSession = annotation.createToken();
- if (saveSession) {
- //在服务器端生成一个唯一的token(令牌),同时在当前用户的Session域中保存这个token
- String token = System.currentTimeMillis() + new Random().nextInt(999999999) + "";
- request.getSession(false).setAttribute("token", token);
- }
-
- boolean removeSession = annotation.removeToken();
- if (removeSession) {
- if (isRepeatSubmit(request)) {
- log.warn("重复提交:" + "url:" + request.getServletPath());
- request.setAttribute("url",request.getServletPath());
- response.sendRedirect(request.getContextPath()+"/resubmitError");
- return false;
- }
- // 处理完后清除当前用户的Session域中存储的token
- request.getSession(false).removeAttribute("token");
- }
- }
- }
- return true;
- }
-
- /**
- * 在服务器端判断客户端提交上来的token与服务器端生成的token是否一致
- * - 如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单
- * - 如果相同则处理表单提交
- * @param request
- * @return 重复提交返回true,否则返回false
- */
- private boolean isRepeatSubmit(HttpServletRequest request) {
- Object token = request.getSession(false).getAttribute("token");
- if(token == null){
- return true;
- }
- String serverToken = (String) token;
- if (serverToken == null) {
- return true;
- }
- String clientToken = request.getParameter("token");
- if (clientToken == null) {
- return true;
- }
- if (!serverToken.equals(clientToken)) {
- return true;
- }
- return false;
- }
- }
- @Configuration
- public class WegoMvcConfig implements WebMvcConfigurer {
- /**
- * 拦截器配置
- */
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- //注册防重复提交拦截器
- registry.addInterceptor(new ResubmitInterceptor())
- .addPathPatterns("/**");
- }
- }
- @Controller
- public class SecurityController {
- /**
- * 打开注册页面
- */
- @GetMapping("openRegister")
- @Resubmit(createToken = true)
- String openRegister() {
- return "frontend/register";
- }
-
- /**
- * 注册逻辑
- */
- @PostMapping("/register")
- @Resubmit(removeToken = true)
- String register(UserRegisterDTO userRegisterDTO, HttpSession session, Model model) {
- //……
- }
-
- @GetMapping("/resubmitError")
- String error(){
- return "frontend/resubmitError";
- }
- }
- <html lang="en" xmlns:th="http://www.thymeleaf.org">
- <body>
- <form id="registerForm" class="reg" method="post" th:action="@{/user/register}">
- <!--防止表单重复提交-->
- <input type="hidden" name="token" th:value="${session.token}">
- 账户名:<input id="account" name="account" type="text"/>
- 请设置密码:<input id="password1" name="password1" type="password"/>
- 请确认密码:<input id="password2" name="password2" type="password"/>
- <input type="submit" value="注册"/>
- </form>
- </body>
- </html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org">
- <body>
- <h2>重复提交<h2>
- </body>
- </html>
五、使用Redis+AOP自定义切入实现 (推荐)
原理:
自定义防止重复提交标记(@AvoidRepeatableCommit)
对需要防止重复提交的Controller里的mapping方法加上该注解
新增Aspect切入点,为@AvoidRepeatableCommit加入切入点
每次提交表单时,Aspect都会保存当前key到redis(须设置过期时间)
重复提交时Aspect会判断当前redis是否有该key,若有则拦截。
示例
第一步:创建注解
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface Resubmit {
-
- /**
- * 指定时间内不可重复提交,单位毫秒,默认120000毫秒
- */
- long timeout() default 120000 ;
- }
- @Slf4j
- @Aspect
- @Component
- public class ResubmitAspect {
- @Resource
- private StringRedisTemplate stringRedisTemplate;
-
- @Around("@annotation(com.wego.common.utils.resubmit.Resubmit)")
- public Object around(ProceedingJoinPoint point) throws Throwable {
- ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
- HttpServletRequest request = attributes.getRequest();
- Object userObj = request.getSession(false).getAttribute("user");
- UserSession user = null;
- if(userObj != null){
- user = (UserSession) userObj;
- }
-
- MethodSignature signature = (MethodSignature) point.getSignature();
- Method method = signature.getMethod();
- //目标类、方法
- String className = method.getDeclaringClass().getName();
- String methodName = method.getName();
-
- String fullMethodName = String.format("%s.%s", className, methodName);
- String key = String.format("%s_%d", Math.abs(user.hashCode()), Math.abs(fullMethodName.hashCode()));
-
- log.info(String.format("ipKey=%s,hashCode=%s,key=%s", fullMethodName, user, key));
-
- //通过反射技术来获取注解对象
- Resubmit resubmit = method.getAnnotation(Resubmit.class);
- long timeout = resubmit.timeout();
- if (timeout < 0) {
- //过期时间10秒
- timeout = 2;
- }
- //获取key键对应的值
- String value = stringRedisTemplate.opsForValue().get(key);
- if (value != null && value.length() > 0) {
- return "请勿重复提交!";
- }
- //新增一个字符串类型的值,key是键,value是值。
- stringRedisTemplate.opsForValue().set(key, UUID.randomUUID().toString(), timeout, TimeUnit.MINUTES);
-
- //返回继续执行被拦截到的方法
- return point.proceed();
- }
- }
- @RestController
- public class DemoController {
- @Resubmit //自定义注解
- @GetMapping("/fun")
- public String fun() {
- System.out.println("fun");
- return "fun";
- }
- }
第一次请求:
再次请求:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。