Yii框架无内置行级数据权限,需在查询层主动注入过滤条件;推荐通过Model静态查询方法、Scopes封装及Behavior全局注入实现一致性控制,避免硬编码和NULL陷阱。

Yii 框架本身不内置「行级数据权限」机制,必须靠开发者在查询层主动注入过滤条件。硬编码 WHERE、拼接 SQL 或在 Controller 里裸取参数再查库,都会绕过模型校验和权限控制逻辑,极易导致越权访问。
Yii2 中用 Query::where() 动态追加行级条件
最常用也最可控的方式,是在构建 ActiveQuery 实例后、执行前调用 where() 或 andWhere() 追加组织/用户维度的过滤条件。关键点是:不能只在 Controller 写一次,而应封装进 Model 的静态查询方法中,确保所有调用路径都经过同一入口。
- 用户只能看自己创建的数据 → 在
UserPost::find()里统一加['created_by' => \Yii::$app->user->id] - 部门员工只能看本部门数据 → 查询时关联
department_id,并从\Yii::$app->user->identity->department_id取值 - 避免在控制器里写
$query = Post::find()->where(...)多次,否则漏一处就破防 - 注意
andWhere()和orWhere()的嵌套优先级,复杂逻辑建议用数组语法:['and', ['status' => 1], ['or', ['created_by' => $uid], ['shared_with' => $uid]]]
用 Scopes 封装可复用的权限条件
把行级过滤逻辑定义为 ActiveRecord 的 scope 方法,比如 public function scopeOwnedByUser($query),后续所有查询都能链式调用 Post::find()->ownedByUser()->all()。这比每次手写 where 更安全,也方便单元测试覆盖。
- scope 必须返回
$query,且内部要用$query->andWhere()而非直接where(),避免覆盖已有条件 - 不要在 scope 里访问
\Yii::$app->user—— 它可能为空(如 console 命令行场景),应由调用方传入用户 ID - 多个 scope 可组合:例如
Post::find()->ownedByUser($uid)->published()->orderBy('created_at DESC') - scope 不会自动触发验证或事件,它只是查询构造器的一部分
在 behaviors() 中统一注入全局行级过滤
如果全站大部分模型都要按租户(tenant_id)隔离,可以用 Behavior 在 beforeFind 钩子中自动补条件。但要注意:这不是银弹,仅适用于强约束、无例外的场景。
- 需继承
yii\base\Behavior,重写events()返回[ActiveRecord::EVENT_BEFORE_FIND => 'onBeforeFind'] -
onBeforeFind()中判断是否已存在tenant_id条件,避免重复添加;可用$event->sender->hasAttribute('tenant_id')兜底 - 该方式对
count()、exists()、asArray()等方法同样生效,但无法拦截原生 SQL 查询(如findBySql) - 调试时容易忽略——错误日志里看不到这个隐式条件,建议在 SQL 日志中开启
yii\db\Command::getRawSql()核对
避免 WHERE 子句中用 != 或 IS NULL 判断权限字段
行级权限常依赖字段非空或等于某值,但 SQL 的 != 和 IS NULL 行为容易误判。例如 WHERE tenant_id != 123 会漏掉所有 tenant_id IS NULL 的记录,而这类记录往往就是越权突破口。
- 权限字段应设为
NOT NULL并有默认值,杜绝 NULL 引发的逻辑漏洞 - 用
=而非!=做正向授权,比如['tenant_id' => $currentTenantId],而不是排除其他租户 - 若必须支持“不限制”选项(如后台管理员),应在应用层做分支:权限检查通过才加条件,而非在 SQL 里写
OR tenant_id IS NULL - MySQL 中
NULL = NULL返回NULL(即 false),任何含 NULL 的比较都需显式用IS NULL或IS NOT NULL
行级过滤真正的难点不在写法,而在「一致性」——每个查询入口、每种数据获取方式(AR、Query、DAO、原生 SQL)、每种执行上下文(Web / Console / API)都得覆盖到。漏掉一个 Post::findBySql() 或一个忘记调用 scope 的地方,整套权限体系就形同虚设。


















