利用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()); }

 

posted @ 2015-09-19 15:17  寂静沙滩  阅读(376)  评论(0编辑  收藏  举报