springmvc 用拦截器+token防止重复提交

 

     首先,防止用户重复提交有很多种方式,总体分为前端JS限制和后端限制,也一直是在后端控制的,第一次遇到这种问题是因为360无限网卡问题,双网卡都发出的请求(问题原因不明确,反正关闭笔记本360无限模块就好了).第二次是因为业务人员重复点击导致的(异步提交的服务器响应慢,也没有控制按钮置灰),第三次是同权限两个用户并发操作了(没几个人的系统,竟然赶到一起了,这个背啊)。

  第一次解决这个问题的方案就是加锁,但是这部分业务操作的是核心数据,不会并发,数据量大也不允许多次操作。这块还是个单机操作。

        1.尝试锁定任务  (先定义一个重入锁作为入参)

        if(!tryLock(lock, opName,opFlag)){

                     renderText("当前有任务正在进行,不进行排队,请稍候再试!");

                     return;

              }

    2.try catch 包围业务处理 finally  释放锁

    下面是尝试获取锁的方法

           public boolean tryLock(ReentrantLock lock,String opName,String opFlag) {

              try {

                     if(lock.tryLock()){

                           logger.info("【"+opName+"-锁定】:" + opFlag);

                           return true;

                     }

                     return false;

              } catch (Exception e) {

                     logger.info("操作标识:"+opFlag+"------"+opName+"锁定异常!",e);

                     return false;

              }

       }

之后的业务没有什么限制了,就需要考虑多线程了,上述方案就不可行了。所以参考了其他人的方法,然后就是一番复制粘贴:

正式代码开始:

页面表单保存服务端发送的token

<input id="token" name="token" type="hidden" th:value="${token}">

首先自定义一个注解

import java.lang.annotation.*;

@Documented

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public @interface Token {

boolean save() default false;

boolean remove() default false;

}

接着实现一个拦截器接口

import java.io.PrintWriter;

import java.lang.reflect.Method;

import java.util.UUID;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import com.bootdo.busi.utils.SendMsgUtil;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.ui.Model;

import org.springframework.web.context.request.RequestAttributes;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class TokenInterceptor extends HandlerInterceptorAdapter {

private static final Logger LOG = LoggerFactory.getLogger(Token.class);

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

if (handler instanceof HandlerMethod) {

HandlerMethod handlerMethod = (HandlerMethod) handler;

Method method = handlerMethod.getMethod();

Token annotation = method.getAnnotation(Token.class);

if (annotation != null) {

boolean needSaveSession = annotation.save();

if (needSaveSession) {

request.getSession(true).setAttribute("token", UUID.randomUUID().toString());

}

boolean needRemoveSession = annotation.remove();

if (needRemoveSession) {

if (isRepeatSubmit(request)) {

LOG.warn("please don't repeat submit,url:"+ request.getServletPath());

SendMsgUtil.sendMessage(response,"repetSubmit");

return false;

}

request.getSession(true).removeAttribute("token");

return false;

}

}

return true;

} else {

return super.preHandle(request, response, handler);

}

}

private boolean isRepeatSubmit(HttpServletRequest request) {

String serverToken = (String) request.getSession(true).getAttribute("token");

if (serverToken == null) {

return true;

}

String clinetToken = request.getParameter("token");

if (clinetToken == null) {

return true;

}

if (!serverToken.equals(clinetToken)) {

return true;

}

return false;

}

}

返回页面数据工具

import com.alibaba.fastjson.JSONObject;

import com.alibaba.fastjson.serializer.SerializerFeature;

import javax.servlet.http.HttpServletResponse;

import java.io.PrintWriter;

public class SendMsgUtil {

/**

* 发送消息 text/html;charset=utf-8

* @param response

* @param str

* @throws Exception

*/

public static void sendMessage(HttpServletResponse response, String str) throws Exception {

response.setContentType("text/html; charset=utf-8");

PrintWriter writer = response.getWriter();

writer.print(str);

writer.close();

response.flushBuffer();

}

/**

* 将某个对象转换成json格式并发送到客户端

* @param response

* @param obj

* @throws Exception

*/

public static void sendJsonMessage(HttpServletResponse response, Object obj) throws Exception {

response.setContentType("application/json; charset=utf-8");

PrintWriter writer = response.getWriter();

writer.print(JSONObject.toJSONString(obj, SerializerFeature.WriteMapNullValue,

SerializerFeature.WriteDateUseDateFormat));

writer.close();

response.flushBuffer();

}

}

定义切面

import org.aspectj.lang.JoinPoint;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.*;

import org.springframework.stereotype.Component;

@Component

@Aspect

public class TokenAspect {

@Pointcut("@annotation(com.bootdo.busi.Token)")

private void token() { }

// /**

// * 定制一个环绕通知

// * @param joinPoint

// */

// @Around("token()")

// public void advice(ProceedingJoinPoint joinPoint) throws Throwable {

// System.out.println("Around Begin");

// joinPoint.proceed();//执行到这里开始走进来的方法体(必须声明)

// System.out.println("Around End");

// }

//当想获得注解里面的属性,可以直接注入改注解

//方法可以带参数,可以同时设置多个方法用&&

@Before("token()")

public void record(JoinPoint joinPoint) {

System.out.println("Before");

}

//

// @After("token()")

// public void after() {

// System.out.println("After");

// }

}

然后配置拦截器, WebConfigurer 中进行配置。

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.stereotype.Component;

import org.springframework.web.servlet.HandlerInterceptor;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Component

class WebConfigurer extends WebMvcConfigurerAdapter {

@Autowired

BootdoConfig bootdoConfig;

@Override

public void addResourceHandlers(ResourceHandlerRegistry registry) {

registry.addResourceHandler("/files/**").addResourceLocations("file:///"+springbootConfig.getUploadPath());

}

@Override

public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(getMyInterceptor()).addPathPatterns("/**");

super.addInterceptors(registry);

}

@Bean

public HandlerInterceptor getMyInterceptor() {

return new TokenInterceptor();

}

}

最最最重要得一点一定要保证页面和后台token得正常传递!!!

在需要生成token(通常是要点击提交得那个页面)得Controller中使用我们刚才自定义好得注解,贴代码:

注:此处为请求的controller (跳转页面),save为true 是在服务端生成一个token 并发送给前端。此时remove默认为false。只进行生成token 不进行重复判断。在isRepeatSubmit方法判断通过返回后,清空服务端token,当重复请求时

服务端token是空,与前端返回token不一致。

@SuppressWarnings({ "unchecked", "finally", "rawtypes" })

@RequestMapping("/SaveDataController/show")

@Token(save=true)

public String saveData(HttpServletRequest request,HttpServletResponse response,String task_id){

Map map = new HashMap();

System.out.println(task_id);

try {

map = saveDataService.queryByTaskId(task_id);

} catch (Exception e) {

e.printStackTrace();

map.put("task_id", task_id);

map.put("savetype", "");

map.put("memoryCodes", "1");

map.put("tablename", "");

map.put("trowkey", "");

map.put("columns", "");

map.put("indextablename", "");

map.put("irowkey", "");

map.put("icolumns", "");

} finally {

request.setAttribute("map", map);

return "savedata/index";

}

}

只要通过这个方法跳向得页面都是随机生成一个token,然后再真正再提交触发得方法上加入@token(remove=true),贴代码:

注:此处为处理业务请求的controller (跳转serivce),此时save默认为false 不会生成一个新的token。此时remove为true。判断当前服务端的token 与前端返回的token是否一致。一致的情况下正常处理

@RequestMapping("/SaveDataController/saveData")

@ResponseBody

@Token(remove=true)

public void saveData(HttpServletRequest request,HttpServletResponse response,

String tablename,String trowkey,String columns,

String indextablename,String irowkey,String icolumns,

String task_id,String savetype,String memoryCodes){

System.out.println(task_id);

saveDataService.saveData(task_id,savetype,memoryCodes,tablename, trowkey, columns, indextablename, irowkey, icolumns);

}

js方法变更

function submit01() {

$.ajax({

cache : true,

type : "POST",

url : prefix + "/save",

data : $('#signupForm').serialize(),

async : false,

error : function(request) {

laryer.alert("Connection error");

},

success : function(data) {

if (data.code == 0) {

parent.layer.msg("保存成功");

parent.reLoad();

var index = parent.layer.getFrameIndex(window.name); // 获取窗口索引

parent.layer.close(index);

} else {

//这个位置增加了判断 判断是否为拦截器返回信息,是就进行重复提交提示

//不是就按原有业务进行提示

if(data=='repetSubmit'){

layer.alert("请不要重复提交表单!")

}else{

layer.alert(data.msg)

}

}

}

});

}

但是,如果页面校验通过,后台校验不过呢,preHandle是业务执行前拦截,此时服务端的token已经被清除了,修改页面参数再提交就会提示重复提交了,用户刷新页面重新填写信息体验很不好。所以有几种方案可以预处理一下:

1.提交表单前调用异步提交校验方法,通过后调用正式方法。

2.校验不通过时,将页面token重新赋值给session。(不推荐,已经添加了拦截器和注解尽量不要修改其他代码)

猜你喜欢

转载自blog.csdn.net/a290450134/article/details/82495673
今日推荐