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

PHP HTTP 基本認證實作:Apache 配置與手動驗證完整教學

     編輯:PHP 綜合知識

基本認證原理

HTTP Basic Access Authentication (RFC 7617) 是最簡單的認證機制:

  1. 客戶端請求受保護資源
  2. 伺服器回應 401 Unauthorized + WWW-Authenticate: Basic realm="..."
  3. 瀏覽器彈出登入對話框,用戶輸入帳密
  4. 客戶端將 Authorization: Basic base64(username:password) 加入後續請求
  5. 伺服器解碼驗證,成功返回資源,失敗再次 401

缺點:Base64 非加密,必須搭配 HTTPS 使用;無登出機制,瀏覽器關閉前持續帶憑證。

Apache 原生配置 (.htaccess / httpd.conf)

<Directory "/var/www/html/protected">
    AuthType Basic
    AuthName "Admin Area"
    AuthUserFile /etc/apache2/.htpasswd
    Require valid-user
</Directory>

建立密碼檔:

htpasswd -c /etc/apache2/.htpasswd admin
# 輸入密碼兩次
# 新增用戶:htpasswd /etc/apache2/.htpasswd user2

PHP 手動實作 (不依賴 Apache 模組)

<?php
// auth.php - 可 include 於任何需保護的腳本頂端
function requireBasicAuth(string $realm = 'Restricted Area', array $users = []): array {
    // 預設用戶 (生產環境應從資料庫/配置讀取,密碼存 bcrypt 雜湊)
    $defaultUsers = [
        'admin' => '$2y$10$...', // password_hash('password', PASSWORD_BCRYPT)
    ];
    $users = $users ?: $defaultUsers;
    
    // 1. 檢查是否已提供認證標頭
    if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
        challenge($realm);
    }
    
    $username = $_SERVER['PHP_AUTH_USER'];
    $password = $_SERVER['PHP_AUTH_PW'];
    
    // 2. 驗證用戶存在
    if (!isset($users[$username])) {
        challenge($realm, '用戶不存在');
    }
    
    // 3. 驗證密碼 (支援 bcrypt 雜湊)
    if (!password_verify($password, $users[$username])) {
        // 防暴力:簡單延遲
        sleep(1);
        challenge($realm, '密碼錯誤');
    }
    
    // 4. 成功:返回用戶資訊
    return ['username' => $username, 'authenticated' => true];
}

function challenge(string $realm, string $message = 'Authentication required'): never {
    header('WWW-Authenticate: Basic realm="' . $realm . '"');
    header('HTTP/1.0 401 Unauthorized');
    // 可自訂錯誤頁面
    echo "<h1>401 Unauthorized</h1><p>$message</p>";
    exit;
}

// 使用範例
$user = requireBasicAuth('My Admin Panel');
echo "歡迎, {$user['username']}!";

進階:資料庫驗證 + 帳號鎖定

<?php
function requireBasicAuthDB(PDO $pdo, string $realm = 'API'): array {
    if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
        challenge($realm);
    }
    
    $username = $_SERVER['PHP_AUTH_USER'];
    $password = $_SERVER['PHP_AUTH_PW'];
    
    // 查詢用戶 (含失敗次數、鎖定狀態)
    $stmt = $pdo->prepare('
        SELECT id, username, password_hash, failed_attempts, locked_until 
        FROM users WHERE username = ? AND deleted_at IS NULL
    ');
    $stmt->execute([$username]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if (!$user) {
        challenge($realm, 'Invalid credentials');
    }
    
    // 檢查鎖定
    if ($user['locked_until'] && new DateTime() < new DateTime($user['locked_until'])) {
        challenge($realm, '帳號暫時鎖定,請稍後再試');
    }
    
    // 驗證密碼
    if (!password_verify($password, $user['password_hash'])) {
        // 增加失敗次數
        $attempts = $user['failed_attempts'] + 1;
        $lockUntil = $attempts >= 5 ? date('Y-m-d H:i:s', strtotime('+15 minutes')) : null;
        $pdo->prepare('UPDATE users SET failed_attempts = ?, locked_until = ? WHERE id = ?')
            ->execute([$attempts, $lockUntil, $user['id']]);
        challenge($realm, 'Invalid credentials');
    }
    
    // 成功:重置失敗計數、更新最後登入
    $pdo->prepare('UPDATE users SET failed_attempts = 0, locked_until = NULL, last_login = NOW() WHERE id = ?')
        ->execute([$user['id']]);
    
    return ['id' => $user['id'], 'username' => $user['username']];
}

PHP 摘要認證 實作

Digest Authentication (RFC 7616) 比 Basic 安全:密碼不以明文/可逆編碼傳輸,而是雜湊挑戰回應。

<?php
function requireDigestAuth(string $realm = 'API', array $users = []): array {
    if (!isset($_SERVER['PHP_AUTH_DIGEST'])) {
        $nonce = bin2hex(random_bytes(16));
        header('WWW-Authenticate: Digest realm="' . $realm . '", qop="auth", nonce="' . $nonce . '", opaque="' . bin2hex(random_bytes(16)) . '"');
        header('HTTP/1.0 401 Unauthorized');
        exit;
    }
    
    // 解析 Authorization 標頭
    $data = parseDigestHeader($_SERVER['PHP_AUTH_DIGEST']);
    if (!$data) { challenge($realm); }
    
    // 檢查用戶
    if (!isset($users[$data['username']])) { challenge($realm); }
    
    // 計算預期回應
    // A1 = MD5(username:realm:password)
    // A2 = MD5(method:uri)
    // response = MD5(A1:nonce:nc:cnonce:qop:A2)
    $A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]);
    $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $data['uri']);
    $validResponse = md5($A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2);
    
    if (!hash_equals($validResponse, $data['response'])) {
        challenge($realm);
    }
    
    // 防重放:檢查 nonce 計數器 nc 遞增 (需儲存上次 nc)
    // 略...
    
    return ['username' => $data['username']];
}

function parseDigestHeader(string $header): ?array {
    $pattern = '/(\w+)=("(?:[^"\\\\]|\\\\.)*"|\w+)/';
    preg_match_all($pattern, $header, $matches, PREG_SET_ORDER);
    $data = [];
    foreach ($matches as $m) {
        $data[$m[1]] = trim($m[2], '"');
    }
    $required = ['username','realm','nonce','uri','response','qop','nc','cnonce'];
    foreach ($required as $field) {
        if (!isset($data[$field])) return null;
    }
    return $data;
}

安全性關鍵點總結

風險對策
Base64 可解碼強制 HTTPS (HSTS、SSL/TLS)
暴力破解失敗延遲、帳號鎖定、IP 限流、CAPTCHA
認證繞過所有入口統一驗證、中介軟體/基底類別強制
憑證洩漏密碼存 bcrypt/argon2、不記錄日誌、定期輪換
CSRFBasic/Digest 非 Cookie 基礎,天然免疫 CSRF
中間人攻擊HSTS、Certificate Pinning (API 客戶端)

適用場景建議

  • 內部管理介面、API 端點、靜態檔案保護:Basic Auth 簡單有效
  • 公開 API、高安全需求:OAuth 2.0 / JWT / API Key
  • 舊系統相容:Digest Auth 比 Basic 略強,但實作複雜
  • 單頁應用 (SPA):不建議 Basic Auth (無登出、無法自訂 UI),改用 Token 認證

總結

PHP 手動實作 HTTP Basic/Digest Auth 給予完全控制權,適合輕量級保護。核心原則:必須 HTTPS、密碼雜湊儲存、防暴力、統一入口。生產環境 API 建議遷移至 OAuth 2.0 / OpenID Connect 標準流程。

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