웹 서버에 접근할 때 사용자명과 비밀번호를 입력하는 팝업 창을 본 적 있을까. 그것이 HTTP Basic Authentication이다. 대부분의 개발자들은 이 방식이 웹 서버(Nginx, Apache) 설정에서만 가능하다고 생각해서, 애플리케이션 레벨에서 구현할 생각을 안 한다. 하지만 PHP 코드 내에서 직접 Basic Auth를 검증하고 제어할 수 있다면, 특정 API 엔드포인트나 관리자 페이지를 더 유연하게 보호할 수 있다. 이번엔 PHP에서 htpasswd 형식의 암호화된 비밀번호를 읽고 검증해서 실제 인증 시스템을 구현하는 방법을 완벽하게 정리해보겠다.

 

HTTP Basic Auth와 htpasswd 파일의 기초

HTTP Basic Authentication은 클라이언트가 Authorization 헤더에 Base64 인코딩된 "사용자명:비밀번호" 값을 담아 서버로 보내는 방식이다. 서버는 이 값을 디코딩해서 사용자 정보와 대조하면 된다. htpasswd 파일은 Apache 웹 서버에서 사용하던 형식인데, 사용자명과 함께 bcrypt나 MD5로 해시된 비밀번호를 저장한다. PHP에서도 이 파일을 읽고 비밀번호를 검증할 수 있다.

htpasswd 파일의 형식은 간단하다. 한 줄에 한 명의 사용자 정보가 기록되며, 사용자명과 해시된 비밀번호를 콜론(:)으로 구분한다. 예를 들어 "admin:$2y$10$..." 같은 형태다. htpasswd 명령어로 파일을 만들 수도 있고, PHP 코드로 직접 생성할 수도 있다.

 

1단계. htpasswd 파일 생성 및 준비

먼저 htpasswd 파일을 만들어야 한다. 서버의 명령어로 생성하거나, PHP 코드로 동적으로 생성할 수 있다.

 

htpasswd 명령어로 파일 생성(터미널)
htpasswd -c /var/www/.htpasswd admin

위 명령어는 "/var/www/.htpasswd" 파일을 만들고 "admin" 사용자를 추가한다. 비밀번호 입력 프롬프트가 뜬다. bcrypt 해시(기본값)로 저장된다.

 

PHP 코드로 htpasswd 파일 생성
<?php
// htpasswd 파일 경로
$htpasswd_file = '/var/www/.htpasswd';

// 사용자 정보 배열
$users = [
    'admin' => 'mypassword123',
    'editor' => 'editor456',
];

// htpasswd 파일 생성
$content = '';
foreach ($users as $username => $password) {
    // password_hash()로 bcrypt 해시 생성
    $hashed = password_hash($password, PASSWORD_BCRYPT);
    $content .= $username . ':' . $hashed . PHP_EOL;
}

file_put_contents($htpasswd_file, $content);
chmod($htpasswd_file, 0600); // 파일 권한 설정(소유자만 읽기)
echo "htpasswd 파일 생성 완료";
?>

이 코드를 실행하면 "/var/www/.htpasswd" 파일이 생성되고, 각 사용자의 비밀번호가 bcrypt로 해시된다. 파일 권한을 0600으로 설정해서 소유자만 읽을 수 있게 한다.

 

2단계. HTTP Basic Auth 검증 함수 작성

PHP 코드에서 Basic Auth를 검증하려면 Authorization 헤더를 파싱하고, htpasswd 파일에서 사용자 정보를 조회해서 비밀번호를 확인해야 한다.

<?php
function validate_http_basic_auth($htpasswd_file) {
    // Authorization 헤더 확인
    if (!isset($_SERVER['HTTP_AUTHORIZATION'])) {
        return false;
    }

    // "Basic base64string" 형태 파싱
    $auth_header = $_SERVER['HTTP_AUTHORIZATION'];
    if (strpos($auth_header, 'Basic ') !== 0) {
        return false;
    }

    // Base64 디코딩
    $credentials = base64_decode(substr($auth_header, 6));
    list($username, $password) = explode(':', $credentials, 2);

    // htpasswd 파일 존재 확인
    if (!file_exists($htpasswd_file)) {
        return false;
    }

    // htpasswd 파일 읽기
    $lines = file($htpasswd_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    foreach ($lines as $line) {
        list($file_username, $file_hashed) = explode(':', $line, 2);
        
        if ($file_username === $username) {
            // bcrypt 검증
            if (password_verify($password, $file_hashed)) {
                return $username; // 인증 성공
            }
        }
    }

    return false;
}
?>

✓ 올바른 함수 설명: 이 함수는 HTTP_AUTHORIZATION 헤더를 확인하고, Base64 디코딩으로 사용자명과 비밀번호를 추출한다. htpasswd 파일을 라인 단위로 읽어서 해당 사용자를 찾고, password_verify()로 bcrypt 해시를 검증한다. 인증 성공 시 사용자명을 반환하고, 실패 시 false를 반환한다.

 

3단계. 실제 API 엔드포인트에서 인증 처리

위 함수를 사용해서 protected API를 만들어보자.

<?php
header('Content-Type: application/json');

$htpasswd_file = '/var/www/.htpasswd';

// 인증 검증
$authenticated_user = validate_http_basic_auth($htpasswd_file);

if ($authenticated_user === false) {
    // 인증 실패 시 401 응답 및 브라우저 팝업 표시
    header('HTTP/1.1 401 Unauthorized');
    header('WWW-Authenticate: Basic realm="API Access"');
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

// 인증 성공
echo json_encode([
    'status' => 'success',
    'user' => $authenticated_user,
    'message' => 'Protected API 접근 허용'
]);
?>

✓ 올바른 사용 방법: 인증 실패 시 "WWW-Authenticate" 헤더와 함께 401 상태 코드를 반환한다. 브라우저는 이 헤더를 감지해서 사용자명/비밀번호 입력 팝업을 표시한다. 인증 성공 시에만 API 응답을 보낸다.

 

4단계. 주의사항 및 보안 점검

✗ 잘못된 것: HTTP 연결에서 Basic Auth를 사용하면 사용자명과 비밀번호가 Base64로 인코딩되지만, 이는 암호화가 아니라 단순 인코딩이다. Base64는 누구나 디코딩할 수 있으므로 HTTPS를 필수로 사용해야 한다.

✓ 올바른 것: 항상 HTTPS 연결 위에서만 Basic Auth를 사용한다. 비밀번호 저장 시 password_hash()와 password_verify()로 bcrypt 해싱을 한다. htpasswd 파일 권한을 0600으로 설정해서 웹 서버 소유자만 읽을 수 있게 한다.

 

추가 보안 기법
<?php
// 1. 사용자 로그인 시도 횟수 제한(Brute Force 방지)
$username_for_auth = 'admin';
$attempt_key = 'login_attempt_' . $username_for_auth;
$cache_file = sys_get_temp_dir() . '/' . md5($attempt_key) . '.cache';

if (file_exists($cache_file)) {
    $data = json_decode(file_get_contents($cache_file), true);
    if ($data['attempts'] >= 5 && time() - $data['last_attempt'] < 300) {
        // 5분 내에 5회 이상 실패하면 잠금
        header('HTTP/1.1 429 Too Many Requests');
        echo json_encode(['error' => 'Too many login attempts']);
        exit;
    }
}

// 2. 로그인 시도 기록
if (validate_http_basic_auth($htpasswd_file) === false) {
    $data = file_exists($cache_file) ? json_decode(file_get_contents($cache_file), true) : ['attempts' => 0];
    $data['attempts']++;
    $data['last_attempt'] = time();
    file_put_contents($cache_file, json_encode($data));
}
?>

이 코드는 로그인 실패 횟수를 기록해서 Brute Force 공격을 방지한다. 5분 내에 5회 이상 실패하면 일시적으로 접근을 차단한다.

 

5단계. 클라이언트에서 Basic Auth 요청 테스트
curl -u admin:mypassword123 https://example.com/protected-api.php

curl의 -u 옵션으로 사용자명:비밀번호를 지정하면 자동으로 Authorization 헤더를 생성해서 요청한다.

 

JavaScript 클라이언트
const username = 'admin';
const password = 'mypassword123';
const credentials = btoa(username + ':' + password); // Base64 인코딩

fetch('https://example.com/protected-api.php', {
    method: 'GET',
    headers: {
        'Authorization': 'Basic ' + credentials
    }
})
.then(response => response.json())
.then(data => console.log(data));

✓ 올바른 방법: JavaScript에서 btoa()로 Base64 인코딩하고, Authorization 헤더에 "Basic " 접두사와 함께 전송한다.

 

정리 및 다음 단계

HTTP Basic Auth는 간단하면서도 강력한 인증 방식이다. 특히 내부 API나 관리자 페이지처럼 사용자가 많지 않은 환경에서 빠르게 적용할 수 있다. 다만 HTTPS 연결이 필수고, 대량의 사용자를 관리할 때는 토큰 기반 인증(JWT, OAuth)으로 전환하는 것이 낫다. 이 글의 htpasswd 파일 생성과 검증 로직을 참고해서 간단한 보호된 API 엔드포인트를 만들면, 더 복잡한 인증 시스템으로 나아갈 기초를 다질 수 있을 것이다.