首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP FTP 文件上傳實戰:從基礎 API 到斷點續傳、被動模式與安全加固

     編輯:PHP 相關知識

FTP 協議核心回顧

  • 雙通道:命令通道 (端口 21) + 數據通道 (主動模式 PORT / 被動模式 PASV)
  • 傳輸模式:ASCII (文本轉換 CRLF) / Binary (二進制原樣) —— 生產環境強制 Binary
  • 認證:明文用戶名/密碼 (FTP) / TLS 加密 (FTPS, 端口 990/21+STARTTLS) / SSH 隧道 (SFTP, 端口 22, 非 FTP 協議)

PHP FTP 基礎 API

函數用途關鍵參數
ftp_connect建立命令通道連接host, port=21, timeout=90
ftp_ssl_connect建立 FTPS 隱式 SSL 連接同 ftp_connect
ftp_login用戶名/密碼認證conn, user, pass
ftp_pasv啟用/禁用被動模式conn, true|false (默認 true, 穿透防火牆/NAT)
ftp_put上傳文件conn, remote, local, mode=FTP_BINARY, startpos=0
ftp_fput從文件指針上傳 (流式)conn, remote, fp, mode, startpos
ftp_get下載文件conn, local, remote, mode, startpos
ftp_nb_put / ftp_nb_fput非阻塞上傳 (需循環 ftp_nb_continue)支持大文件、進度條、超時控制
ftp_size獲取遠程文件大小conn, remote
ftp_mdtm獲取遠程文件修改時間conn, remote
ftp_chdir / ftp_mkdir / ftp_rmdir目錄操作-
ftp_delete / ftp_rename文件刪除/重命名-
ftp_raw / ftp_rawlist發送原始命令 / 詳細列表支持 MLSD (機器可解析)
ftp_close關閉連接-

生產級上傳封裝類

config = array_merge([
            'host' => '',
            'port' => 21,
            'user' => '',
            'pass' => '',
            'timeout' => 30,
            'passive' => true,
            'ssl' => false,           // true = FTPS 隱式
            'verify_peer' => true,    // FTPS 證書驗證
            'cafile' => null,         // 自定義 CA
        ], $config);
    }

    public function connect(): bool {
        $connectFn = $this->config['ssl'] ? 'ftp_ssl_connect' : 'ftp_connect';
        $this->conn = $connectFn($this->config['host'], $this->config['port'], $this->config['timeout']);
        if (!$this->conn) return false;

        if (!ftp_login($this->conn, $this->config['user'], $this->config['pass'])) {
            $this->close();
            return false;
        }
        ftp_pasv($this->conn, $this->config['passive']);
        return true;
    }

    public function upload(string $localPath, string $remotePath, callable $progressCallback = null): bool {
        if (!is_file($localPath)) return false;
        $localSize = filesize($localPath);
        if ($localSize === false) return false;

        // 確保遠程目錄存在
        $this->ensureRemoteDir(dirname($remotePath));

        // 斷點續傳:檢查遠程已有大小
        $remoteSize = @ftp_size($this->conn, $remotePath);
        $resumePos = ($remoteSize !== -1 && $remoteSize < $localSize) ? $remoteSize : 0;

        $fp = fopen($localPath, 'rb');
        if (!$fp) return false;
        if ($resumePos > 0) fseek($fp, $resumePos);

        $mode = FTP_BINARY;
        $result = false;

        if ($progressCallback) {
            // 非阻塞上傳 + 進度回調
            $ret = ftp_nb_fput($this->conn, $remotePath, $fp, $mode, $resumePos);
            while ($ret === FTP_MOREDATA) {
                $ret = ftp_nb_continue($this->conn);
                $uploaded = $resumePos + ftell($fp);
                $progressCallback($uploaded, $localSize);
            }
            $result = ($ret === FTP_FINISHED);
        } else {
            // 阻塞上傳
            $result = ftp_fput($this->conn, $remotePath, $fp, $mode, $resumePos);
        }

        fclose($fp);
        return $result;
    }

    public function ensureRemoteDir(string $remoteDir): void {
        $dirs = explode('/', ltrim($remoteDir, '/'));
        $current = '';
        foreach ($dirs as $dir) {
            if ($dir === '' || $dir === '.') continue;
            $current .= "/$dir";
            if (@ftp_chdir($this->conn, $current) === false) {
                @ftp_mkdir($this->conn, $current);
                @ftp_chdir($this->conn, $current);
            }
        }
    }

    public function close(): void {
        if ($this->conn) {
            @ftp_close($this->conn);
            $this->conn = null;
        }
    }

    public function __destruct() {
        $this->close();
    }
}

// 使用示例
$uploader = new FtpUploader([
    'host' => 'ftp.example.com',
    'user' => 'backup_user',
    'pass' => 'secure_password',
    'passive' => true,
    'ssl' => true, // FTPS
]);

if ($uploader->connect()) {
    $success = $uploader->upload(
        '/var/backups/db_20240101.sql.gz',
        '/backups/db/db_20240101.sql.gz',
        function($uploaded, $total) {
            $pct = round($uploaded / $total * 100, 1);
            echo "\r上傳進度: {$pct}% ({$uploaded}/{$total} bytes)";
        }
    );
    echo $success ? "\n上傳成功\n" : "\n上傳失敗\n";
    $uploader->close();
} else {
    echo "FTP 連接失敗\n";
}

被動模式 (PASV) 與防火牆/NAT 穿透

主動模式 (PORT):
客戶端 → 告知服務器 "我監聽 IP:Port,請連接我"
服務器 → 從 20 端口主動連接客戶端數據端口
問題:客戶端通常在 NAT/防火牆後,服務器無法主動連入

被動模式 (PASV) (默認、推薦):
客戶端 → 發送 PASV
服務器 → 回應 "我在 IP:Port 等你連接" (隨機高端口)
客戶端 → 主動連接服務器數據端口
優勢:客戶端發起所有連接,易穿透 NAT/防火牆
服務器端配置關鍵:
  - 固定 PASV 端口範圍 (如 50000-51000)
  - 防火牆開放 21 + PASV 範圍
  - 宣告外網 IP (masquerade_address) 而非內網 IP

安全加固清單

  1. 強制 FTPS (Explicit TLS):ftp_ssl_connect + ftp_login 後發送 AUTH TLS,或隱式 FTPS (端口 990)。
  2. 證書驗證:stream_context_set_option 設置 verify_peer => true, cafile => '/path/to/ca.pem'。
  3. 禁用匿名/弱密碼:服務器配置強密碼策略、失敗鎖定、IP 白名單。
  4. 目錄隔離:chroot 限制用戶僅見自身目錄,ftp_chroot 或 vsftpd chroot_local_user=YES。
  5. 審計日誌:記錄上傳/下載/刪除操作、IP、時間、文件名、大小。
  6. 病毒掃描:上傳完成後調用 ClamAV 掃描,通過再移入業務目錄。

SFTP 不是 FTP:別混淆

SFTP (SSH File Transfer Protocol) 運行在 SSH 協議之上 (端口 22),單通道加密,支持公鑰認證、斷點續傳、權限管理。PHP 使用 ssh2_sftp (pecl/ssh2) 或 phpseclib/phpseclib (純 PHP) 實現。ftp_* 函數無法操作 SFTP。

結語

FTP 雖老,但在批量文件交換、跨網段傳輸、嵌入式設備、舊系統集成中仍廣泛使用。掌握 被動模式穿透 NAT、FTPS 加密、斷點續傳、非阻塞進度回調、目錄自動創建、安全加固 等核心技能,能構建穩健的文件傳輸管道。新架構優先考慮 SFTP (SSH) 或 HTTPS/REST API + 對象存儲 (S3/MinIO),以統一協議、簡化運維、提升安全性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved