Spring MVC防止重复提交最佳实践

防止表单重复提交是个老生常谈的问题,有些框架层面已经有实现,比如Struts2中的token,但Spring MVC中并未找到相应的功能,只能自己实现。

 

网上搜索“Spring MVC防止重复提交”,会有一大推的案例实现,但多数都存在以下几个问题或者不便:

  1. 防止重复提交页面需要添加隐藏域,类似<input type=”hidden” name=”token” value=”${token}”>,如果页面很多,会是一个体力活;
  2. 如果后台服务时分布式的,放入session中的值如何在另一台服务器获取;
  3. 多服务器多实例部署后,多个请求同时验证,有同时通过验证的可能,比如A服务器和B服务器都去Redis中验证是否存在token,同时通过验证后再保存操作,也会出现重复提交的问题。

针对以上问题,我们先进行分析一下:

  1. token机制肯定是服务器端产生的并且需要传递给客户端,然后客户端提交请求时带着该token。那有什么机制能保证不改客户端的代码,访问服务器时自动带上token,答案是利用cookie机制;当服务器产生token后,放入cookie,该token自动保存到客户端的cookie,客户端提交请求时,会默认带着所有的cookie信息。
  2. 针对该问题还是有好多解决方法,方法一:我们可以把token存入到一个公共地方,比如Redis,memcached中(需要考虑多用户同时生成token问题,放入规则可以userId+token);方法二:使用插件做到session共享,这样多个服务器都能获取同样的session。
  3. 第三个问题是典型的分布式锁问题,同一时刻不能2个线程去校验,否则会出现同时验证通过的问题。

以下是代码实现,基础框架参照的网上示例,session使用了插件做了session共享;分布式锁,由于使用的是memcached,所以使用memcached的add方式实现分布式锁(参照http://timyang.net/programming/memcache-mutex/)。

 

实现步骤:

  1. 新建注解
/**
 * <p>
 * 防止重复提交注解,用于方法上<br/>
 * 在新建页面方法上,设置needSaveToken()为true,此时拦截器会在Session中保存一个token,
 * 同时需要在新建的页面中添加
 * <input type="hidden" name="token" value="${token}">
 * <br/>
 * 保存方法需要验证重复提交的,设置needRemoveToken为true
 * 此时会在拦截器中验证是否重复提交
 * </p>
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface AvoidDuplicateSubmit {
    boolean needSaveToken() default false;
    boolean needRemoveToken() default false;
}

 

    2.定义切面(处理逻辑)

 

@Aspect
@Component
public class AvoidDuplicateSubmitAspect {
    
    @Autowired(required=false)
    private SessionServiceImpl sessionService;
    private static ICacheService<?> cacheService = CacheFactory.getCache();
    private static IMemcachedCache memcachedCache = null;
    
    static {
        if(cacheService instanceof RemoteCacheServiceImpl) {
            memcachedCache = ((RemoteCacheServiceImpl)cacheService).getCache();
        }
    }
    
    
    @Before("@annotation(sec)")
    public void execute(JoinPoint jp,AvoidDuplicateSubmit sec) { 
        //JoinPoint会获取到注解所在方法的参数
        Object[] args = jp.getArgs();
        //使用该注解的方法参数第一个必须是HttpServletRequest,第二个必须是HttpServletResponse  
        HttpServletRequest request = (HttpServletRequest) args[0];
        HttpServletResponse response = (HttpServletResponse) args[1];
        
        boolean needSaveSession = sec.needSaveToken();
        if (needSaveSession) {
            String uuid = UUID.randomUUID().toString();
            request.getSession(false).setAttribute("token", uuid);
            CookieUtil.addCookie(response, "token", uuid, 0);
        }

        boolean needRemoveSession = sec.needRemoveToken();
        if (needRemoveSession) {
            String serverToken = (String) request.getSession(false).getAttribute("token");
            Cookie c = CookieUtil.getCookieByName(request, "token");
             String clientToken = c.getValue();
            if (isRepeatSubmit(serverToken, clientToken)) {
                throw new ValidateException("请勿重复提交!");
            }
            //校验通过后从session中删除token 
            request.getSession(false).removeAttribute("token");
            if(null != memcachedCache) {
                //删除memcached锁
                memcachedCache.delete(serverToken);
            }
        }
        
    }
    
    private boolean isRepeatSubmit(String serverToken, String clientToken) {
        if (serverToken == null) {
            return true;
        }
        
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.MINUTE, 1);
        //memcached add 失败,即没有获取到锁,返回true
        if(null != memcachedCache && !memcachedCache.add(serverToken, 1, cal.getTime())) {
            return true;
        }
        
        if (clientToken == null) {
            return true;
        }
        if (!serverToken.equals(clientToken)) {
            return true;
        }
        return false;
    }
    
}

    

    3.在spring dispatcher-servlet.xml中开启该注解

 

<aop:aspectj-autoproxy proxy-target-class="true">
    <aop:include name="avoidDuplicateSubmitAspect"/>
</aop:aspectj-autoproxy>
<bean id="avoidDuplicateSubmitAspect" class="com.xuehuifei.annotation.submit.AvoidDuplicateSubmitAspect"></bean>

   

    4.接口层使用

 

//页面展现接口,添加生成token注解
@RequestMapping(value = "/{userId}", method = RequestMethod.GET)
@AvoidDuplicateSubmit(needSaveToken=true)
public @ResponseBody ResultObject list(HttpServletRequest request, HttpServletResponse response, @PathVariable Long userId) {
        ResultObject obj = new ResultObject();
        return obj;
}

//提交请求接口,添加删除token注解
@RequestMapping(method=RequestMethod.POST)   
@AvoidDuplicateSubmit(needRemoveToken=true)
public @ResponseBody ResultObject save(HttpServletRequest request, HttpServletResponse response, HostModel hostModel) {
        ResultObject obj = new ResultObject();      
        return obj;
}  

 

ps:cookie保存token机制还设计到客户端禁用cookie的问题,该文章忽略这种情况。

 

http://www.xuehuifei.com/934.html

猜你喜欢

转载自speed.iteye.com/blog/2325594
今日推荐