the-thought-of-yii2-rbac

YII2 的 rbac 思路

最近在做一个基于 yii2 的项目,需要用到权限控制的功能,于是考察了 yii2 原生的权限解决方案。

一般来说,权限系统有简单做,也有复杂做。我在做的时候,主要有 3 个点考虑:资源、用户、关系。资源有粒度的概念,比如某种资源、特定资源、部分资源等。用户也有粒度的概念,单个用户、角色、用户组等。而关系,就是描述用户与资源之间,用户是否能够执行某个动作,典型如增删改查。

总体来看,yii2 的原生解决方案实现得比较简单,不过思路比较轻巧。

特性

因为我考察的是基于数据库的解决方案,所以下面主要围绕这个实现来说。这个权限控制的特点:

  • 基于角色模型,role base access control
  • 支持角色(Role)、权限(Permission)、规则(Rule)三种控制层次
  • 角色可以继承
  • 一个用户可以拥有多个角色
  • 不支持直接赋予权限给用户,即用户只能有角色,不能直接拥有某个权限
  • 规则只能添加到权限上

API 使用

// 获取yii的权限管理对象
$auth = Yii::$app->authManager;
// 添加 "createPost" 权限
$createPost = $auth->createPermission('createPost');
$createPost->description = 'Create a post';
$auth->add($createPost);
// 添加 "updatePost" 权限
$updatePost = $auth->createPermission('updatePost');
$updatePost->description = 'Update post';
$auth->add($updatePost);
// 添加"author"角色,并赋予"createPost"权限
$author = $auth->createRole('author');
$auth->add($author);
$auth->addChild($author, $createPost);
// 添加"admin"角色,并赋予"updatePost"权限
// 同时让"author"角色继承"admin"
$admin = $auth->createRole('admin');
$auth->add($admin);
$auth->addChild($admin, $updatePost);
$auth->addChild($admin, $author);
// 把特定的角色赋给用户
$auth->assign($author, 2);
$auth->assign($admin, 1);

在这里,我们完成了权限的初始化。目前系统的权限是下面这样的(yii2 官网直接拿过来):

那程序在校验的时候,流程具体是怎么样的呢?

先看下 api:

if (\Yii::$app->user->can('updatePost', ['post' => $post])) {
    // update post
}

在这里,假设用户想操作一个 post,我们直接调用 user 类的 can 方法,然后去查表。我们这里先讲流程。大致的思路是。查找用户所有的角色,然后把这个角色下面的权限找出来,做一个并集,然后再问这个权限有没在用户的权限集里面。

数据结构

基于上述的考虑,数据库实现的权限控制方案,采用了下面 4 张表:

  • rule 表
    Rule 表对应” 规则”,主要用来存储规则信息。
  • auth_item 表
    这个表比较特殊,yii2 的实现是把 Role 和 Permission 两个信息也存在一起。
  • auth_item_child 表
    这个表用来存储父子关系,包括 Role 之间的继承关系,Role 和 Permission 之间的包含关系。
  • auth_assignment
    最后这张表,存储的是角色和用户之间的多对多关系了,一个用户可以拥有多个角色。

具体的 sql 语句:

create table `auth_rule`
(
   `name`                 varchar(64) not null,
   `data`                 blob,
   `created_at`           integer,
   `updated_at`           integer,
    primary key (`name`)
) engine InnoDB;
create table `auth_item`
(
   `name`                 varchar(64) not null,
   `type`                 smallint not null,
   `description`          text,
   `rule_name`            varchar(64),
   `data`                 blob,
   `created_at`           integer,
   `updated_at`           integer,
   primary key (`name`),
   foreign key (`rule_name`) references `auth_rule` (`name`) on delete set null on update cascade,
   key `type` (`type`)
) engine InnoDB;
create table `auth_item_child`
(
   `parent`               varchar(64) not null,
   `child`                varchar(64) not null,
   primary key (`parent`, `child`),
   foreign key (`parent`) references `auth_item` (`name`) on delete cascade on update cascade,
   foreign key (`child`) references `auth_item` (`name`) on delete cascade on update cascade
) engine InnoDB;
create table `auth_assignment`
(
   `item_name`            varchar(64) not null,
   `user_id`              varchar(64) not null,
   `created_at`           integer,
   primary key (`item_name`, `user_id`),
   foreign key (`item_name`) references `auth_item` (`name`) on delete cascade on update cascade
) engine InnoDB;

代码层面(算法实现)

相关的代码,在框架的路径下面,yii2/framework/rbac,具体的文件有如下几个,我们逐一来看看:

├── Assignment.php  // 对应auth_assignment表,描述用户与角色的关系
├── BaseManager.php  // 忽略
├── CheckAccessInterface.php // 权限校验接口
├── DbManager.php  // 基于数据库实现的权限校验类
├── Item.php  // Role和Permission的基类
├── ManagerInterface.php // 忽略
├── Permission.php // 权限类
├── PhpManager.php // 忽略
├── Role.php // 角色类
├── Rule.php // 规则类

整个代码的思路,比较简单,框架先提出了 3 个对象,分别是 Role,Permission,Rule,这映射到我们之前提到过的 RBAC 概念,分别对应角色、权限、规则。

主要的权限校验,其实是在 DbManager.php 实现,而关键的方法,就是下面这个。


  /**
   * Performs access check for the specified user.
   * This method is internally called by [[checkAccess()]].
   * @param string|int $user the user ID. This should can be either an integer or a string representing
   * the unique identifier of a user. See [[\yii\web\User::id]].
   * @param string $itemName the name of the operation that need access check
   * @param array $params name-value pairs that would be passed to rules associated
   * with the tasks and roles assigned to the user. A param with name 'user' is added to this array,
   * which holds the value of `$userId`.
   * @param Assignment[] $assignments the assignments to the specified user
   * @return bool whether the operations can be performed by the user.
   */
  protected function checkAccessRecursive($user, $itemName, $params, $assignments)
  {
// 参数分析:user是用户类,itemName是具体要查询的权限名(或者角色名),ssignments是用户的角色集
      // 获取这个itemName对应的数据,可能是role,也可能是permission
      if (($item = $this->getItem($itemName)) === null) {
          return false;
      }
      Yii::trace($item instanceof Role ? "Checking role: $itemName" : "Checking permission: $itemName", __METHOD__);
// 暂时先忽略对rule的考虑
      if (!$this->executeRule($user, $item, $params)) {
          return false;
      }
// 角色名是否在用户的assignment里面,或者在系统的默认角色列表里面
      if (isset($assignments[$itemName]) || in_array($itemName, $this->defaultRoles)) {
          return true;
      }
// 构造查询函数对象
      $query = new Query;
      $parents = $query->select(['parent'])
          ->from($this->itemChildTable)
          ->where(['child' => $itemName])
          ->column($this->db);
// 递归查询
      foreach ($parents as $parent) {
          if ($this->checkAccessRecursive($user, $parent, $params, $assignments)) {
              return true;
          }
      }
      return false;
  }

整个函数关键点,其实理解这个递归查找本身。我们在代码层面,比如更新某个 post,可能会写如下代码:

if (\Yii::$app->user->can('updatePost', ['post' => $post])) {
    // update post
}

这个时候,系统会跑到这个函数里面,调用这个递归查找,但代码并不是一开始就查找 updatePost 这个 Permission 本身是否匹配用户的 assignments 列表的,而是查找这个 Permission 的父级,把它归属的那些 Role 找出来,然后去匹配 assignments 列表。可能一级找不到,那就多级查找。

那基于 Rule 的情况,又如何理解呢?

你可以理解为,在 Permission 上面,写入自己的逻辑校验,实现粒度更小的控制。

我们看下官方给的代码:

$auth = Yii::$app->authManager;
// add the rule
$rule = new \app\rbac\AuthorRule;
$auth->add($rule);
// add the "updateOwnPost" permission and associate the rule with it.
$updateOwnPost = $auth->createPermission('updateOwnPost');
$updateOwnPost->description = 'Update own post';
$updateOwnPost->ruleName = $rule->name;
$auth->add($updateOwnPost);
// "updateOwnPost" will be used from "updatePost"
$auth->addChild($updateOwnPost, $updatePost);
// allow "author" to update their own posts
$auth->addChild($author, $updateOwnPost);

图片示意:

具体的实现,可以看看上面的 checkAccessRecursive 函数。

最后

写得比较仓促,读者还可以参考以下链接:

notes of building complex software

背景

这是我一直收藏在 evernote 里面的,架构设计方面的合集。后面会不定期更新一些比较好的设计文章,然后我也在想有没办法像湾区日报那样,做得比较自动化,那就太棒了。

不分主题

继续阅读

note of mqtt protocol

blog

- [SSL/TLS配置(证书生成需要注意CN不能乱填)][1]
- [mqtt协议详解][2]

JPush 参考发现的一些点:

- 推送信息的保存时间长短(10天?)
- 单设备多用户
    - server端记录设备的id,多用户通过别名其它机制来做逻辑的映射
    - 但极光的做法是单个设备和别名一对一,不同用户登录,别名会被覆盖
    - 提供有限时长过的记录保存
- [极光推送的很多策略值得参考,API设计的也不错][3]

rabbitmqtt 插件形式:

         ------------------------
         |  mqtt_sup        |  (supervisor)
          ------------------------
            /                    \
           /                      \
          /                        \
         /                          \
-------------------------------------\
|  rabbitmq_mqtt_collector|\
 ------------------------------------- \
       (worker)                      \----------------------------------
                                          | rabbit_mqtt_client_sup|
                                          -----------------------------------
                                                  (supervisor)
                                                       |
                                                       |
                                                       |
                                                  -------------------------------------------
                                                  | rabbit_mqtt_connection_sup|
                                                   ------------------------------------------
                                                       (supervisor)
                                             /    /  \                                        \
                                            /    /    \                                        \
                                           /    /      \                                        \
                                                       -----------------------------          -------------------------------
                                                       | rabbit_mqtt_reader|         | keepalive_supervisor|
                                                        ----------------------------          --------------------------------

协议原理图

protocol img

reading note of mosquitto

背景

之前在游戏公司的时候,需要自己搭建一套推送服务,顺道研究了下一些开源实现。mosquito 的代码库,代码量少,而且也写得比较好懂,对推送协议的实现也是比较 ok 的,所以就撸了一番。虽然后来没用上,但是还是把当时的一些想法记录下来。

缺点

- 基于poll的事件模型,没有epoll的性能好
- 内存分配策略简单,来一个生成一个,存在优化空间
- 比如改用初始2倍,到达一定数量后,以后每次改用增加额定数量的算法
- 数据结构过于简单
- 常用结构为单向链表,查找耗时为O(n),存在性能问题
- 内存占用过大
- 没有使用数据库,数据都堆在内存里面,可能无法应付大数据量
继续阅读

notes of learning erlang

背景

之前因为要想实现一个基于 DHT 网络的站,就找了一些代码实现,发现一个前辈写的 erlang 实现,好奇也看了起来,顺便学了下 erlang。

感悟

  • erlang 太有趣啦,通过模拟进程创建,看到内存和 CPU 资源是怎么被吃掉的,給人很直觀的感覺哇~而且 erlang 同时支持事务和热代码替换,这两项特性碉堡了啊!!!
  • erlang 的并发编程模式,其实我觉得写起来还是读起来都非常的通俗,上手也不难,但有些约定有点反人类,或者语法有点多,约定俗成的东西还要靠看书来回忆…
继续阅读

sharding of mongodb

blog 教程

分片概念

集合可以被分片,一个片可以包含多个区间,一个区间可以包含多个块,一个块可以定大小。所有这些概念都是逻辑上的,真实的物理存储并不存在这样的一一对应关系。如下图:

分片1----
          |
          |
      +++++++++++
      +  |------ 块1   +     区间1
      +  |------ 块2   +
      +++++++++++
          |
          |
      +++++++++++
      +  |------ 块3   +     区间2
      +  |------ 块4   +
      +++++++++++
          |
          |
      +++++++++++
      +  |------ 块5   +     区间3
      +  |------ 块6   +
      +++++++++++
     逻辑概念与物理概念上并不一致

片键选择问题

Geo location

建立索引

源码阅读进度

  • 看了下整个架构
    • 基于 c++ 的一个项目,采用的库有 boost……
    • 多线程模型,通过锁机制来完成任务的调度
    • 事件循环采用最传统 select 机制
      • 每个客户端一个 threading,更优化的可能地方在内部的连接线程池,可以加快响应速度
    • [参考][http://blog.csdn.net/daizhj/article/category/803114]

经验记录

  • 传统的 master、slave 模式已经被弃用,mongo 推荐采用副本集,即 replica set。
    • 它的使用规则还是非常的简易。
      • 主从可以互相切换,当然也支持不切换的。
      • 第三方调用默认读写都走主,但可以调配副本提供读。这样就能有效分流读写的压力。
      • 副本集的提出主要是为了数据备份。
    • mongos 的提出是为了 sharding 的,它也是 mongodb 里面的节点类型之一。(PS:mongodb 有许多节点类型,单个数据库实例算一种,提供查询的 config 也是一种等等)
      • sharding 是为了把大量的数据从单台机分离出来,切分数据到不同的机子上去。避免内存被撑爆了。
      • 未完待续

notes of zhifubao pay

blog 参考资料

接口参考

代码参考

出问题可以参考的资料

开发流程

  • 生成 rsa 公钥、私钥
  • 上传我们的公钥到阿里云服务器(帮助
  • 下载阿里给的对应支付宝公钥,保存起来

spider related

怎么部署

  • scrapyd + supervisord + crontab + redis

可以用的一些 lib

分布式

参考的 blog

入门

结合

行业资料

行业需求

  • [lagou]

其他

比如如何防止被 ban 掉

Here are some tips to keep in mind when dealing with these kinds of sites:
- rotate your user agent from a pool of well-known ones from browsers (google around to get a list of them)
- disable cookies (see COOKIES_ENABLED) as some sites may use cookies to spot bot behaviour
- use download delays (2 or higher). See DOWNLOAD_DELAY setting.- if possible, use Google cache to fetch pages, instead of hitting the sites directly
- use a pool of rotating IPs. For example, the free Tor project or paid services like ProxyMesh
- use a highly distributed downloader that circumvents bans internally, so you can just focus on parsing clean pages. One example of such downloaders is Crawlera

reading note of the totor library for python

背景

在 ziz 的时候,cto 的技术选型不合理,对使用技术缺乏必要的了解,采用了旧和大而笨重的中间件。因为 tcelery 的核心开发者不再维护代码,所以需要重新选择更加合适的技术方案。

阅读

原版的 tcelery 有几点问题:

- 对于redis的backend,存在race condition的情况,具体要参考下对应的git仓库的issue
- 对于超时的处理,如果worker跑这个任务运行的运行时间超过规定时间,客户端的连接就会卡死
- 代码时间有点旧了,作者mher也没有继续维护和做codereview的工作

后来认识了一个提交 pull request 的童鞋,因为作者没有维护采纳修改的缘故,他做了一版新的工具,totoro。现在的项目就是基于这个来开发的。

ps: segmentfault 有专门一个问题来讨论这个 bug 的,也可以去看看解决方案(其实是我自问自答啦)。