概述
有些时候,我们在想某个网页提交了信息之后,由于某些原因,我们会重复点击提交,或者刷新页面,或者是在提交页面呈现之后点击后退按钮,从而导致这些表单数据被重复提交。在大多数情况下我们是不希望这种情况发生的,我们不可能强迫使用者不这么做,那么我们就只能自己想办法来尽量避免这些情况了。
下面我们来看一个例子:
public class HelloServlet extends HttpServlet {
private int i = 0;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String userName = req.getParameter("name");
String password = req.getParameter("password");
int count = 0;
for (int i = 0; i < 1000; i++) {
count++;
}
resp.getWriter().println(
i + ": hello" + userName + " your password is " + password);
i++;
}
}
input.jsp
<body>
<form action="Hello"method="post">
<input type="text"name="name"><br>
<input type="text"name="password"><br>
<input type="submit"value="submit">
</form>
</body>
测试(提交后刷新页面):
我们看到,在提交之后,刷新页面,同样会触发服务端的操作!
防止重复提交
对于防止表单重复提交,网上的朋友想出了很多方案。比如有用javascript来实现检测的,有通过提交后进行页面跳转来防止重复提交的,也有利用cookie来检测是否重复提交的。也有人利用session来判断是否重复提交的。当然,这些方法都有他们的用处,我个人觉得利用session来防止重复提交是最佳的方案。javascript防止不了页面刷新,重定向防止不了页面后退。对于cookie和session,由于session保存在服务端,所以要安全一点。(当然大多时候session也是基于cookie的)。Struts2中提供了一个专门的token标签来实现防止表单重复提交。基本原理就是我们在表单输入页面呈现之前生成一个独一无二的标记,将它放到表单页面(一般放在hidden表单中),并且同时将这个标记保存到session中。那么当页面提交时,我们首先检查请求参数中的标记与session中的标记是否相同,如果相同,将session中的标记删除,这次提交就是第一次提交。那么当表单重复提交时,由于session中的标记已经在第一次提交的时候被我们删掉了,因此在检测的时候就通不过,由此来判断表单为重复提交,再做出相应的处理。
jsp/servlet实现session方式防止重复提交
InputServlet.java
public class InputServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String token = String.valueOf(System.currentTimeMillis());
req.setAttribute("token",token);
req.getSession().setAttribute("token",token);
RequestDispatcher rd = req.getRequestDispatcher("/input.jsp");
rd.forward(req,resp);
}
}
HelloServlet.java
public class HelloServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String reqToken = req.getParameter("token");
String sessionToken = (String) req.getSession().getAttribute("token");
if (reqToken == null || !reqToken.equals(sessionToken)) {
resp.getWriter().println("repeat submit");
return;
}
//删除session中的token
req.getSession().removeAttribute("token");
String userName = req.getParameter("name");
String password = req.getParameter("password");
int count = 0;
for (int i = 0; i < 1000; i++) {
count++;
}
resp.getWriter().println(
": hello" + userName + " your password is " + password);
}
}
input.jsp
<body>
<form action="Hello"method="post">
<input type="text" name="name">
<br>
<input type="text" name="password">
<br>
<input type="hidden" name="token"
value=<%=request.getAttribute("token")%>/>
<br />
<input type="submit" value="submit">
</form>
</body>
测试(访问InputServlet):
这时我们查看页面源代码:
可以看到增加了一个hidden类型的token表单。
提交页面:
我们刷新页面:
Struts2实现防止表单重复提交
Struts2中的token标签可以用来生成一个唯一的标记,这个标记必须嵌套在form标签内使用,它将在表单里插入一个隐藏字段并把标记保存到Session中。这个token标签必须和token拦截器或者tokenSession拦截器配合使用,这两个拦截器的差别就是,使用token拦截器,如果发现表单重复提交,那么会返回”invalid.token”结果并且加上一条Action error,这个默认消息是:The form has already bean processed or no token was supplied,pleasetry agin.这个消息看起来可能会让用户觉得以为是让他们”try agin”,要想覆盖这个消息,那么可以新建一个TokenInterceptor.properties添加一个键为struts.messages.invalid.token的消息,这个文件必须放在/WEB_INF/classes/org/apache/struts2/interceptor下。这个操作起来很麻烦。一般情况下只要我们将用户重复提交的请求忽略掉就可以了,那么tokenSession拦截器正是我们想要的。
需要注意的是是这两个拦截器都没在defaultStack拦截器栈中,需要我们手动添加到Action的拦截器栈中。
tokenSession拦截器对应的实现类是org.apache.struts2.interceptor.TokenSessionStoreInterceptor,它是token拦截器的实现类org.apache.struts2.interceptor.TokenInterceptor的子类。我们查看他们的源代码:
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) {
if (!TokenHelper.validToken()) {
return handleInvalidToken(invocation);
}
}
return handleValidToken(invocation);
}
TokenHelper类的validToken方法:
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 sessionToken = (String) session.get(tokenName);
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(tokenName);
return true;
}
从中我们看以看出,处理的基本思想和我们前面使用jsp/servlet实现的差不多。只是token的生成转交的token标签来做,验证交给了拦截器来做。这整个过程都不许我们做任何操作.
下面动手做一个实例:
public class SubmitAction extends ActionSupport implements SessionAware {
private String name;
private String password;
private Map<String, Object> session;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setSession(Map<String, Object> session) {
this.session = session;
}
@Override
public String execute() throws Exception {
return SUCCESS;
}
}
input.jsp
<formaction="submit.action"method="post">
name : <input type="text" name="name"/><br/>
password : <input type="text" name=password/><br>
<s:token></s:token>
<input type="submit"value="submit"/>
</form>
struts.xml
<packagename="default"namespace="/"extends="struts-default">
<action name="submit"class="action.SubmitAction">
<result name="success">/success.jsp</result>
<result name="input">/input.jsp</result>
<interceptor-ref name="tokenSession"></interceptor-ref>
<interceptor-ref name="defaultStack"></interceptor-ref>
</action>
</package>
success.jsp
<body>
hello ${name } your password is ${password}
</body>
测试:提交表单后多次刷新,页面没有看到的是同样的响应,就像提交了一次一样。实际上重复的提交未做处理。如果开启了dev模式,那么会从控制台从看到一些信息:
警告: Form token5XIHXGFEA4F15ZJKN07B88V9956ZAMPN does not match the session token null.
其实只要理解了这些原理,不管是用jsp/servlet亦或是php都能够实现出来!