表单重复提交
1、造成重复提交主要的两个原因:
(1) 一是,服务器处理时间久。当用户在表单中填完信息,点击“提交”按钮后,由于服务器反应时间过长没能及时看到响应信息,或者出于其它目的,再次点击“提交”按钮,从而导致在服务器端接收到两条或多条相同的信息。如果信息需要存储到后台数据库中,如此以来就会产生数据库操作异常提示信息,以至于给用户带来错误信息提示,从而给用户的使用带来不便。
(1) 二是,forward跳转引起的重复提交。当用户将信息提交到服务器,服务器响应采用forward方式调转到下一个页面后,此时地址栏中显示的是上个页面的URL,若刷新当前页面,浏览器会将再次提交用户先前输入的数据,就会再次出现表单重复提交的问题。当然你可以选择redirect方式跳转页面,这样就不会出现重复提交的问题;但有时为了达到某种效果或式。者出于网站安全的目的需要隐藏网页跳转,而不得不采用forward跳转方
2、 对token的简单理解:
(1) 当用户首次访问包含表单的页面时,服务器会在这次会话中创建一个session对象,并产生一个令牌值,然后将这个令牌值作为隐藏输入域的值,随表单一起发送到服务器端,同时将令牌值保存到Session中。
(2) 当用户提交页面时,服务器首先判断请求参数中的令牌值和Session中保存的令牌值是否相等,若相等,则清楚Session中的令牌值,然后执行数据处理操作。如果不相等,则提示用户已经提交过了表单,同时产生一个新的令牌值,保存到Session中。当用户重新访问提交数据页面时,将新产生的令牌值作为隐藏输入域的值。
3、应用步骤:
(1)struts.xml配置文件中添加token拦截器
1
2
3
4
|
<action name=
"doAddParameter"
class
=
"com.do.action.CaAction"
method=
"doAddParameter"
>
<interceptor-ref name=
"defaultStack"
/>
<interceptor-ref name=
"token"
/>
<result name=
"success"
type=
"redirect"
>/car/listParameter.action?calculator_product_id=${#request.calculator_product_id}</result>
|
1
2
|
<span style=
"line-height:18px;font-family:'Courier New';font-size:12px;background-color:#F5F5F5;"
> </span><span style=
"line-height:18px;font-family:'Courier New';font-size:12px;background-color:#F5F5F5;"
><!--</span><span style=
"line-height:18px;font-family:'Courier New';font-size:12px;background-color:#F5F5F5;"
> 如果重复提交,跳转到。。。jsp页面 </span><span style=
"line-height:18px;font-family:'Courier New';font-size:12px;background-color:#F5F5F5;"
>--></span> <result name=
"<span style="
font-family:
'Courier New'
;font-size:12px;line-height:18px;background-color:#F5F5F5;
">invalid.token</span>"
type=
"redirect"
>/ca/addParameter.action?calculator_product_id=${#request.calculator_product_id}</result>
</action>
|
<interceptor-ref name="token" />
(2)jsp页面中在form表单中添加<s:token></s:token>,并且在jsp头上引入<%@ taglib uri="/struts-tags" prefix="s"%>
4、源码分析:
(1)<s:token>标签在struts-tags.tld的定义:
1
2
3
4
5
6
7
8
9
10
11
12
|
<tag>
<name>token</name>
<tag-
class
>org.apache.struts2.views.jsp.ui.TokenTag</tag-
class
>
<body-content>JSP</body-content>
<description><![CDATA[Stop
double
-submission of forms]]></description>
<attribute>
<name>accesskey</name>
<required>
false
</required>
<rtexprvalue>
false
</rtexprvalue>
<description><![CDATA[Set the html accesskey attribute on rendered html element]]></description>
</attribute>
。。。。
|
(2)TokenTag.java的源码:
1
2
3
4
5
6
7
8
9
10
11
|
/**
* @see Token
*/
public
class
TokenTag
extends
AbstractUITag {
private
static
final
long
serialVersionUID = 722480798151703457L;
public
Component getBean(ValueStack stack, HttpServletRequest req, HttpServletResponse res) {
return
new
Token(stack, req, res);
}
}
|
注释:在这里面new Token对象。
(3)Token.java的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
@StrutsTag
(name=
"token"
, tldTagClass=
"org.apache.struts2.views.jsp.ui.TokenTag"
, description=
"Stop double-submission of forms"
)
public
class
Token
extends
UIBean {
public
static
final
String TEMPLATE =
"token"
;
public
Token(ValueStack stack, HttpServletRequest request, HttpServletResponse response) {
super
(stack, request, response);
}
protected
String getDefaultTemplate() {
return
TEMPLATE;
}
/**
* First looks for the token in the PageContext using the supplied name (or {@link org.apache.struts2.util.TokenHelper#DEFAULT_TOKEN_NAME}
* if no name is provided) so that the same token can be re-used for the scope of a request for the same name. If
* the token is not in the PageContext, a new Token is created and set into the Session and the PageContext with
* the name.
*/
protected
void
evaluateExtraParams() {
super
.evaluateExtraParams();
String tokenName;
Map parameters = getParameters();
(
1
)注释在参数map中查看是否包含name字段,假设没有
if
(parameters.containsKey(
"name"
)) {
tokenName = (String) parameters.get(
"name"
);
}
else
{
if
(name ==
null
) {
tokenName = TokenHelper.DEFAULT_TOKEN_NAME;
//(2)<span></span> } else {
tokenName = findString(name);
if
(tokenName ==
null
) {
tokenName = name;
}
}
addParameter(
"name"
, tokenName);
}
String token = buildToken(tokenName);
addParameter(
"token"
, token);
//(3)保存Token
addParameter(
"tokenNameField"
, TokenHelper.TOKEN_NAME_FIELD);
}
/**
* This will be removed in a future version of Struts.
* @deprecated Templates should use $parameters from now on, not $tag.
*/
public
String getTokenNameField() {
return
TokenHelper.TOKEN_NAME_FIELD;
}
<p>
(
4
)创建Token
</p>
private
String buildToken(String name) {
Map context = stack.getContext();
Object myToken = context.get(name);
if
(myToken ==
null
) {
myToken = TokenHelper.setToken(name);
context.put(name, myToken);
}
return
myToken.toString();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public
static
String setToken(String tokenName) {
Map session = ActionContext.getContext().getSession();
String token = generateGUID();
try
{
session.put(tokenName, token);
}
catch
(IllegalStateException e) {
// WW-1182 explain to user what the problem is
String msg =
"Error creating HttpSession due response is commited to client. You can use the CreateSessionInterceptor or create the HttpSession from your action before the result is rendered to the client: "
+ e.getMessage();
LOG.error(msg, e);
throw
new
IllegalArgumentException(msg);
}
return
token;
}
|
注释:产生一个UUID,并且保存到session中.
上面的步骤中Token已经创建好了,并且保存到了session中,现在我们看看拦截器是怎么处理的?
(5)Struts2的内置拦截器<interceptor name="token" class="org.apache.struts2.interceptor.TokenInterceptor"/>
中TokenInterceptor.java 的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
*
@see
TokenSessionStoreInterceptor
*
@see
TokenHelper
*/
public
class
TokenInterceptor
extends
MethodFilterInterceptor {
private
static
final
long
serialVersionUID = -6680894220590585506L;
public
static
final
String INVALID_TOKEN_CODE =
"invalid.token"
;
/**
* @see com.opensymphony.xwork2.interceptor.MethodFilterInterceptor#doIntercept(com.opensymphony.xwork2.ActionInvocation)
*/
protected
String doIntercept(ActionInvocation invocation)
throws
Exception {
if
(log.isDebugEnabled()) {
log.debug(
"Intercepting invocation to check for valid transaction token."
);
}
//see WW-2902: we need to use the real HttpSession here, as opposed to the map
//that wraps the session, because a new wrap is created on every request
HttpSession session = ServletActionContext.getRequest().getSession(
true
);
synchronized
(session) {
//(1)判断Token是否有效
if
(!TokenHelper.validToken()) {
(
2
)Token无效,返回结果invalid.token
return
handleInvalidToken(invocation);
}
}
//(3)Token有效时,去做更多的处理
return
handleValidToken(invocation);
}
/**
* Determines what to do if an invalid token is provided. If the action implements {@link ValidationAware}
*
* @param invocation the action invocation where the invalid token failed
* @return the return code to indicate should be processed
* @throws Exception when any unexpected error occurs.
*/
protected
String handleInvalidToken(ActionInvocation invocation)
throws
Exception {
Object action = invocation.getAction();
String errorMessage = LocalizedTextUtil.findText(
this
.getClass(),
"struts.messages.invalid.token"
,
invocation.getInvocationContext().getLocale(),
"The form has already been processed or no token was supplied, please try again."
,
new
Object[
0
]);
if
(action
instanceof
ValidationAware) {
((ValidationAware) action).addActionError(errorMessage);
}
else
{
log.warn(errorMessage);
}
return
INVALID_TOKEN_CODE;
}
/**
* Called when a valid token is found. This method invokes the action by can be changed to do something more
* interesting.
*
* @param invocation the action invocation
* @throws Exception when any unexpected error occurs.
*/
protected
String handleValidToken(ActionInvocation invocation)
throws
Exception {
return
invocation.invoke();
}
}
|
(6)查看Token是否有效TokenHelper.validToken()源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
|
public
static
boolean
validToken() {
String tokenName = getTokenName();
//(1)获取tokenName
if
(tokenName ==
null
) {
if
(LOG.isDebugEnabled()) {
LOG.debug(
"no token name found -> Invalid token "
);
}
return
false
;
}
String token = getToken(tokenName);
//(2)获取token的值,注意这是页面上传来的
if
(token ==
null
) {
if
(LOG.isDebugEnabled()) {
LOG.debug(
"no token found for token name "
+tokenName+
" -> Invalid token "
);
}
return
false
;
}
//(3)在session中获取token值
Map session = ActionContext.getContext().getSession();
String sessionToken = (String) session.get(tokenName);
//(4)比较2个token是否一致
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
//(5)token合法,把session中的token删除
session.remove(tokenName);
return
true
;
}
public
static
String getTokenName() {
Map params = ActionContext.getContext().getParameters();
if
(!params.containsKey(TOKEN_NAME_FIELD)) {
if
(LOG.isWarnEnabled()) {
LOG.warn(
"Could not find token name in params."
);
}
return
null
;
}
String[] tokenNames = (String[]) params.get(TOKEN_NAME_FIELD);
String tokenName;
if
((tokenNames ==
null
) || (tokenNames.length <
1
)) {
if
(LOG.isWarnEnabled()) {
LOG.warn(
"Got a null or empty token name."
);
}
return
null
;
}
tokenName = tokenNames[
0
];
return
tokenName;
}
public
static
String getToken(String tokenName) {
if
(tokenName ==
null
) {
return
null
;
}
Map params = ActionContext.getContext().getParameters();
String[] tokens = (String[]) params.get(tokenName);
String token;
if
((tokens ==
null
) || (tokens.length <
1
)) {
if
(LOG.isWarnEnabled()) {
LOG.warn(
"Could not find token mapped to token name "
+ tokenName);
}
return
null
;
}
token = tokens[
0
];
return
token;
}
|