文件权限
概述
PHP 提供了 chmod()、chown()、chgrp() 等函数来管理文件权限。在 Unix 系统上,权限使用八进制表示法(如 0755、0644)。理解权限模型对于构建安全的文件操作至关重要。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/.htaccessphp
<?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);
}
}
}