请问唯一性验证具体的执行方式,以及消耗。避免重复提交

新手提问 · xjdata · 于 7年前 发布 · 14454 次阅读

大家好,提这个问题还是因为避免重复提交。

比如,咱们站的发布帖子部分,假设 表中有字段,user_id,created_at,content 那么我们建立唯一规则时 一般情况下会进行 user_id,created_at联合唯一。 但是当某些情况下, 我们在点击发布的时候 如果按钮没有通过js进行限定或客户端禁用js,那么我在一个比较短的时间内(点击按钮到收到服务器响应进行跳转这段时间内)有多次进行点击了。或者在提交的时候 每个1秒点一次提交,一共点了10次。 经过个人测试,是会产生10条 逻辑上重复的数据的。 但是表内存储的内容确实是唯一的。

比如

user_idcreated_atcontent
1100内容
1101内容
1102内容
1103内容
1104内容
1105内容
1106内容
1107内容
1108内容
1109内容

这就出现了 逻辑上的重复数据了。

想到不行就联合验证3个字段的内容
[['user_id', 'created_at'], 'unique', 'targetAttribute' => ['user_id','created_at', 'content']],

于是问题是: 1.因为content 是varchar并且内容可能会比较多,那么yii在进行 unique 的时候会不会消耗比较大? 2.对于我说的这种情况,各位有什么好的办法吗? 3.在一个应用中 需要防止重复提交的地方应该是比较多的。如果通过php后台方式进行避免重复,难道每个action中都要进行相应代码的编写吗?那不一堆一堆的重复了?

欢迎大家指导,讨论,谢谢。

本帖已被设为精华帖!
共收到 22 条回复
forecho#17年前 0 个赞

现在我能想到的是:如果不用 JS 的防止的话,就用缓存来防止重复提交,创建一个帖子之后加入缓存,下次创建帖子的时候,文章标题和文章内容对比缓存,如重复就不会有创建操作。

forecho#27年前 0 个赞

其实这个问题我之前也有发现,一直想找一个简单的解决方案,刚开始用的 JS 来控制,但是会有一个 bug,点击提交的时候按钮 disabled 掉,但是如果提交的时候表单验证失败,按钮就变成 disabled 状态了,即使你纠正表单,提交按钮也不能恢复,所以我后来就索性去掉了那个 JS,然后就没有然后了……

Rambone#37年前 0 个赞

完全意义上的重复提交是避免不了的

user_idcreate_at 联合唯一,如果 create_at 两次提交的不一样,这个索引是不生效的。 只有当两者完全相同时,才会拒绝重复,所以看你 created_at 的最小单位了,如果是分钟或者秒,点的慢点,可能都会重复提交。

还有我觉得不该用数据库去约束, 应该用 session 之类的,全站统一一个类似过滤器的东西,在用户所有创建内容的地方,判断 session 去确定是否符合最小间隔时间。

if (time() - session('comment_on_article_1') < 15) {
  return 0;
}
return 1

session 的命名,应该能通过你的内容栏目名字逆向出来,这样就有迹可循了,可以指定类似“每篇文章允许15秒内评论一次” 的规则。

xjdata#57年前 0 个赞

@abrahamgreyson

因为才开始web不久,所以下面的内容可能让你见笑了。还请指导,如果应用有100个需要发布的地方,那么session中的key就有100个,我的问题是,如果有100个用户同时在线,那么session中的存储是不是100*100个?

@zuoRambo

恩,可以这么说,可是我面对的用户年龄都大一点,以前c/s的时候就会有 不小心手一抖就2下鼠标点下去了。所以必须得避免。

@forecho

以下是我暂时不成熟的解决办法。如果有好的方式,还请大家多多分享指导。

view部分

<?php \common\widgets\JsBlock::begin() ?>
    <script>
        $('form#<?=$form->id?>').on('beforeValidate', function (e) {
            $(':submit').attr('disabled', true).addClass('disabled');
        });
        $('form#<?=$form->id?>').on('afterValidate', function (e) {
            if (cheched = $(this).data('yiiActiveForm').validated == false) {
                $(':submit').removeAttr('disabled').removeClass('disabled');
            }
        });
        $('form#<?=$form->id?>').on('beforeSubmit', function (e) {
            $(':submit').attr('disabled', true).addClass('disabled');
        });
    </script>
<?php \common\widgets\JsBlock::end() ?>

controller部分

 public function actionCreate()
{
         if (Yii::$app->request->isPost) {
            if (yii::$app->session->getFlash('sending_' . yii::$app->user->id, false)) {
                //返回发布成功时应该返回的页面,但是也有问题,就是如果出现js判断不了的错误,那因为第二次或n次提交造成,之前的model中的error信息无法显示了。但暂时我还是使用了这个方法.
                return $this->redirect(['client/view', 'id' => $model->client_id]);
            }
            //...
            //...
        }
        yii::$app->session->setFlash('sending_' . yii::$app->user->id);
         return $this->render('create', [
            'model' => $model,
        ]);
}

虽然看起来貌似解决了,但是我依然还是有第一点疑问。如果应用有100个需要发布的地方,那么session中的key就有100个,我的问题是,如果有100个用户同时在线,那么session中的存储是不是100*100个?

forecho#67年前 0 个赞

@xjdata #5楼 session 设置个过期时间就可以了

xjdata#77年前 0 个赞

@forecho #6楼 可以麻烦贴下代码吗?新手有很多细节都想搞清楚。谢谢。

forecho#87年前 0 个赞

单独为某个 Session 设置过期时间好像有点麻烦。其实你也可以用 cookies 来实现这个功能,同时还能解决服务器压力。

关于 cookies 的使用,可以查看:

yiqing#97年前 1 个赞

$repeatCheckKey = $this->action->id ;
if($cache->get($repeatCheckKey) !== false ){
  // 提示用户提交过快 
}
if($model->save()){
   // 60 秒内防止重复提交
   $cache->set($repeatCheckKey ,time(),60) ;
}

上面这个只是示意做法 如果真的可行 那么可以利用事件机制 无侵入实现

只需要监听ActiveRecord::EVENT_BEFORE_INSERT 以及 ActiveRecord::EVENT_AFTER_INSERT 就可以无侵入实现

还有一种方法 直接跟ar无关 直接监听控制器的beforeAction 跟afterAction 利用这种思路也可以实现
经过思考 觉得还是监听控制器吧

forecho#107年前 0 个赞

还有一种方案

就是先查询下 提交前查询下 内容是否存在 如果存在提示重复提交

还有就是用MD5 或者哈希下 前一次提交的内容 第二次提交时 对比签名 这样 md5的值可以放在session中 因为资源较小 相比再次查询db能稍好些

yiqing#117年前 0 个赞

关于重复提交:

接 @forecho的回复。

思路就是 哈希POST (注意下 两次提交有没有不一样的参数混进来 比如xxxToken)中的值 提交成功后存一个md5 (位数比较少哈! ) 下次处理提交前对比哈希 如果一样 就是内容重复 。 session key 可以命名为 $lastActionParamsHash , 值:md5(\Yii::$app->request->post()) ; 这样一定程度内可以防止 “手抖”多次点击提交按钮

对于手抖 其实在js端也可以做限制 比如点击按钮后 就灰显(disable)之。

这个方案 跟防止快速提交性质稍微不一样了 但也好像有些关联 :d:

@xjdata 是的,如果有 100 个用户同时在线,那么每个用户维护 100 个, 就是一万个。 session 要设置过期时间,过期后,会被 PHP 的垃圾回收自动处理,详情见:PHP session 现在 PHP session 很少有用默认的文件存储了, 因为会存在文件锁引起的 bug,大多数用内存缓存或数据库缓存。所以关于性能和空间占用完全可以不用考虑,很多时候,开发就是用冗余去换用户体验,在两者中间找平衡。 过早的优化,是 bug 产生的根源。 用 hash 也是一个办法,但是比这个办法更不简单。

xjdata#137年前 0 个赞

@abrahamgreyson #12楼 非常赞成 过早的优化,是 bug 产生的根源。 刚刚接触yii看了很多资料,知道了缓存,于是自己尝试写了一些,可是现在自己联手的项目越来越多的问题,归根结底还是因为自己对这些技术没有经验并且掌握的不够透彻,哎。。。慢慢来。

xjdata#147年前 0 个赞

@yiqing #11楼 个人感觉这个可能是相对较好的办法了。同时也感谢其他的朋友们,恩。c/s过来的,感觉web其实就是一个开放了ui的c/s。 只是细节细分的太多太多了。 比如现在的这个问题,也许现在看起来合适,但以后其他的场景,或者其他的解决方案会有很多很多。 慢慢来吧。

谢谢各位!

c376685128#156年前 1 个赞

这个貌似可以用yii的csrftoken来解决吧,将csrftoken的有效次数设置为一次,进入页面后会生成一个csrftoken,提交一次后失效了,后面提交的数据就不能成功了,虽然csrftoken是防止恶意注入的,但是重复插入本身也算是一种注入了

forecho#166年前 0 个赞

@c376685128 #15楼 不行的,不一样的,csrftoken 不是用来解决这个问题的。

@forecho [#2楼](#comment2) 可以设置延时,200ms 之后,将按钮disabled 属性删除掉.

yiqing#186年前 1 个赞

关于cacheKey的设计 最好加上用户或者浏览器特定的因素在里面 如果不考虑这些因素 所有用户的操作可能就被串行化了(不同的人 浏览器 都在做这个action 动作时 如果key不是用户客户端特定的 他们将被同一个key给锁住)

针对对不同的人 要用不同的缓存键 所以可以用某种前后缀方法 保证键对客户端的唯一性:

  • php 早期有个 ip2long 方法可以把客户端的ip转换为整数 但后来ipv6出现后 这个方法好像有些失效 网上有人提出了ip2bin方法(可以搜索下)

  • 还有一个方法是 cacheKey 里面添加sessionId 做前缀

$repeatCheckKey = {$sessionId}.$this->action->id
yidashi#196年前 0 个赞

正如15楼所说,虽然csrf验证不是为了防止重复提交,但是跟这个原理几乎一样,是能实现防止重复提交的。稍加修改,让csrf提交验证通过后token失效即可

forecho#206年前 0 个赞

@yidashi #19楼 我试过不行的,一般重复提交都是提交按钮重复点了多次,这种情况分两种,一种是网络比较慢的时候,以为点了之后没反应,重复点击。还有一种情况是故意点击的。

而这两种情况都是每次都会重新生成 csrf token,所以 csrf 验证是通过的。

yidashi#216年前 0 个赞

@forecho #20楼 你说的两种情况不管是恶意还是无意都是重复点击,当前页面没有刷新,表单里的csrf token没变啊,而第一个请求一通过就让token失效了(session),后边的不可能再通过。我试的可以啊~

zoofei#225年前 0 个赞

使用csrf token是无效的,我试过,post提交之后验证csrf,验证通过之后刷新csrf key,理论上如果在同一个页面重复刷新之后csrf是无法验证通过的,但是。。。。。如果很快速的提交或者刷新,两次或者多次提交同时经过验证,同时都通过验证,还是有多次重复提交,然并卵,没什么用,主要是验证时间太慢,本次提交正在验证的时候,下次一次提交如已经过来了。本省csrf就不是用来干这个的。。。

添加回复 (需要登录)
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册