利用token防止重复提交的初步设计方案(切合公司小项目的简单设计)
这个项目用struts2作mvc。一直没有使用标签,很多提交都不是form提交,都是用的jquery的ajax发的请求。所以现在要用struts2自带的拦截器token方式是不可能的。
由于发起ajax请求的地方太多,而且不能控制开发人员如何设计自己的页面,并且什么时候去发起请求。所以要在口子上进行统一处理,切合这个项目后端strut2和前端jquery设计了一个简单的方案。
前端:
使用jquery的ajax库的beforeSend和complete函数,在发送ajax请求和完成请求的时候分别进行拦截。
$.ajaxSetup({ beforeSend :function (XMLHttpRequest) { //带上token var newUrl = this.url; if(newUrl.indexOf('?')>0){ this.url = newUrl.concat('&tokenId='+ pageParam.token); //pageParam是一个全局变量 }else this.url = newUrl.concat('?tokenId='+ pageParam.token); }, complete : function (XMLHttpRequest, textStatus) { //看看有没有token返回,有就更新 var reJson = eval('('+XMLHttpRequest.responseText+')'); if(reJson.tokenId){ pageParam.token = reJson.tokenId; } } }
后端
进行了一些与业务逻辑有关的判断来确定哪些请求要进行token验证:
1 我们项目里只有返回json形式时的提交是要修改数据库的提交操作,多次进行这种操作会造成错误的结果,也就是这种请求不是幂等的。要进行控制。
2 目前项目里只有“单据”这种类型的业务比较复杂,在这个Action里需要进行重复提交控制。
3 如果方法是服务器启动后的第一次调用,不会进行控制,而是会执行完了去根据结果来判断这个方法要怎么控制,加入一个控制方法的集合里,然后再来就会进行控制了。这种方式我觉得够了,因为不用去手动的把所有方法枚举出来,而是自动判断。而且服务器启动后第一次调用就发生重复提交的几率太小了。如果真的要完美的话,那可以再启动的时候用反射的方式去把BillAction的所有Method执行一遍后加入这些集合,但我觉得没必须这么复杂。
public final class TokenInterceptor extends AbstractInterceptor { private static Set<String> jsonReturnMethod = new HashSet<String>(); //返回json结果的Action方法 private static Set<String> initPageMethod = new HashSet<String>(); //初始化页面的方法 private static Set<String> excludeMethod = new HashSet<String>(); //不用控制的json返回方法(以后可以开放一个接口给开发人员让他们可以自己定义自己的 Action,现在先定义一个公共的) static { excludeMethod.add("detail"); } @Override public String intercept(ActionInvocation ai) throws Exception { if(ai.getAction() instanceof BillAction){ //判断是不是单据的Action String methodName = ai.getProxy().getMethod(); //调用的方法名 BaseAction action = (BaseAction)ai.getAction(); if(jsonReturnMethod.contains(methodName)&&!excludeMethod.contains(methodName)){ // 验证token,并保证读写同步 String lockStr = action.getContext().getKey()+"_"+methodName; //这个key是我们项目里定义的每个业务模块的唯一标识,再加上方法名,就保证是同一种 类型业务的同一种请求了 synchronized (lockStr){ // 用这个字符串常量作为锁 if(TokenManager.checkToken(action)){ TokenManager.resetToken(action); //token验证通过就重置token,否则就直接返回不调用业务方法。 } else { return ActionConsts.JSON; } } }else if (initPageMethod.contains(methodName)){ //如果是初始化加载页面的方法,不用验证直接重置token TokenManager.resetToken(action); } String result = ai.invoke(); //因为根据struts2的执行流程,最后是在DefaultActionInvocation中执行,执行完所有拦截器后,执行Action,根据返回的字符串 // ,找到Result去处理。这里只简单分两种情况,一种是返回JSON,一种是返回JSP。返回JSON用的JSONResult处理,返回JSP用 // 的ServletDispatcherResult处理。所以要在执行Action之前把重置的tokenId放到响应里,这样Result处理的时候才能找到。 if(!jsonReturnMethod.contains(methodName)&&ActionConsts.JSON.equals(result)){ jsonReturnMethod.add(methodName); } if(!initPageMethod.contains(methodName)&&ActionConsts.JSP.equals(result)&&action.getJsp().contains("editFlow.jsp")){ initPageMethod.add(methodName); //单据的初始化加载页面都是这个jsp,这样可以排除也是调这个Action,但 } //返回的是嵌套子页面jsp的情况。 return result; } return ai.invoke(); } }
把Token的验证和重置都交给Manager类
public class TokenManager { public final static String tokenStr = "tokenId" ; public static boolean checkToken(BaseAction action) { HttpServletRequest request = action.getContext().getRequest(); String clientToken = request.getParameter(tokenStr); if(UtilPub.isNotEmpty(clientToken)){ Object serverTokenMap = request.getSession().getAttribute(tokenStr); if(serverTokenMap == null){ serverTokenMap = new HashMap(); request.getSession().setAttribute(tokenStr,serverTokenMap); } String serverToken = (String)((Map)serverTokenMap).get(action.getContext().getKey()); if(!clientToken.equals(serverToken)) return false; } return true; //请求没带token和token与后端一致的都返回true } public static String resetToken( BaseAction action) { HttpServletRequest request = action.getContext().getRequest(); String newToken = createToken();
//重置后端token request.getSession().setAttribute(tokenStr, newToken); //这个Map是封装了的,专门用于前端输出,因为在strut.xml里配置了,JSONResut会去调用getMap这个方法来获取json结果,所以Json的形式能获取到。而Jsp页面的输出的时 //候也能通过代码获取到。 action.getMap().put(TokenManager.tokenStr,newToken); return newToken; } //简单处理,只是为了防重复,不用考虑安全,目前初步设计阶段,就用时间戳 private static String createToken () { return String.valueOf(System.currentTimeMillis()); }