数据库权限管理
概述
数据库权限管理遵循最小权限原则(Principle of Least Privilege)— 每个用户只拥有完成其工作所需的最小权限。这是数据库安全防护的重要环节,即使应用被 SQL 注入,也能限制攻击者能造成的破坏。
权限管理核心
- 不同应用功能使用不同的数据库用户
- 只读操作使用只读用户
- 写入操作使用受限的写入用户
- 迁移操作使用 DDL 用户
- 永远不要使用 root/superuser 账户运行应用
基础概念
MySQL 权限层级
| 层级 | 说明 | 示例 |
|---|---|---|
| 全局权限 | 适用于所有数据库 | GRANT SELECT ON *.* |
| 数据库权限 | 适用于特定数据库 | GRANT SELECT ON app_db.* |
| 表权限 | 适用于特定表 | GRANT SELECT ON app_db.users |
| 列权限 | 适用于特定列 | GRANT SELECT (name) ON app_db.users |
| 存储过程权限 | 适用于存储过程 | GRANT EXECUTE ON PROCEDURE app_db.get_user |
常用权限列表
| 权限 | 说明 | 应用场景 |
|---|---|---|
| SELECT | 读取数据 | 读操作 |
| INSERT | 插入数据 | 写入 |
| UPDATE | 更新数据 | 修改 |
| DELETE | 删除数据 | 删除 |
| CREATE | 创建表/数据库 | 迁移 |
| ALTER | 修改表结构 | 迁移 |
| DROP | 删除表/数据库 | 迁移 |
| INDEX | 创建/删除索引 | 迁移 |
| EXECUTE | 执行存储过程 | 存储过程调用 |
| ALL PRIVILEGES | 所有权限 | 仅限管理员 |
语法与代码
创建专用数据库用户
sql
-- 1. 创建只读用户(报表查询、API 读取)
CREATE USER 'app_readonly'@'%' IDENTIFIED BY 'StrongPassword123!';
GRANT SELECT ON app_db.* TO 'app_readonly'@'%';
FLUSH PRIVILEGES;
-- 2. 创建应用写入用户(日常业务操作)
CREATE USER 'app_readwrite'@'%' IDENTIFIED BY 'StrongPassword456!';
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_readwrite'@'%';
FLUSH PRIVILEGES;
-- 3. 创建迁移用户(数据库结构变更)
CREATE USER 'app_migrate'@'localhost' IDENTIFIED BY 'StrongPassword789!';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, INDEX
ON app_db.* TO 'app_migrate'@'localhost';
FLUSH PRIVILEGES;
-- 4. 创建管理员用户(日常运维)
CREATE USER 'app_admin'@'localhost' IDENTIFIED BY 'StrongPassword000!';
GRANT ALL PRIVILEGES ON app_db.* TO 'app_admin'@'localhost';
GRANT RELOAD, PROCESS ON *.* TO 'app_admin'@'localhost';
FLUSH PRIVILEGES;权限管理操作
sql
-- 查看用户权限
SHOW GRANTS FOR 'app_readwrite'@'%';
-- 输出: GRANT SELECT, INSERT, UPDATE, DELETE ON `app_db`.* TO `app_readwrite`@`%`
-- 查看所有用户
SELECT User, Host FROM mysql.user;
-- 回收权限
REVOKE DELETE ON app_db.* FROM 'app_readonly'@'%';
-- 修改密码
ALTER USER 'app_readwrite'@'%' IDENTIFIED BY 'NewStrongPassword!';
-- 删除用户
DROP USER 'old_user'@'%';
-- 限制连接来源
-- '%' — 任意主机
-- 'localhost' — 仅本地
-- '192.168.1.%' — 特定网段
-- '10.0.0.5' — 特定 IP
CREATE USER 'app_readonly'@'192.168.1.%' IDENTIFIED BY 'Password!';
GRANT SELECT ON app_db.* TO 'app_readonly'@'192.168.1.%';细粒度权限控制
sql
-- 表级权限 — 不同表不同权限
GRANT SELECT ON app_db.products TO 'app_readonly'@'%';
GRANT SELECT ON app_db.orders TO 'app_readonly'@'%';
-- 不授权 users 表的访问(如密码字段)
-- 列级权限 — 隐藏敏感列
GRANT SELECT (id, name, email, created_at) ON app_db.users TO 'app_readonly'@'%';
-- app_readonly 无法查询 password_hash、phone 等敏感列
-- 行级权限 — 通过视图实现
CREATE VIEW app_db.public_users AS
SELECT id, name, email, avatar FROM app_db.users WHERE status = 'active';
GRANT SELECT ON app_db.public_users TO 'app_readonly'@'%';实战示例
PHP 连接配置分离
php
<?php
declare(strict_types=1);
class DatabaseConfig
{
/**
* 获取不同场景的 PDO 连接
*/
public static function getConnection(string $purpose = 'readwrite'): PDO
{
$configs = [
'readonly' => [
'host' => getenv('DB_READONLY_HOST') ?: 'localhost',
'port' => 3306,
'database' => 'app_db',
'username' => 'app_readonly',
'password' => getenv('DB_READONLY_PASSWORD'),
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
'readwrite' => [
'host' => getenv('DB_HOST') ?: 'localhost',
'port' => 3306,
'database' => 'app_db',
'username' => 'app_readwrite',
'password' => getenv('DB_PASSWORD'),
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
'migrate' => [
'host' => getenv('DB_HOST') ?: 'localhost',
'port' => 3306,
'database' => 'app_db',
'username' => 'app_migrate',
'password' => getenv('DB_MIGRATE_PASSWORD'),
'options' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
];
$config = $configs[$purpose] ?? $configs['readwrite'];
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
$config['host'],
$config['port'],
$config['database']
);
return new PDO($dsn, $config['username'], $config['password'], $config['options']);
}
/**
* 读写分离
*/
public static function readConnection(): PDO
{
// 可以配置为读从库
return self::getConnection('readonly');
}
public static function writeConnection(): PDO
{
return self::getConnection('readwrite');
}
}
// 使用示例
// 读操作
$users = DatabaseConfig::readConnection()
->prepare('SELECT id, name FROM users WHERE status = ?')
->execute(['active'])
?->fetchAll() ?? [];
// 写操作
DatabaseConfig::writeConnection()
->prepare('INSERT INTO audit_log (action, user_id) VALUES (?, ?)')
->execute(['login', $userId]);数据库初始化脚本
bash
#!/bin/bash
# database_init.sh — 数据库用户初始化脚本
DB_HOST="${DB_HOST:-localhost}"
DB_ROOT_USER="${DB_ROOT_USER:-root}"
DB_ROOT_PASS="${DB_ROOT_PASS:-}"
MYSQL="mysql -h ${DB_HOST} -u ${DB_ROOT_USER}"
if [ -n "$DB_ROOT_PASS" ]; then
MYSQL="${MYSQL} -p${DB_ROOT_PASS}"
fi
echo "创建数据库..."
$MYSQL -e "CREATE DATABASE IF NOT EXISTS app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
echo "创建只读用户..."
$MYSQL -e "CREATE USER IF NOT EXISTS 'app_readonly'@'%' IDENTIFIED BY '${READONLY_PASS}';"
$MYSQL -e "GRANT SELECT ON app_db.* TO 'app_readonly'@'%';"
echo "创建读写用户..."
$MYSQL -e "CREATE USER IF NOT EXISTS 'app_readwrite'@'%' IDENTIFIED BY '${READWRITE_PASS}';"
$MYSQL -e "GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_readwrite'@'%';"
echo "创建迁移用户..."
$MYSQL -e "CREATE USER IF NOT EXISTS 'app_migrate'@'localhost' IDENTIFIED BY '${MIGRATE_PASS}';"
$MYSQL -e "GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, INDEX ON app_db.* TO 'app_migrate'@'localhost';"
echo "刷新权限..."
$MYSQL -e "FLUSH PRIVILEGES;"
echo "数据库初始化完成"注意事项
密码安全
php
<?php
// 1. 密码存储在环境变量中,不写入代码
// .env 文件
// DB_HOST=localhost
// DB_PASSWORD=your_secure_password
// 2. 使用 .env 文件(不提交到版本控制)
// .gitignore 中添加:
// .env
// .env.local
// .env.production
// 3. 生产环境使用密钥管理服务
// - AWS Secrets Manager
// - HashiCorp Vault
// - Docker Secrets
// 4. 密码强度要求
// - 至少 16 个字符
// - 包含大小写字母、数字、特殊字符
// - 不使用常见密码模式永远不要在代码中硬编码密码
数据库密码、API 密钥等敏感信息应存储在环境变量或密钥管理服务中,绝对不能出现在代码仓库中。
连接安全
php
<?php
// SSL/TLS 加密连接
$dsn = 'mysql:host=db.example.com;dbname=app_db;charset=utf8mb4';
$options = [
PDO::MYSQL_ATTR_SSL_CA => '/path/to/ca.pem',
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
PDO::MYSQL_ATTR_SSL_CIPHER => 'AES256-SHA',
];
$pdo = new PDO($dsn, 'app_readwrite', $password, $options);
// Docker 内部网络连接
// 数据库只在 Docker 内部网络暴露,不映射端口到宿主机
// docker-compose.yml:
// services:
// db:
// ports: [] # 不暴露端口
// app:
// # 通过内部网络连接 db:3306最佳实践
1. 权限审计
sql
-- 定期审计用户权限
SELECT User, Host FROM mysql.user;
-- 查看所有授权
SELECT * FROM mysql.user\G;
-- 查看当前用户的权限
SELECT CURRENT_USER();
SHOW GRANTS FOR CURRENT_USER();
-- 检查是否有用户拥有过多权限
SELECT User, Host,
Insert_priv, Update_priv, Delete_priv,
Create_priv, Alter_priv, Drop_priv,
Grant_priv, Super_priv
FROM mysql.user
WHERE User NOT IN ('mysql.sys', 'mysql.session', 'mysql.infoschema', 'root')
AND (Super_priv = 'Y' OR Grant_priv = 'Y' OR Drop_priv = 'Y' ON all databases);2. 权限管理清单
bash
# 定期执行的安全检查清单:
# 1. 确认没有应用使用 root 账户
# 2. 确认只读用户没有写权限
# 3. 确认迁移用户不能在生产环境直接使用
# 4. 确认数据库不暴露在公网(3306端口不对外开放)
# 5. 确认使用了 SSL/TLS 加密连接
# 6. 确认密码定期轮换(建议90天)
# 7. 确认环境变量中不包含默认密码
# 8. 确认 .env 文件不被提交到版本控制3. 数据库安全配置
sql
-- my.cnf 安全配置
-- [mysqld]
-- skip-name-resolve -- 跳过域名解析
-- bind-address = 127.0.0.1 -- 只监听本地
-- local-infile = OFF -- 禁止 LOAD DATA LOCAL
-- max_connections = 200 -- 限制连接数
-- max_allowed_packet = 16M -- 限制包大小
-- log-warnings = 2 -- 记录警告
-- slow_query_log = ON -- 开启慢查询日志
-- long_query_time = 2 -- 慢查询阈值2秒进阶用法
调试与测试技巧
php
<?php
declare(strict_types=1);
// 单元测试辅助函数
function createTestResource(): mixed
{
return match (true) {
default => new stdClass(),
};
}
// 调试输出函数
function debugOutput(mixed , string = ''): void
{
= ? ": " : '';
.= print_r(, true);
fwrite(STDERR, . "\n");
}
// 性能基准测试
function benchmark(callable , int = 1000): float
{
= hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$fn();
}
return (hrtime(true) - $start) / 1e9;
}日志记录实践
php
<?php
declare(strict_types=1);
/**
* 简易日志记录器
*/
class SimpleLogger
{
private string $logFile;
private string $level = 'INFO';
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
public function info(string $message, array $context = []): void
{
$this->log('INFO', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('WARNING', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('ERROR', $message, $context);
}
private function log(string $level, string $message, array $context): void
{
$timestamp = date('Y-m-d H:i:s');
$contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
$line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
}配置与环境检测
php
<?php
declare(strict_types=1);
// 环境检测工具
class EnvironmentChecker
{
public static function checkRequirements(array $requirements): array
{
$results = [];
foreach ($requirements as $name => $check) {
$results[$name] = is_callable($check) ? $check() : false;
}
return $results;
}
public static function getSystemInfo(): array
{
return [
'php_version' => PHP_VERSION,
'os' => PHP_OS,
'sapi' => PHP_SAPI,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'loaded_extensions' => get_loaded_extensions(),
];
}
}常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 连接超时 | 网络问题/配置错误 | 检查配置,增加超时时间 |
| 权限不足 | 文件/目录权限 | 使用 chmod/chown 修正 |
| 性能下降 | 索引缺失/数据量大 | 添加索引,优化查询 |
| 数据不一致 | 并发冲突/事务残留 | 使用锁机制和事务 |
| 内存溢出 | 大数据集/未释放资源 | 增大内存限制,分批处理 |
故障排除步骤
- 检查错误日志和异常信息
- 确认配置和环境是否正确
- 使用调试工具逐步排查
- 参考官方文档查找已知问题
版本兼容性说明
| 功能 | 最低版本 | 说明 |
|---|---|---|
| 基础功能 | PHP 8.1 | 本文档基准版本 |
| 只读属性 | PHP 8.1 | public readonly 修饰符 |
| 枚举类型 | PHP 8.1 | enum 类型和 match 表达式 |
| Fiber | PHP 8.1 | 协程/轻量级并发 |
| 命名参数 | PHP 8.0 | foo(arg_name: value) |
| 联合类型 | PHP 8.0 | `int |
| Null 安全运算符 | PHP 8.0 | $obj?->method() |
| 析构器 promotion | PHP 8.0 | __construct(public $x) |
php
<?php
declare(strict_types=1);
// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
if (version_compare(PHP_VERSION, $minVersion, '<')) {
throw new RuntimeException(
sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
);
}
}
ensureVersion('8.1.0');