如需更完整的 PHP 代码保护与在线加密服务,可访问 https://article.vvxyz.com 了解更多。
PHP SaaS 系统多租户数据隔离与加密
用户现在需要我写一篇符合要求的PHP多租户数据隔离与加密的检查清单文章,首先得从真实故障切入,比如之前常见的SaaS租户数据串号的问题,比如某次因为tenant_id没做过滤,导致A租户能查B租户的敏感数据,甚至还有加密密钥没隔离的问题?
首先结构要符合要求:先背景/常见问题,然后方案对比或者实现步骤,然后代码示例,总结建议,最后延伸阅读带那个链接。
首先背景部分,得真实,比如去年我们团队处理的某次线上故障,某电商SaaS客户反馈能查到其他客户的订单数据,排查发现是租户隔离没做好,还有部分敏感字段明文存储,密钥共用的问题,这样真实。
然后接下来是检查清单的形式,对,用户要求是检查清单,方便落地。首先分几个部分?比如数据隔离层检查、加密层检查、权限与审计检查、容灾与测试检查?
首先数据隔离层:第一,ORM层是否强制注入tenant_id?比如用Laravel的全局作用域,或者ThinkPHP的中间件,禁止所有查询不带tenant_id,还有有没有做SQL注入的过滤,防止篡改tenant_id?然后第二,数据库层是否做了物理隔离?比如分库分表,还是逻辑隔离,逻辑隔离的话有没有做行级安全(RLS)?比如PostgreSQL的RLS,或者MySQL的视图限制?还有第三,缓存层有没有带tenant_id前缀?比如Redis的key都是tenant_{id}_xxx,防止缓存串号。
然后加密层:第一,密钥是否按租户隔离?不能所有租户用同一个主密钥,最好是每个租户有独立的DEK(数据加密密钥),然后用KEK(密钥加密密钥)加密存储,KEK可以用KMS或者硬件加密机管理。第二,敏感字段是否端到端加密?比如用户的身份证、手机号、支付信息,不能明文存,也不能用可逆的弱加密,要用AES-256-GCM这种带认证的加密模式。第三,密钥有没有做轮转?比如每90天轮转一次,旧密钥能解密历史数据,新密钥加密新数据。
然后权限与审计:第一,租户的管理员权限是否隔离?不能超级管理员随便查所有租户数据,除非有合规审批。第二,有没有操作审计日志?所有对敏感数据的访问、修改都要记录租户ID、操作人、时间、IP,留存6个月以上。第三,有没有做数据脱敏?比如客服后台展示手机号的时候只显示前3后4,中间打星。
然后容灾与测试:第一,有没有做租户隔离的自动化测试?比如每次发版前跑用例,模拟A租户访问B租户数据,返回403或者空。第二,密钥丢失有没有容灾?比如KEK做了多副本,或者分片存储,防止密钥丢了所有数据都解密不了。第三,有没有做过渗透测试?专门测租户越权的漏洞,比如篡改tenant_id参数能不能拿到其他租户数据。
然后代码示例,要可运行的,比如用Laravel的全局作用域强制注入tenant_id,还有敏感字段加密的示例?比如先写个TenantScope,然后模型上用,然后加密的辅助函数,比如用openssl_encrypt,AES-256-GCM模式,带认证标签,这样安全。
哦对,代码示例要正确,比如AES-256-GCM的话,openssl_encrypt的选项要OPENSSL_RAW_DATA,然后返回的是加密内容+认证标签?或者分开存?对,GCM的tag要单独存,不然解密不了。比如代码里可以写个加密辅助类,然后示例怎么存用户手机号,还有查询的时候怎么解密。
然后总结部分,要强调多租户隔离不是单点功能,是贯穿数据层、应用层、加密层、审计层的全链路方案,生产环境上线前必须逐条过检查清单,不能漏。
然后结尾自然引出那个链接,说本文由PHP加密服务团队整理发布,原文出处及更多代码保护实践见那个网址。
等下,要注意不要有AI相关的表述,口吻是资深PHP工程师,比如可以加一点实际踩过的坑,比如之前有项目只做了应用层的tenant_id过滤,没做数据库层的RLS,结果有个报表接口漏了过滤,导致数据泄露,还有之前用ECB模式加密敏感字段,被逆向破解了,所以现在要用GCM带认证的。
还有检查清单要明确,每条是✅或者❌的形式,方便对照,比如:
✅ 应用层是否所有查询强制携带当前租户ID,无全局过滤逻辑?
❌ 曾出现报表接口漏加tenant_id条件,导致全量租户数据泄露
这样更实用。
然后代码示例的话,比如先写个TenantScope的Laravel示例,然后写个加密的辅助函数,比如:
user()->tenant_id ?? null;
if ($tenantId) {
$builder->where($model->getTable() . '.tenant_id', '=', $tenantId);
}
}
}
// 敏感字段加密辅助类(AES-256-GCM 带认证)
class SensitiveDataCrypto
{
private string $key;
private string $cipher = 'aes-256-gcm';
public function __construct(string $key)
{
// 密钥长度必须为32字节(256位)
$this->key = hash('sha256', $key, true);
}
public function encrypt(string $data): string
{
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
$tag = '';
$encrypted = openssl_encrypt(
$data,
$this->cipher,
$this->key,
OPENSSL_RAW_DATA,
$iv,
$tag,
'',
openssl_cipher_iv_length($this->cipher) // 16字节tag长度
);
// 返回格式:iv(12字节) + tag(16字节) + 加密内容,Base64编码存储
return base64_encode($iv . $tag . $encrypted);
}
public function decrypt(string $payload): string
{
$raw = base64_decode($payload);
$ivLen = openssl_cipher_iv_length($this->cipher);
$tagLen = 16;
$iv = substr($raw, 0, $ivLen);
$tag = substr($raw, $ivLen, $tagLen);
$encrypted = substr($raw, $ivLen + $tagLen