Skip to content

文件权限

概述

PHP 提供了 chmod()chown()chgrp() 等函数来管理文件权限。在 Unix 系统上,权限使用八进制表示法(如 07550644)。理解权限模型对于构建安全的文件操作至关重要。is_readable()is_writable()is_executable() 用于检查当前进程对文件的访问权限。

适用场景

  • 设置上传目录权限
  • 配置日志文件权限
  • 安全检查文件可访问性
  • Web 应用权限管理

基础概念

Unix 权限模型

权限表示:rwx rwx rwx
          │   │   │
          │   │   └── 其他用户 (others)
          │   └────── 所属组 (group)
          └────────── 文件所有者 (owner)

r = 读取 (4)
w = 写入 (2)
x = 执行 (1)

常用权限:
0777 = rwxrwxrwx(所有人均可读写执行)
0755 = rwxr-xr-x(所有者可全部,其他人只读执行)
0750 = rwxr-x---(所有者可全部,组可读执行,其他无权限)
0644 = rw-r--r--(所有者可读写,其他人只读)
0600 = rw-------(仅所有者可读写)

核心权限函数

函数功能平台
chmod()修改权限Unix/Win
chown()修改所有者Unix
chgrp()修改所属组Unix
is_readable()检查可读权限全平台
is_writable()检查可写权限全平台
is_executable()检查可执行权限全平台
fileperms()获取权限值全平台

Windows 限制

chown()chgrp() 在 Windows 上不可用。chmod() 在 Windows 上功能有限(只能设置只读属性)。

语法与代码示例

chmod 修改权限

php
<?php

// 设置文件权限
chmod('/tmp/config.ini', 0644);  // rw-r--r--

// 设置目录权限
chmod('/tmp/uploads', 0755);      // rwxr-xr-x

// 设置可执行权限
chmod('/tmp/script.sh', 0755);    // rwxr-xr-x

// 设置仅所有者可访问
chmod('/tmp/secret.key', 0600);   // rw-------

// 使用字符串格式(PHP 不支持,需使用八进制)
// chmod('/tmp/file.txt', '755'); // 错误!必须是八进制数字

// 批量设置权限
$files = glob('/tmp/cache/*.tmp');
foreach ($files as $file) {
    chmod($file, 0644);
}

// 递归设置目录权限
function chmodRecursive(string $dir, int $dirPerm, int $filePerm): void
{
    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
        RecursiveIteratorIterator::SELF_FIRST
    );

    foreach ($iterator as $item) {
        $item->isDir() ? chmod($item->getPathname(), $dirPerm) : chmod($item->getPathname(), $filePerm);
    }
}

chmodRecursive('/tmp/project', 0755, 0644);

chown 和 chgrp

php
<?php

// 修改文件所有者(需要 root 权限)
$success = chown('/tmp/app.log', 'www-data');

// 使用 UID
$success = chown('/tmp/app.log', 33); // www-data 的 UID

// 修改所属组
$success = chgrp('/tmp/app.log', 'www-data');

// 使用 GID
$success = chgrp('/tmp/app.log', 33);

// 检查操作结果
if (!$success) {
    throw new RuntimeException('权限修改失败(可能需要 root 权限)');
}

权限检查函数

php
<?php

$filePath = '/tmp/config.json';

// 检查文件可读性
if (is_readable($filePath)) {
    echo "文件可读\n";
    $content = file_get_contents($filePath);
}

// 检查文件可写性
if (is_writable($filePath)) {
    echo "文件可写\n";
    file_put_contents($filePath, json_encode(['updated' => true]));
}

// 检查文件可执行性
if (is_executable('/tmp/deploy.sh')) {
    echo "脚本可执行\n";
    exec('/tmp/deploy.sh');
}

// 检查目录
if (is_writable('/tmp/cache') && is_executable('/tmp/cache')) {
    echo "缓存目录可读写和遍历\n";
}

// 综合检查
function ensureWritable(string $path): void
{
    if (!file_exists($path)) {
        throw new RuntimeException("路径不存在: {$path}");
    }
    if (!is_writable($path)) {
        throw new RuntimeException("路径不可写: {$path}");
    }
}

权限值转换

php
<?php

// 获取权限值
$perms = fileperms('/tmp/example.txt');

// 转为八进制字符串
echo sprintf('%o', $perms) . PHP_EOL;     // 如:100644
echo substr(sprintf('%o', $perms), -4) . PHP_EOL; // 0644

// 转为 rwx 字符串格式
function permToString(int $perms): string
{
    $mode = $perms & 0x1FF; // 取后 9 位
    $str = '';

    for ($i = 2; $i >= 0; $i--) {
        $block = ($mode >> ($i * 3)) & 0x7;
        $str .= ($block & 0x4) ? 'r' : '-';
        $str .= ($block & 0x2) ? 'w' : '-';
        $str .= ($block & 0x1) ? 'x' : '-';
    }

    return $str;
}

echo permToString($perms); // rw-r--r--

实战示例

安全的文件权限管理器

php
<?php

declare(strict_types=1);

class PermissionManager
{
    /**
     * 确保目录存在并设置正确权限
     */
    public static function ensureDirectory(
        string $path,
        int $permissions = 0755,
        bool $recursive = true
    ): void {
        if (is_dir($path)) {
            $currentPerms = fileperms($path) & 0x1FF;
            if ($currentPerms !== $permissions) {
                chmod($path, $permissions);
            }
            return;
        }

        if (!mkdir($path, $permissions, $recursive)) {
            throw new RuntimeException("目录创建失败: {$path}");
        }
    }

    /**
     * 安全地设置文件权限
     */
    public static function setSecureFilePermissions(string $path): void
    {
        // 配置文件:仅所有者可读写
        $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));

        $permissionMap = [
            'env'  => 0600,
            'key'  => 0600,
            'pem'  => 0600,
            'ini'  => 0640,
            'json' => 0644,
            'php'  => 0644,
            'log'  => 0640,
            'sh'   => 0750,
        ];

        $perm = $permissionMap[$ext] ?? 0644;
        chmod($path, $perm);
    }

    /**
     * 检查上传目录安全性
     */
    public static function checkUploadDir(string $dir): array
    {
        $issues = [];

        if (!is_dir($dir)) {
            $issues[] = "上传目录不存在: {$dir}";
            return $issues;
        }

        // 检查是否可执行(必须,否则无法进入目录)
        if (!is_executable($dir)) {
            $issues[] = "上传目录不可遍历";
        }

        // 检查是否可写
        if (!is_writable($dir)) {
            $issues[] = "上传目录不可写";
        }

        // 检查权限是否过大
        $perms = fileperms($dir) & 0x1FF;
        if ($perms > 0755) {
            $issues[] = "上传目录权限过大: " . sprintf('%o', $perms) . "(建议 0755 或更低)";
        }

        // 检查是否启用了 PHP 执行
        $htaccess = $dir . DIRECTORY_SEPARATOR . '.htaccess';
        if (!file_exists($htaccess)) {
            $issues[] = "缺少 .htaccess 禁止 PHP 执行";
        }

        return $issues;
    }
}

// 使用示例
PermissionManager::ensureDirectory('/var/www/html/uploads', 0755);
PermissionManager::setSecureFilePermissions('/var/www/html/.env');

$issues = PermissionManager::checkUploadDir('/var/www/html/uploads');
if (!empty($issues)) {
    echo "安全问题:\n" . implode("\n", $issues) . "\n";
}

安装目录权限设置

bash
# 标准 Web 应用目录权限设置
```bash
# 项目目录权限设置脚本

# 设置目录权限
find /var/www/html -type d -exec chmod 0755 {} \;

# 设置文件权限
find /var/www/html -type f -exec chmod 0644 {} \;

# 敏感文件更严格
chmod 0600 /var/www/html/.env
chmod 0600 /var/www/html/config/secret.key

# 确保所有者正确
chown -R www-data:www-data /var/www/html

# 上传目录
chmod 0755 /var/www/html/uploads
# 禁止在上传目录执行 PHP
echo 'php_flag engine off' > /var/www/html/uploads/.htaccess
php
<?php

// PHP 版本的权限设置
class DeploymentPermissions
{
    public static function setPermissions(string $root): void
    {
        $dirs = [
            'var/cache'       => 0775,
            'var/logs'        => 0775,
            'var/sessions'    => 0770,
            'uploads'         => 0755,
            'uploads/private' => 0700,
        ];

        foreach ($dirs as $dir => $perm) {
            $fullPath = $root . DIRECTORY_SEPARATOR . $dir;
            if (is_dir($fullPath)) {
                chmod($fullPath, $perm);
            }
        }

        $sensitive = ['.env', 'config/secret.key'];
        foreach ($sensitive as $file) {
            $fullPath = $root . DIRECTORY_SEPARATOR . $file;
            if (file_exists($fullPath)) {
                chmod($fullPath, 0600);
            }
        }
    }
}

注意事项

umask 影响

php
<?php

// umask 会影响 mkdir 和 chmod 的最终权限
// 实际权限 = 指定权限 & ~umask

// 查看当前 umask
$oldUmask = umask(); // 通常为 0022
echo sprintf('%04o', $oldUmask) . PHP_EOL; // 0022

// 临时修改 umask
$oldUmask = umask(0);
mkdir('/tmp/testdir', 0777); // 实际权限 0777
umask($oldUmask); // 恢复

// 不修改 umask 的情况下创建目录
mkdir('/tmp/testdir', 0777);
chmod('/tmp/testdir', 0777); // 二次 chmod 确保权限正确

Web 服务器用户

php
<?php

// 获取 Web 服务器运行用户
$webUser = posix_getpwuid(posix_geteuid())['name'];
echo "当前用户: {$webUser}\n"; // www-data 或 nginx

// 确保文件属于 Web 服务器用户
function ensureOwnership(string $path, string $owner): void
{
    $currentOwner = posix_getpwuid(fileowner($path))['name'];
    if ($currentOwner !== $owner) {
        chown($path, $owner);
    }
}

权限过大风险

设置 777 权限意味着任何用户都可以修改文件,这是严重的安全隐患。Web 应用中应避免使用 777 权限。

最佳实践

1. 最小权限原则

php
<?php

// 文件权限
// 配置文件: 0600 或 0640(所有者读写,组只读)
// 日志文件: 0640(所有者读写,组只读)
// 上传文件: 0644(所有者读写,其他人只读)
// 可执行脚本: 0750(所有者全部,组读执行)

// 目录权限
// Web 根目录: 0755(所有者全部,其他人读执行)
// 上传目录: 0755 或 0750(所有者全部,其他人读执行或无)
// 敏感目录: 0700(仅所有者)

2. 使用 chmod 配合 mkdir

php
<?php

// 创建目录后立即设置权限(避免 umask 影响)
$dir = '/tmp/cache';
if (!is_dir($dir)) {
    mkdir($dir, 0755, true);
    chmod($dir, 0755); // 确保 umask 不影响
}

3. 安全检查模板

php
<?php

function checkPathSecurity(string $path): void
{
    if (!file_exists($path)) {
        throw new RuntimeException("路径不存在: {$path}");
    }

    if (is_writable($path) && is_executable($path)) {
        // 可写且可执行的目录允许上传和遍历
        $perms = fileperms($path) & 0x1FF;
        if ($perms > 0755) {
            trigger_error("路径权限过大: {$path} ({$perms})", E_USER_WARNING);
        }
    }
}

参考链接