对于防止重复提交,最简单也最不安全的做法相信大家也都经历过,前端在一个请求发送后立即禁用掉按钮,这里咱们来讨论一下后端对防止重复提交的处理方式。 主要针对非分布式环境下防止重复提交与分布式环境下的防止重复提交。一般分布式环境下也可以通过网关路由的方式将同一个用户的请求路由到一个实例上处理。
单个进程内防止重复提交可以选取的方式有很多种,因为并不是每一个接口都需要做防止重复提交的校验,所以在java中通常采用注解+拦截器的方式来实现。 另外一点就是,针对每一个接口的每一次请求都要有一个与之相对应的key来做去重操作。在当有一个key的请求正在处理时,另一个携带相同key的请求会被拒绝掉。key 的取值取决于系统对接口和资源的切分粒度。 废话不说,直接上代码:
@Documented@Inherited@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface SubmitPassport {
boolean validate() default true;
String methodName() default "";
/** * 0表示普通的rest接口,9表示html接口 * @return */ int interfaceType() default 0;
/** * 用户单线程 * 会使用当前用户id做一个分布式锁来控制 * @return */ boolean userSingleThread() default false;
int time() default 5;
enum InterfaceType{ /** * 普通的 */ Normal(0), /** * ftl格式的 */ FTL(9);
int code;
InterfaceType(int code) { this.code = code; }
public int getCode() { return code; } }
}
拦截器里面的处理:
public class SubmitInterceptor extends HandlerInterceptorAdapter {
private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder() // 最大缓存 100 个 .maximumSize(1000) // 设置写缓存后 5 秒钟过期 .expireAfterWrite(5, TimeUnit.SECONDS) .build();
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException, ServletException { if (handler.getClass().isAssignableFrom(HandlerMethod.class)) { SubmitPassport submitPassport = ((HandlerMethod) handler).getMethodAnnotation(SubmitPassport.class); // 没有声明需要权限,或者声明不验证权限 if (submitPassport == null || submitPassport.validate() == false) { return true; } else { Object attribute = request.getAttribute(BaseGlobalConstants.CURRENT_USER); if(submitPassport.userSingleThread() && attribute != null){ //有些操作是需要在同一时间同一用户只能操作一次的 防止用户多浏览器登录的情况 UserBaseTo userBaseTo = (UserBaseTo) attribute; Long userId = userBaseTo.getId(); String key = submitPassport.methodName() + userId; if (CACHES.getIfPresent(key) != null) { throw new RuntimeException("请勿重复请求"); } }else{ Cookie[] cookies = request.getCookies(); String cookiesId = null; if (cookies != null) { for (Cookie cookie : cookies) { if (OpSysConstants.CSRF_TOKEN_KEY.equals(cookie.getName())) { cookiesId = cookie.getValue(); break; } } } if (cookiesId == null) { String servletPath = request.getServletPath(); String[] split = servletPath.split("/"); if (MBEnum.ANDROID.getValue().equals(split[1]) || MBEnum.IOS.getValue().equals(split[1])) { cookiesId = request.getRemoteAddr(); }else { throw new OpBusinessException(PublicExceptionCodeEnum.EX_ILLEGAL_REQUEST.getCode(),PublicExceptionCodeEnum.EX_ILLEGAL_REQUEST.getMsg()); } } String key = submitPassport.methodName() + cookiesId; if (CACHES.getIfPresent(key) != null) { throw new RuntimeException("请勿重复请求"); } } try { return pjp.proceed(); } catch (Throwable throwable) { throw new RuntimeException("服务器异常"); } finally { //处理完之后移除key CACHES.invalidate(key); } } } return true; }
上面使用的是guava的cache作为容器来存放key的,当然还可以使用concurrentHashMap作为存放key的容器,其他缓存工具比如ehcache等也可以使用。
map操作获取和释放锁的操作如下:
/** * 获取object lock * * @param key * @return */ private boolean tryLock(String key) { if (key == null) { LOGGER.error(" ================the key can not be null"); return false; } String putIfAbsent = sessionIdMap.putIfAbsent(key, key); if (putIfAbsent == null) { return true; } return false; }
/** * 释放锁 * * @param key */ private void releaseLock(String key) { if (key != null) { sessionIdMap.remove(key, key); } }
进程内防止重复提交的特点很明显,就是构建一个锁池,每个需要防止重复提交的请求需要来池中获取锁,每个请求处理完了之后会释放相应的锁,而锁的粒度是根据业务边界而定。
和单进程的实现方式类似,只是这个锁池是分布式的,多个进程来这里申请锁,然后资源利用完之后会释放锁。没错,这就是传说中的分布式锁。其他的操作与单进程内的处理方式一样。关于redis实现分布式锁的几种方式和需要注意的点,请关注之后的文章。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有