论坛首页 Java企业应用论坛

防止表单重复提交机制在JSF2中的实现

浏览 5773 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2012-03-28   最后修改:2012-03-30
在B/S系统开发过程中,关于如何防止表单的重复提交问题(补充: 这里的防止重复提交,是指用户使用浏览器的回退按钮退回来再次提交同一个表单的情况),也是一个老生常谈的问题,这里说说如何在JSF2的开发环境下防止表单重复提交。

问题解决的思路基本和struts的思路是一致的,那就是

1.生成一个字符串(token),放置在session里,
2.在表单生成时,同时把这个token作为表单的一部分,放置在一个hidden input中,
3.表单提交时,在backingbean中验证一下页面提交过来的token是否和session中的一致。
4.业务完成之后,重置一下token.

因为如果是用浏览器的后退按钮退回到表单页面的话,表单的内容是不会变化的,包括表单里面的token,这样在后退再
提交的时候,由于session中的token已经重置,这时候,我们就认为提交是失败的。

具体实现比较简单,经过2次重构,已经有了比较友好的使用体验。

首先是一个session级的bean, 用它来存储和操作token


/**
 * @author Bill
 * @version 2012-03-21
 */
@SessionScoped
@ManagedBean
public class FormTokenBean {

    public static final String BEAN_NAME = "formTokenBean";

    private String token;

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public String resetToken() {
        return token = "T" + System.nanoTime();
    }

    public boolean validateToken(String token) {
        return token != null && token.equals(this.token);
    }

    @PostConstruct
    public void init () {
        resetToken();
    }

}


然后需要一个Tag,

/**
 * @author Bill
 * @version 2012-03-27
 */
@FacesComponent("org.billxiong.faces.FormToken")
public class FormTokenTag extends HtmlInputHidden{

    public FormTokenTag() {
        setRendererType("javax.faces.Hidden"); // render as a standard InputHidden
        addValidator(new FormTokenValidator());

        String token = FacesUtils.getObject("formTokenBean.token", String.class); 
        setValue(token);
    }

    @Override
    public void decode(FacesContext context) {
        super.decode(context);

        String clientId = getClientId(context);
        String submittedValue = (String) context.getExternalContext().getRequestParameterMap().get(clientId);

        if(submittedValue != null) {
            setSubmittedValue(submittedValue);
        }
    }

}


在taglib中注册组件,
    <tag>
        <tag-name>formToken</tag-name>
        <component>
            <component-type>org.billxiong.faces.FormToken</component-type>
        </component>
        <attribute>
            <name>id</name>
            <required>false</required>
            <type>java.lang.String</type>
        </attribute>
        <attribute>
            <name>validatorMessage</name>
            <required>false</required>
            <type>java.lang.String</type>
        </attribute>
    </tag>


如何验证Token是否有效呢?根据JSF的特点,编写一个Validator,
@FacesValidator("formTokenValidator")
public class FormTokenValidator implements Validator{
    @Override
    public void validate(FacesContext context, UIComponent uiComponent, Object o) throws ValidatorException {
        String token = o == null ? null : o.toString();

        FormTokenBean tokenBean = FacesUtils.getObject(FormTokenBean.BEAN_NAME, FormTokenBean.class);

        if (null == token || null == tokenBean || !tokenBean.validateToken(token)) {
            throw new ValidatorException(new FacesMessage(FacesMessage.SEVERITY_ERROR, FacesUtils.getMessage("global.exception.tokenExpired"), 

""));
        }
    }
}


在validator中检查一下是不是和session中一致。

最后,看看页面中的使用,
        <h:form prependId="false">
            <pgfn:formToken/>

            <h:messages errorClass="error-msgs" errorStyle="color: red;"/>

            <h:commandButton id="btnSubmit" action="#{xxxBean.xxxMethod}" value="Submit}"            

        </h:form>


业务方法执行完毕,需要重置一下Token。

总结:得益于JSF2的大幅改进,使得编写一个标签组件是如此的容易,另外,也要感谢一下struts提供的思路

本文系原创,首发Iteye.com, 作者: Bill
   发表时间:2012-03-30  
一点建议:在组件中直接通过ExternalContext操作Session比较好,组件反过来依赖一个ManagedBean有点乱了,不利于分包和移植。
0 请登录后投票
   发表时间:2012-03-30   最后修改:2012-03-30
你这不是“防止页面重复提交”,而是“防止后台重复处理”。

不要认为我把东西给后台,后台不处理就很好,应该做到“不该给后台的东西,不要去打扰后台,后台很忙的,快递(网络传输)很累的”。即要注意减少网络流量,特别是在JSF这种流量大的技术。
你的做法属于“我就喜欢烦你,你要嫌烦,可以不理我”。推荐的做法是“能自己搞好的事就不要麻烦别人”。
最好是同时在页面做些js的处理,比如在ajax请求时记住formid,然后ajax响应回来时清除记住的formid。如果有一个请求过来,看看下有没有这个formid的记录,如果有直接返回(可以给点提示)。
0 请登录后投票
   发表时间:2012-03-30  
kidneyball 写道
一点建议:在组件中直接通过ExternalContext操作Session比较好,组件反过来依赖一个ManagedBean有点乱了,不利于分包和移植。

原本是没有这个组件的,直接用的InputHidden做试验。单独写成标签后,也就懒的再把validator和bean去掉了,留作以后改进吧
0 请登录后投票
   发表时间:2012-03-30  
mfkvfn 写道
你这不是“防止页面重复提交”,而是“防止后台重复处理”。

不要认为我把东西给后台,后台不处理就很好,应该做到“不该给后台的东西,不要去打扰后台,后台很忙的,快递(网络传输)很累的”。即要注意减少网络流量,特别是在JSF这种流量大的技术。
你的做法属于“我就喜欢烦你,你要嫌烦,可以不理我”。推荐的做法是“能自己搞好的事就不要麻烦别人”。
最好是同时在页面做些js的处理,比如在ajax请求时记住formid,然后ajax响应回来时清除记住的formid。如果有一个请求过来,看看下有没有这个formid的记录,如果有直接返回(可以给点提示)。

感谢拍砖,这个是用validator的方式,请求是不会到后台的吧,校验不通过的话,JSF会直接输出页面。
0 请登录后投票
   发表时间:2012-03-30  
几年前会采用后台处理,现在全部处理,后台不做太多处理
0 请登录后投票
   发表时间:2012-03-30  
xzdmms 写道
mfkvfn 写道
你这不是“防止页面重复提交”,而是“防止后台重复处理”。

不要认为我把东西给后台,后台不处理就很好,应该做到“不该给后台的东西,不要去打扰后台,后台很忙的,快递(网络传输)很累的”。即要注意减少网络流量,特别是在JSF这种流量大的技术。
你的做法属于“我就喜欢烦你,你要嫌烦,可以不理我”。推荐的做法是“能自己搞好的事就不要麻烦别人”。
最好是同时在页面做些js的处理,比如在ajax请求时记住formid,然后ajax响应回来时清除记住的formid。如果有一个请求过来,看看下有没有这个formid的记录,如果有直接返回(可以给点提示)。

感谢拍砖,这个是用validator的方式,请求是不会到后台的吧,校验不通过的话,JSF会直接输出页面。


mfkvfn说的不错,Validation发生在JSF后台处理的第4阶段,在这之前的Restoer View就比较耗资源了,如果你采用了View State放在客户端的方式,提交请求还会有比较可观的带宽开销,应该是在客户端把这事办了。不过只靠客户端来校验也是不靠谱的,一不小心就会被其他代码干扰了就把你的校验代码绕过了,最好是客户端和服务器端双管齐下。你反正都已经抽出一个组件来了,完全可以写个Renderer加入客户端的校验,这样只要把你的组件一放,客户端和服务器端都搞定了,这才是JSF的优势。
0 请登录后投票
   发表时间:2012-03-30  
kidneyball 写道
xzdmms 写道
mfkvfn 写道
你这不是“防止页面重复提交”,而是“防止后台重复处理”。

不要认为我把东西给后台,后台不处理就很好,应该做到“不该给后台的东西,不要去打扰后台,后台很忙的,快递(网络传输)很累的”。即要注意减少网络流量,特别是在JSF这种流量大的技术。
你的做法属于“我就喜欢烦你,你要嫌烦,可以不理我”。推荐的做法是“能自己搞好的事就不要麻烦别人”。
最好是同时在页面做些js的处理,比如在ajax请求时记住formid,然后ajax响应回来时清除记住的formid。如果有一个请求过来,看看下有没有这个formid的记录,如果有直接返回(可以给点提示)。

感谢拍砖,这个是用validator的方式,请求是不会到后台的吧,校验不通过的话,JSF会直接输出页面。


mfkvfn说的不错,Validation发生在JSF后台处理的第4阶段,在这之前的Restoer View就比较耗资源了,如果你采用了View State放在客户端的方式,提交请求还会有比较可观的带宽开销,应该是在客户端把这事办了。不过只靠客户端来校验也是不靠谱的,一不小心就会被其他代码干扰了就把你的校验代码绕过了,最好是客户端和服务器端双管齐下。你反正都已经抽出一个组件来了,完全可以写个Renderer加入客户端的校验,这样只要把你的组件一放,客户端和服务器端都搞定了,这才是JSF的优势。


如果非要纠结带宽的问题,恐怕JSF不是个好选择。对View组件树的恢复和处理,多这么一个组件也不多吧。
我们项目在实际应用当中,在不少的场景也是结合使用了JSF2的ajax。目前看来,还没有什么性能的问题。
0 请登录后投票
   发表时间:2012-03-30  
xzdmms 写道
kidneyball 写道
xzdmms 写道
mfkvfn 写道
你这不是“防止页面重复提交”,而是“防止后台重复处理”。

不要认为我把东西给后台,后台不处理就很好,应该做到“不该给后台的东西,不要去打扰后台,后台很忙的,快递(网络传输)很累的”。即要注意减少网络流量,特别是在JSF这种流量大的技术。
你的做法属于“我就喜欢烦你,你要嫌烦,可以不理我”。推荐的做法是“能自己搞好的事就不要麻烦别人”。
最好是同时在页面做些js的处理,比如在ajax请求时记住formid,然后ajax响应回来时清除记住的formid。如果有一个请求过来,看看下有没有这个formid的记录,如果有直接返回(可以给点提示)。

感谢拍砖,这个是用validator的方式,请求是不会到后台的吧,校验不通过的话,JSF会直接输出页面。


mfkvfn说的不错,Validation发生在JSF后台处理的第4阶段,在这之前的Restoer View就比较耗资源了,如果你采用了View State放在客户端的方式,提交请求还会有比较可观的带宽开销,应该是在客户端把这事办了。不过只靠客户端来校验也是不靠谱的,一不小心就会被其他代码干扰了就把你的校验代码绕过了,最好是客户端和服务器端双管齐下。你反正都已经抽出一个组件来了,完全可以写个Renderer加入客户端的校验,这样只要把你的组件一放,客户端和服务器端都搞定了,这才是JSF的优势。


如果非要纠结带宽的问题,恐怕JSF不是个好选择。对View组件树的恢复和处理,多这么一个组件也不多吧。
我们项目在实际应用当中,在不少的场景也是结合使用了JSF2的ajax。目前看来,还没有什么性能的问题。


呵呵,“如果。。。”嘛,如果没有把ViewState放在客户端那就没带宽问题了。默认情况下ViewState就放在服务器端,占用服务器空间,不过JSF2采用局部ViewState,如果页面动态性不大也没啥问题。关于RestoreView,不是个别组件的问题,虽然JSF用了局部ViewState,但它还是要把整棵组件树恢复出来的(只不过没改变的部分使用缓存的原始状态组件记录),而且接下来的ApplyRequestValue和Convert也需要遍历组件树两次。如果你在客户端把校验干了,就根本不会有这部分开销。当然,如果你实际使用没有出现性能问题,就没必要专门做额外的优化。只不过在很多ajax系统中,客户端和服务器双校验是标配了。

顺便说下,除非你的客户坚决坚定坚持立誓保证这个产品绝对绝对不会考虑用移动设备访问,否则带宽还是应该考虑一下的,能省一点是一点,真金白银呀。 
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics