Skip to content
// 0x
0x0B // 后端实践

三层防御:设计安全的危险操作

场景

系统里有个”清理测试店铺数据”的功能,用来一键清空测试订单、日志和商品记录。需求很直接:给测试人员用的,点一下就把脏数据清干净,店铺保留以便重新测试。

问题在于:这个按钮不能用在生产店铺上。

最直接的方案是加一个 if 判断:

if ($shop->shop_type !== 'test') {
    die('Only test shops can be cleaned up');
}

但这就够了吗?

如果这段代码的校验逻辑因为某个疏忽被跳过,或者未来的开发者没注意到这个约束,生产数据一样会被清掉。这不是假设——类似的线上事故每天都在发生。

第一层:应用层校验

写在 Controller 里,语义最清晰:

if (!$shop->isTest()) {
    return error('Only test shops can be cleaned up');
}

isTest() 是一个显式的命名,比 shop_type !== 'test' 更容易被代码审查发现。但这一层完全依赖程序员的正确执行——如果某人绕过这个 Controller 直接调 Repository,它就失效了。

这一层防的是”有意绕过”,不防”无意疏忽”。

第二层:持久层 SQL 保护

把校验下沉到 Repository 的 SQL 语句中:

$sql = "DELETE FROM orders WHERE shop_id = ? AND ? = 'test'";
$stmt->bind_param('is', $shopId, $shop->shop_type);

这里的关键是 AND ? = 'test'? 是 shop_type 的值,它不是列名匹配,而是作为一个常量条件拼接进 SQL。如果 shop_type 是 'production',条件就变成了 AND 'production' = 'test'——永假,一行数据都不会删。

这一层不依赖上层的调用是否正确。即使 Controller 层忘记调用 isTest(),SQL 层面的”弹药库”是锁住的。

第三层:数据库约束兜底

最后的物理防线:

`shop_type` enum('production','test') NOT NULL DEFAULT 'production'

ENUM 限制了 shop_type 只能取这两个值,NOT NULL 防止空值绕过,DEFAULT ‘production’ 确保新增记录默认就是生产类型。

如果因为某种原因,上两层全部失效(比如 BUG 导致传入的 shop_id 和 shop_type 不匹配),物理上存在的 NOT NULLENUM 限制依然会阻止非法操作。

三层对比

应用层校验    → 语义检查(是否符合业务规则)
Repository 层 → SQL 级保护(AND ? = 'test')
数据库层      → 约束兜底(ENUM + NOT NULL + DEFAULT)
维度应用层Repository 层数据库层
依赖代码逻辑正确绑定参数正确表结构正确
绕过方式跳过 Controller修改 SQL修改表结构
绕过难度
故障发现编译/测试期运行时 SQL 报错运行时 SQL 报错

每一层的保护强度和绕过难度依次递增。三层合在一起,不是简单的”三重保险”,而是从业务语义到物理约束的递进式防御

这不是特例

同样的模式可以套用到很多场景:

删除用户

Controller:  currentUser.isAdmin()
Repository: DELETE FROM users WHERE id = ? AND role != 'super_admin'
DB:         FOREIGN KEY + ON DELETE RESTRICT

退款操作

Controller:  order.isRefundable()
Repository: UPDATE orders SET refunded = 1 WHERE id = ? AND status = 'paid'
DB:         CHECK (refunded IN (0, 1))

发布上线

Controller:  deploy.hasApproval()
Repository: INSERT INTO releases WHERE branch = ? AND environment IN ('staging')
DB:         INDEX (environment, branch) UNIQUE

总结

一个 if 判断不够,因为代码会变、人会犯错、架构会重构。但数据库的物理约束不会。

三层防御 = 应用语义校验 × SQL 级保护 × 数据库物理约束

每一层独立运作,互不依赖。任何一层失守,后面还有一层兜底。这不是过度设计——这是承认”人总会犯错”之后做出的合理安排。


Share this post on:

Previous Post
设计经验的容器:AI Skill 的迭代过程
Next Post
Shopify OAuth 跨账号授权:一个调试中的意外发现