欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring MVC防止重复提交最佳实践 重复提交常见问题分布式锁memcached

程序员文章站 2024-01-09 17:32:22
...

防止表单重复提交是个老生常谈的问题,有些框架层面已经有实现,比如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

上一篇:

下一篇: