Spring MVC防止重复提交最佳实践 重复提交常见问题分布式锁memcached
程序员文章站
2024-01-09 17:32:22
...
防止表单重复提交是个老生常谈的问题,有些框架层面已经有实现,比如Struts2中的token,但Spring MVC中并未找到相应的功能,只能自己实现。
网上搜索“Spring MVC防止重复提交”,会有一大推的案例实现,但多数都存在以下几个问题或者不便:
- 防止重复提交页面需要添加隐藏域,类似<input type=”hidden” name=”token” value=”${token}”>,如果页面很多,会是一个体力活;
- 如果后台服务时分布式的,放入session中的值如何在另一台服务器获取;
- 多服务器多实例部署后,多个请求同时验证,有同时通过验证的可能,比如A服务器和B服务器都去Redis中验证是否存在token,同时通过验证后再保存操作,也会出现重复提交的问题。
针对以上问题,我们先进行分析一下:
- token机制肯定是服务器端产生的并且需要传递给客户端,然后客户端提交请求时带着该token。那有什么机制能保证不改客户端的代码,访问服务器时自动带上token,答案是利用cookie机制;当服务器产生token后,放入cookie,该token自动保存到客户端的cookie,客户端提交请求时,会默认带着所有的cookie信息。
- 针对该问题还是有好多解决方法,方法一:我们可以把token存入到一个公共地方,比如Redis,memcached中(需要考虑多用户同时生成token问题,放入规则可以userId+token);方法二:使用插件做到session共享,这样多个服务器都能获取同样的session。
- 第三个问题是典型的分布式锁问题,同一时刻不能2个线程去校验,否则会出现同时验证通过的问题。
以下是代码实现,基础框架参照的网上示例,session使用了插件做了session共享;分布式锁,由于使用的是memcached,所以使用memcached的add方式实现分布式锁(参照http://timyang.net/programming/memcache-mutex/)。
实现步骤:
- 新建注解
/** * <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的问题,该文章忽略这种情况。