一、概述
表单重复提交已经存在很久了,也有很多讨论。防止表单重复提交主要是防止“服务器处理慢时的页面刷新”,以及浏览器后退后再次提交,甚至是点击提交按钮的时候手快点了很多次。
常用的JS将提交按钮设置成disabled,这种防止不了页面刷新,重定向防止不了浏览器后退后重复提交,两者结合也没用。
struts2采用的是页面hidden+session来实现防止重复提交,通过拦截器token或或tokenSession来说hi线,其思想很简单,本文主要是讨论实现代码中涉及的细节。
二、原理
简单来说,在请求一个表单提交页面该页面还未加载到浏览器时,struts后台程序会生成一个随机的字符串,这个随机的字符串会放到浏览器请求页面的hidden域和服务器session域,当提交该表单的时候,服务器会比对一同提交过来的hidden值,相同则处理,不同则视为无效或重复提交,如果配置的是token拦截器,那么出现重复提交则会转向一个invalid.token的result,如果配置的是tokenSession则忽略重复提交的请求(通过保存token串),保留在成功页面。
原理很简单,但是细追下来还是有很多问题。如果自己实现的话,其中可能涉及的问题有以下几个。
1. token验证是否采用同步机制。无论是哪一种token,每一次请求和响应都是一次HTTP事务,都需要访问token验证的代码临界区,是采用同步还是其他的机制防止重复访问临界区?
2. token什么时候在session里面删除?在tokenSession下,多次点击或者后退都不会转发到invalid.token,需要保存token值,但是鉴于历史愿意,老版本的token是在验证完之后立即删除token,那么新的tokenSession怎么实现?
3. 如果采用了同步机制,那么怎么保证高并发?是否会导致所有用户同步提交造成系统性能瓶颈?
三、源码分析
struts防止表单操重复提交,主要由三个类来实现,token=org.apache.struts2.interceptor.TokenInterceptor,tokenSession=org.apache.struts2.interceptor.TokenSessionStoreInterceptor,还有org.apache.struts2.util.TokenHelper,外加一个org.apache.struts2.util.InvocationSessionStore来保存tokenSession第一次执行现场。
首先看他们的关系,token继承com.opensymphony.xwork2.interceptor.MethodFilterInterceptor拦截器,tokenSession继承token,如下图。
其次,看两者对token验证的同步机制。
入口都是doIntercept方法,根据token选择和多态从而调用不同类的handleToken方法进行业务逻辑处理。下图是两者handleToken方法的比较。可以看出struts的token防止表单重复提交采用的是同步机制,同步的锁对象是session,这样同一个浏览器提交的请求处于同步状态,但是不同浏览器是并发的。
其次,handleToken方法主要是进行token验证,验证通过然后转交给代理执行原来的逻辑。上图红线部分可以看出,父类token的同步不包含调用代理处理原有逻辑,而tokenSession则把这一部分逻辑进行了同步。原因是在同步代码块中进行if验证的时候会删除session中的token,两种token方式都会删除session中存放的token串,即TokenHelper.validToken()方法进行token验证的时候删除token,如源码所示。
/** * Checks for a valid transaction token in the current request params. If a valid token is found, it is * removed so the it is not valid again. * * @return false if there was no token set into the params (check by looking for {@link #TOKEN_NAME_FIELD}), true if a valid token is found */ public static boolean validToken() { String tokenName = getTokenName(); if (tokenName == null) { if (LOG.isDebugEnabled()) { LOG.debug("no token name found -> Invalid token "); } return false; } String token = getToken(tokenName); if (token == null) { if (LOG.isDebugEnabled()) { LOG.debug("no token found for token name "+tokenName+" -> Invalid token "); } return false; } Map session = ActionContext.getContext().getSession(); String tokenSessionName = buildTokenSessionAttributeName(tokenName); String sessionToken = (String) session.get(tokenSessionName); if (!token.equals(sessionToken)) { if (LOG.isWarnEnabled()) { LOG.warn(LocalizedTextUtil.findText(TokenHelper.class, "struts.internal.invalid.token", ActionContext.getContext().getLocale(), "Form token {0} does not match the session token {1}.", new Object[]{ token, sessionToken })); } return false; } // remove the token so it won't be used again session.remove(tokenSessionName);//删除token return true; }
引入的新问题是在验证完成之后就删除token,那么tokenSession是怎么保存token串的?查看token和tokenSession验证完token调用原有逻辑的代码,如下图所示。
结合一开始讨论的handleToken入口方法中token验证的同步代码块范围:
1. 父类handleValidToken方法没有同步,因为token不需要保存token串,可以直接调用原有逻辑,重复提交直接定向到invalid.token;
2. tokenSession的handleValidToken方法包含在同步块中,即第一次访问该方法的时候,验证完token,虽然在session中已经删除了,但是在调用原有逻辑之前通过InvocationSessionStore类来保存了执行现场(token name,token串,原有逻辑的代理执行类),当浏览器后退再次执行的时候会还原现场,不进行逻辑处理直接返回结果页面,因此tokenSession提供了更友好的结果页面,而不是转发到一个非法token提示页面。
四、具体实现
1. 给处理表单的action配置token拦截器。struts-default.xml中配置了默认引用的拦截器栈,但是token和tokenSession都不在其中,因此需要单独引用。但是token拦截器不是所有action的动作都要拦截,因此只需要配置在特定的action就可以了。
2. 在页面开启struts标签,在表单引入<s:token></token>标签,完成struts token防止表单重复提交所有过程。
至此,本文讨论结束。
附注:
本文如有错漏,烦请不吝指正,谢谢!