웹 서버에 접근할 때 사용자명과 비밀번호를 입력하는 팝업 창을 본 적 있을까. 그것이 HTTP Basic Authentication이다. 대부분의 개발자들은 이 방식이 웹 서버(Nginx, Apache) 설정에서만 가능하다고 생각해서, 애플리케이션 레벨에서 구현할 생각을 안 한다. 하지만 PHP 코드 내에서 직접 Basic Auth를 검증하고 제어할 수 있다면, 특정 API 엔드포인트나 관리자 페이지를 더 유연하게 보호할 수 있다. 이번엔 PHP에서 htpasswd 형식의 암호화된 비밀번호를 읽고 검증해서 실제 인증 시스템을 구현하는 방법을 완벽하게 정리해보겠다.
HTTP Basic Authentication은 클라이언트가 Authorization 헤더에 Base64 인코딩된 "사용자명:비밀번호" 값을 담아 서버로 보내는 방식이다. 서버는 이 값을 디코딩해서 사용자 정보와 대조하면 된다. htpasswd 파일은 Apache 웹 서버에서 사용하던 형식인데, 사용자명과 함께 bcrypt나 MD5로 해시된 비밀번호를 저장한다. PHP에서도 이 파일을 읽고 비밀번호를 검증할 수 있다.
htpasswd 파일의 형식은 간단하다. 한 줄에 한 명의 사용자 정보가 기록되며, 사용자명과 해시된 비밀번호를 콜론(:)으로 구분한다. 예를 들어 "admin:$2y$10$..." 같은 형태다. htpasswd 명령어로 파일을 만들 수도 있고, PHP 코드로 직접 생성할 수도 있다.
먼저 htpasswd 파일을 만들어야 한다. 서버의 명령어로 생성하거나, PHP 코드로 동적으로 생성할 수 있다.
htpasswd -c /var/www/.htpasswd admin
위 명령어는 "/var/www/.htpasswd" 파일을 만들고 "admin" 사용자를 추가한다. 비밀번호 입력 프롬프트가 뜬다. bcrypt 해시(기본값)로 저장된다.
<?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으로 설정해서 소유자만 읽을 수 있게 한다.
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를 반환한다.
위 함수를 사용해서 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 응답을 보낸다.
✗ 잘못된 것: 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회 이상 실패하면 일시적으로 접근을 차단한다.
curl -u admin:mypassword123 https://example.com/protected-api.php
curl의 -u 옵션으로 사용자명:비밀번호를 지정하면 자동으로 Authorization 헤더를 생성해서 요청한다.
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 엔드포인트를 만들면, 더 복잡한 인증 시스템으로 나아갈 기초를 다질 수 있을 것이다.