首先,防止用户重复提交有很多种方式,总体分为前端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
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。(不推荐,已经添加了拦截器和注解尽量不要修改其他代码)