Skip to content

数据库权限管理

概述

数据库权限管理遵循最小权限原则(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 修正
性能下降索引缺失/数据量大添加索引,优化查询
数据不一致并发冲突/事务残留使用锁机制和事务
内存溢出大数据集/未释放资源增大内存限制,分批处理

故障排除步骤

  1. 检查错误日志和异常信息
  2. 确认配置和环境是否正确
  3. 使用调试工具逐步排查
  4. 参考官方文档查找已知问题

版本兼容性说明

功能最低版本说明
基础功能PHP 8.1本文档基准版本
只读属性PHP 8.1public readonly 修饰符
枚举类型PHP 8.1enum 类型和 match 表达式
FiberPHP 8.1协程/轻量级并发
命名参数PHP 8.0foo(arg_name: value)
联合类型PHP 8.0`int
Null 安全运算符PHP 8.0$obj?->method()
析构器 promotionPHP 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');

参考链接