真正的只读安全必须通过数据库用户权限实现,即创建仅具SELECT权限的账号并用于PHP连接;PHP层的只读设置(如READ ONLY、SQL关键词拦截、ORM配置)仅为辅助手段,无法替代DB权限控制。

PHP 本身不直接“创建只读表”,数据库的只读属性由底层 DBMS(如 MySQL、PostgreSQL)控制;PHP 只能通过连接权限、SQL 语句或 ORM 配置间接实现只读约束。强行在 PHP 层模拟只读(比如拦截 INSERT/UPDATE)不可靠,容易被绕过。
MySQL 用户级只读权限设置(最有效)
真正安全的只读,必须从数据库用户权限入手。用具有 SELECT 权限但无 INSERT、UPDATE、DELETE、DROP、ALTER 权限的账号连接数据库。
- 创建只读用户:
CREATE USER 'app_ro'@'%' IDENTIFIED BY 'strong_password'; GRANT SELECT ON `mydb`.* TO 'app_ro'@'%'; FLUSH PRIVILEGES;
- PHP 中使用该账号连接:
$pdo = new PDO('mysql:host=localhost;dbname=mydb', 'app_ro', 'strong_password'); - 一旦执行写操作,MySQL 直接报错:
SQLSTATE[42000]: Syntax error or access rule violation: 1142 INSERT command denied to user... - 注意:不要给
app_ro赋予USAGE或全局权限;SELECT必须显式指定库/表范围,避免GRANT SELECT ON *.*
PHP 连接层强制只读(辅助手段)
可在 PDO 构造后立即执行 SET SESSION TRANSACTION READ ONLY(MySQL 5.6+),或设连接参数 PDO::ATTR_EMULATE_PREPARES => false 防止预处理语句绕过权限检查。
-
READ ONLY会拒绝所有写操作(包括CREATE TEMPORARY TABLE),但前提是用户已有READ_ONLY全局变量修改权限——通常只给管理员,所以更推荐用用户权限控制 - 若必须用 PHP 控制,可封装一个只读 PDO 类,在
exec()/prepare()前校验 SQL 是否含INSERT、UPDATE等关键词——但这只是防御性检查,不能替代 DB 权限 - 避免使用
mysqli_real_escape_string()后拼接 SQL,这种场景下关键词检测极易被绕过
Laravel/Eloquent 中启用只读连接
框架可通过配置分离读写连接,或强制某模型只读。这不是数据库级防护,但能降低误操作风险。
立即学习“PHP免费学习笔记(深入)”;
- 在
config/database.php中配置读写分离:'mysql' => [ 'read' => ['host' => ['192.168.1.10']], 'write' => ['host' => ['192.168.1.11']], // ...其他配置 ] - 在 Eloquent 模型中禁用写操作:
class Report extends Model { public $timestamps = false; protected $fillable = []; public static function boot() { parent::boot(); static::creating(function () { return false; }); static::updating(function () { return false; }); static::deleting(function () { return false; }); } } - 注意:
boot()钩子可被绕过(如直接调用DB::table()->update()),仅适用于业务逻辑层防护
真正的只读安全,核心在于数据库用户的最小权限原则。PHP 层的任何“只读开关”都只是补充,不是防线。最容易被忽略的是:开发环境常共用 root 账号,导致权限策略在测试阶段完全失效;上线前务必验证生产数据库账号是否真的只有 SELECT 权限,并用实际写操作触发一次报错来确认。











