您的位置:首页 > 编程语言 > Java开发

【struts2】struts防止表单重复提交源码分析

2015-04-09 16:34 489 查看

一、概述

表单重复提交已经存在很久了,也有很多讨论。防止表单重复提交主要是防止“服务器处理慢时的页面刷新”,以及浏览器后退后再次提交,甚至是点击提交按钮的时候手快点了很多次。

常用的JS将提交按钮设置成disabled,这种防止不了页面刷新,重定向防止不了浏览器后退后重复提交,两者结合也没用。

struts2采用的是页面hidden+session来实现防止重复提交,通过拦截器token或或tokenSession来实现,其思想很简单,本文主要是讨论实现代码中涉及的细节。

二、原理

简单来说,在请求一个表单提交页面该页面还未加载到浏览器时,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(token值的生成也是由该类完成的),外加一个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防止表单重复提交所有过程。



至此,本文讨论结束。

附注:

本文如有错漏,烦请不吝指正,谢谢!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: