首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实战之防止表单重复提交

实战之防止表单重复提交

作者头像
山行AI
发布2019-06-28 16:38:40
2.8K0
发布2019-06-28 16:38:40
举报
文章被收录于专栏:山行AI山行AI山行AI

防止重复提交

对于防止重复提交,最简单也最不安全的做法相信大家也都经历过,前端在一个请求发送后立即禁用掉按钮,这里咱们来讨论一下后端对防止重复提交的处理方式。 主要针对非分布式环境下防止重复提交与分布式环境下的防止重复提交。一般分布式环境下也可以通过网关路由的方式将同一个用户的请求路由到一个实例上处理。

单进程内的防止重复提交

单个进程内防止重复提交可以选取的方式有很多种,因为并不是每一个接口都需要做防止重复提交的校验,所以在java中通常采用注解+拦截器的方式来实现。 另外一点就是,针对每一个接口的每一次请求都要有一个与之相对应的key来做去重操作。在当有一个key的请求正在处理时,另一个携带相同key的请求会被拒绝掉。key 的取值取决于系统对接口和资源的切分粒度。 废话不说,直接上代码:

1. 注解
@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;        }    }
}
  • 在每个有效用户访问时,平台会为该用户颁发一个token,这个token在pc端是放在cookie中的,移动端是放在header中的。
  • 在有些时候,这个token可以简单地使用ip来替换(针对一台交换机上过来的公网请求ip,这种方式会有问题)。
  • 在上面注解中是通过接口方法名与token或当前用户id来切分资源的。

拦截器里面的处理:

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实现分布式锁的几种方式和需要注意的点,请关注之后的文章。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开发架构二三事 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 防止重复提交
    • 单进程内的防止重复提交
      • 1. 注解
  • 分布式环境下防止重复提交
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档