Web Hosting프로그래밍

[aws lamp] 외부 서비스나 API에 의존하지 않고 서버 안에서 100% 자급자족하는 자체 PHP 이미지 캡차(captcha) 구현 방법 예제

외부 서비스나 API에 의존하지 않고 서버 안에서 100% 자급자족하는 자체 PHP 이미지 캡차를 적용해 보겠습니다. 비용이 완전히 제로이며, 외부 서버 먹통으로 인한 장애 걱정도 없습니다.

현재 웹사이트의 모든 주소(특히 악성 봇이 쑤시고 있는 php 페이지들)에 이 감지 시스템이 일괄 적용되도록 설치하는 전체 단계를 안내해 드립니다.

[1단계] PHP GD 라이브러리 설치 확인

실시간으로 글자가 적힌 이미지를 그리려면 PHP의 GD 라이브러리가 필요합니다. Bitnami LAMP 환경의 SSH 터미널에서 아래 명령어를 입력해 설치되어 있는지 확인하고, 없으면 설치합니다.

# GD 라이브러리 설치 (Ubuntu 기반 Bitnami 기준)
sudo apt-get update
sudo apt-get install -y php-gd

# 적용을 위해 아파치 재시작
sudo /opt/bitnami/ctlscript.sh restart apache

라이트세일 LAMP는 이미 설치되어 있음으로 1단계는 패스합니다.

[2단계] 소스 코드 파일 3개 생성하기

웹 소스 코드가 위치한 루트 디렉터리(일반적으로 /opt/bitnami/apache/htdocs/)에 아래 3개의 파일을 생성합니다.

bot_protector.php (상단 감지 스크립트)

이 코드가 모든 페이지 맨 위에서 실행되면서 트래픽 속도를 감시합니다.

<?php
if (session_status() == PHP_SESSION_NONE) {
    session_start();
}

// 캡차 화면이나 이미지 자체는 무한 루프 방지를 위해 감지에서 제외
$current_script = basename($_SERVER['SCRIPT_NAME']);
if ($current_script === 'captcha_page.php' || $current_script === 'captcha_img.php') {
    return;
}

$current_time = time();

// 1. 요청 기록 세션 관리
if (!isset($_SESSION['request_history'])) {
    $_SESSION['request_history'] = [];
}

$_SESSION['request_history'][] = $current_time;

// 최근 10초간의 기록만 필터링하여 남김
$_SESSION['request_history'] = array_filter($_SESSION['request_history'], function($timestamp) use ($current_time) {
    return $timestamp > ($current_time - 10);
});

// 2. 임계값 설정 (10초 동안 요청이 15회를 초과하면 캡차 모드 발동)
if (count($_SESSION['request_history']) > 15) {
    $_SESSION['captcha_required'] = true;
}

// 3. 캡차 인증을 통과하지 못한 경우 페이지 접근을 막고 캡차 화면을 강제 표시
if (isset($_SESSION['captcha_required']) && $_SESSION['captcha_required'] === true) {
    include __DIR__ . '/captcha_page.php';
    exit;
}
?>

captcha_img.php (일회성 숫자 이미지 생성)

요청할 때마다 새로운 숫자 4자리를 세션에 굽고 가짜 이미지를 출력합니다.

<?php
if (session_status() == PHP_SESSION_NONE) {
    session_start();
}
header("Content-Type: image/png");

// 4자리 숫자를 무작위 생성하여 세션에 저장
$captcha_code = (string)rand(1000, 9999);
$_SESSION['my_captcha'] = $captcha_code;

// 가로 120, 세로 40 크기의 도화지 생성
$img = imagecreatetruecolor(120, 40);

// 색상 지정 (RGB)
$bg_color = imagecolorallocate($img, 245, 245, 245); // 배경: 밝은 회색
$text_color = imagecolorallocate($img, 30, 30, 30);    // 글자: 짙은 검은색
$noise_color = imagecolorallocate($img, 180, 180, 180); // 방해물: 연한 회색

imagefill($img, 0, 0, $bg_color);

// 봇의 이미지 분석을 방해하기 위한 선(Line) 그리기
for ($i = 0; $i < 6; $i++) {
    imageline($img, rand(0, 120), rand(0, 40), rand(0, 120), rand(0, 40), $noise_color);
}

// 점(Dot) 노이즈 뿌리기
for ($i = 0; $i < 100; $i++) {
    imagesetpixel($img, rand(0, 120), rand(0, 40), $noise_color);
}

// 내장 폰트(크기 5)를 이용해 글자 배치 (약간의 여백 부여)
imagestring($img, 5, 40, 12, $captcha_code, $text_color);

imagepng($img);
imagedestroy($img);
?>

captcha_page.php (사용자에게 보일 입력 폼)

봇이 걸렸을 때 강제로 띄워줄 화면입니다.

<?php
if (session_status() == PHP_SESSION_NONE) {
    session_start();
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['user_code'])) {
    // 입력값과 세션에 저장된 정답 비교
    if (!empty($_SESSION['my_captcha']) && $_POST['user_code'] === $_SESSION['my_captcha']) {
        $_SESSION['captcha_required'] = false;
        $_SESSION['request_history'] = []; // 기록 리셋
        
        // 보던 원래 페이지로 안전하게 리다이렉트
        header("Location: " . $_SERVER['REQUEST_URI']);
        exit;
    } else {
        $error_msg = "번호가 일치하지 않습니다. 다시 정확히 입력해 주세요.";
    }
}
?>
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>보안을 위한 안내</title>
</head>
<body style="text-align: center; margin-top: 120px; font-family: sans-serif; background-color: #f9f9f9; color: #333;">
    <div style="display: inline-block; padding: 30px; background: white; border: 1px solid #ddd; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05);">
        <h2 style="margin-top: 0; color: #d9534f;">⚠️ 비정상적인 접근 감지</h2>
        <p style="font-size: 14px; color: #666;">짧은 시간 동안 너무 많은 요청이 발생하여 접근이 일시 제한되었습니다.<br>서비스를 계속 이용하시려면 아래 이미지 안의 숫자 4자리를 입력해 주세요.</p>
        
        <!-- 실시간 이미지 호출 -->
        <div style="margin: 20px 0;">
            <img src="captcha_img.php?t=<?php echo time(); ?>" alt="보안 문자" style="border: 1px solid #ccc; border-radius: 4px; vertical-align: middle;">
        </div>
        
        <?php if (isset($error_msg)): ?>
            <p style="color: red; font-size: 13px; font-weight: bold;"><?php echo $error_msg; ?></p>
        <?php endif; ?>

        <form method="POST" action="<?php echo htmlspecialchars($_SERVER['REQUEST_URI']); ?>">
            <input type="text" name="user_code" placeholder="숫자 4자리 입력" style="padding: 10px; width: 140px; text-align: center; font-size: 16px; border: 1px solid #ccc; border-radius: 4px; margin-right: 5px;" autocomplete="off" required maxlength="4">
            <button type="submit" style="padding: 10px 20px; font-size: 16px; background-color: #0275d8; color: white; border: none; border-radius: 4px; cursor: pointer;">확인</button>
        </form>
    </div>
</body>
</html>

[3단계] 웹사이트 전체에 일괄 적용하기 (가장 중요)

기존에 만들어둔 웹사이트 소스 코드 파일이 한두 개라면 각각의 파일 맨 위에 include 'bot_protector.php';를 적어주면 됩니다.

하지만 지금 크롤러 봇은 여러 경로의 파일들을 무작위로 타격하고 있으므로, 아파치 설정을 이용해 서버 내 모든 PHP 파일이 실행되기 전에 bot_protector.php가 무조건 자동 실행되도록 강제하는 것이 완벽합니다.


1.php.ini 설치 위치 찾기

AWS 라이트세일 비트나미 LAMP 스택 버전에 따라 php.ini 파일의 실제 위치가 다를 수 있습니다

php -i | grep "Loaded Configuration File"

정확한 경로를 찾기 위해 SSH 터미널창에 명령어를 입력해 보세요. 현재 웹 서버가 사용 중인 php.ini 경로를 바로 출력해 줍니다

명령어를 실행하면 보통 아래 3가지 중 하나의 경로가 나타납니다.

  • 최신 비트나미 버전 경로 (가장 유력):
    /opt/bitnami/php/etc/php.ini
  • 일부 구버전 경로:
    /opt/bitnami/php/lib/php.ini
  • 기타 패키지 경로:
    /etc/php/.../apache2/php.ini

2. php기본 설파일 열기

vi /opt/bitnami/php/lib/php.ini

3. 파일이 열리면 키보드 명령 모드 상태입니다. 아래 명령어를 입력하고 Enter를 누르면 해당 설정 단어로 바로 이동합니다.

/auto_prepend_file

(만약 엉뚱한 곳으로 갔다면 알파벳 n을 누르면 다음 일치하는 단어로 이동합니다.)

4. 코드 수정하기 (편집 모드 전환)
auto_prepend_file = 항목 위치를 찾았다면, 키보드 알파벳 i를 누릅니다.

화면 맨 아래에 -- INSERT -- 또는 -- 끼워넣기 --라고 표시되며 편집이 가능한 상태가 됩니다.

방향키로 이동하여 기존 내용을 지우고 아래와 같이 절대 경로를 정확히 입력합니다.

auto_prepend_file = "/opt/bitnami/apache/htdocs/bot_protector.php"

5. 저장하고 나가기
수정이 끝났다면 아래 순서대로 키를 입력하여 저장하고 빠져나옵니다.

wq를 입력한 뒤 Enter를 누릅니다. (write and quit, 저장 후 종료)

키보드 Esc 키를 누릅니다 (화면 아래 -- INSERT -- 표시가 사라집니다).

콜론(:)을 입력합니다.

6. 수정한 설정을 전체 웹 서버에 반영하기 위해 서비스를 재시작합니다.

sudo /opt/bitnami/ctlscript.sh restart

요약정리

# 1. 확인된 경로로 php.ini 열기
sudo nano /opt/bitnami/php/etc/php.ini

# 2. Ctrl + W 를 눌러 auto_prepend_file 을 검색합니다.

# 3. 비어있는 해당 항목을 아래처럼 절대 경로로 수정합니다.
auto_prepend_file = "/opt/bitnami/apache/htdocs/bot_protector.php"

# 4. 저장 후 종료 (Ctrl + O -> Enter -> Ctrl + X)

# 5. 비트나미 서비스 전체 재시작
sudo /opt/bitnami/ctlscript.sh restart


작동 원리 요약

이제 일반 사용자가 접속할 때는 아무런 변화가 없습니다. 하지만 악성 봇이 초당 여러 번 페이지를 요청(F5 폭주 등)하면 bot_protector.php가 이를 캐치하여 세션을 끊고 captcha_page.php로 강제 화면을 전환합니다.

봇은 글자를 입력해 버튼을 누르는 POST 액션을 정상적으로 수행하지 못하므로 해당 시점부터 웹 서버 내부의 실제 연산 로직(_calculator.php 등의 본문)을 전혀 실행하지 못하고 빈 페이지만 보게 되어 크롤링이 완벽하게 차단됩니다.

이 방식으로 세팅을 마친 후 브라우저에서 새로고침을 빠르게 연타(F5)해보시면 실제로 캡차 화면이 작동하는 것을 직접 테스트해 보실 수 있습니다.

Hi, I’m 관리자