現代 CMS 系統(WordPress、Typecho、Discuz 等)均採用「安裝鎖文件」機制防止重複安裝:首次安裝成功後寫入標記文件,後續請求檢測到該文件存在則直接進入正常運行流程,否則跳轉至安裝向導。本文解析這一機制的實現原理、安全考量與生產級代碼模式。
用戶訪問入口
↓
檢測安裝鎖文件 (data/install.lock)
↓
存在? ──是──→ 正常啟動應用
↓ 否
跳轉至安裝程序 (install/index.php)
↓
安裝完成 → 寫入 install.lock → 重定向首頁
<?php
// index.php 或公共入口文件最頂部
// 1. 定義項目常量(防止直接訪問核心文件)
define('APP_START', true);
define('APP_ROOT', __DIR__ . '/');
define('DATA_DIR', APP_ROOT . 'data/');
// 2. 安裝鎖檢測(必須在輸出任何內容前)
$lockFile = DATA_DIR . 'install.lock';
if (!file_exists($lockFile)) {
// 開發環境提示;生產環境直接跳轉
if (php_sapi_name() === 'cli') {
die("系統未安裝,請訪問 Web 端運行安裝向導\n");
}
// 設置安裝向導 URL(支持子目錄部署)
$installUrl = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/') . '/install/index.php';
// 防止安裝頁面本身被攔截導致無限循環
if (strpos($_SERVER['REQUEST_URI'], '/install/') === false) {
header("Location: {$installUrl}", true, 302);
exit;
}
}
// 3. 正常應用啟動流程...
require_once APP_ROOT . 'bootstrap/app.php';
<?php
// install/step_final.php — 安裝最後一步
// ... 執行數據庫初始化、創建管理員賬號、生成配置文件 ...
// 寫入安裝鎖(原子操作防併發)
$lockFile = __DIR__ . '/../data/install.lock';
$lockContent = json_encode([
'installed_at' => date('c'),
'version' => '2.1.0',
'install_ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
], JSON_UNESCAPED_UNICODE);
// 使用 file_put_contents + LOCK_EX 確保原子寫入
$result = file_put_contents($lockFile, $lockContent, LOCK_EX);
if ($result === false) {
die('安裝鎖寫入失敗,請檢查 data 目錄寫入權限');
}
// 設置嚴格權限:僅所有者可讀寫
chmod($lockFile, 0600);
// 跳轉至後台或首頁
header('Location: ' . dirname($_SERVER['SCRIPT_NAME']) . '/admin/');
exit;
# Linux 部署腳本
chown www-data:www-data data/
chmod 750 data/ # 僅擁有者和組可進入
chmod 640 data/install.lock # 僅擁有者可寫,組可讀
# 或更嚴格:
chmod 600 data/install.lock # 僅擁有者可讀寫
data/ 目錄移至 Web 根目錄外部(如 /var/www/data/),配置 open_basedir 限制data/ 目錄:
# Nginx
location ~ ^/data/ {
deny all;
return 404;
}
<?php
// 安裝入口加入併發鎖
$installLock = __DIR__ . '/../data/.installing.lock';
$fp = fopen($installLock, 'c+');
if (!flock($fp, LOCK_EX | LOCK_NB)) {
die('安裝程序正在運行中,請勿重複提交');
}
// ... 安裝邏輯 ...
flock($fp, LOCK_UN);
fclose($fp);
@unlink($installLock);
<?php
// 多站點共用代碼庫時,按域名/子目錄區分鎖文件
$siteId = md5($_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME']);
$lockFile = DATA_DIR . "install.{$siteId}.lock";
<?php
enum InstallStatus: string
{
case NOT_INSTALLED = 'not_installed';
case INSTALLING = 'installing';
case INSTALLED = 'installed';
case UPGRADING = 'upgrading';
case FAILED = 'failed';
}
class InstallStateManager
{
private string $stateFile;
public function __construct(string $dataDir)
{
$this->stateFile = $dataDir . '/install_state.json';
}
public function getStatus(): InstallStatus
{
if (!file_exists($this->stateFile)) {
return InstallStatus::NOT_INSTALLED;
}
$data = json_decode(file_get_contents($this->stateFile), true);
return InstallStatus::tryFrom($data['status'] ?? '') ?? InstallStatus::FAILED;
}
public function setStatus(InstallStatus $status, array $meta = []): void
{
$data = array_merge(['status' => $status->value, 'updated_at' => date('c')], $meta);
file_put_contents($this->stateFile, json_encode($data, JSON_UNESCAPED_UNICODE), LOCK_EX);
}
}
data/ 目錄無寫入權限 | chown www-data:www-data data/ && chmod 750 data/ |
| 安裝完成仍提示未安裝 | 鎖文件路徑不一致(相對/絕對路徑混用) | 統一使用 __DIR__ 基準的絕對路徑 |
| 子目錄部署跳轉 404 | SCRIPT_NAME 解析錯誤 | 使用 dirname($_SERVER['SCRIPT_NAME']) 動態構建安裝 URL |
| Windows IIS 權限問題 | IIS_IUSRS 用戶無寫入權限 | 為 data/ 授予 IIS_IUSRS 修改權限 |
安裝鎖機制雖簡單,但關係到系統安全與用戶體驗。關鍵點:
掌握這套模式,不僅適用於 CMS 安裝,同樣可用於插件啟用檢測、功能模塊授權驗證、系統維護模式切換等場景。