웹 애플리케이션 개발 시 사용자의 폼 데이터가 의도치 않게 중복 전송되거나, 외부 사이트로부터 위조된 요청이 발생하는 것을 막는 것은 보안 측면에서 매우 중요합니다. PHP에서는 세션 기반의 토큰(Token) 메커니즘을 활용해 이러한 문제를 효과적으로 해결할 수 있습니다.
토큰의 역할과 원리
토큰은 무작위로 생성되는 문자열로, 주로 사용자 세션에 저장되어 클라이언트와 서버 간 요청의 유효성을 검증하는 데 사용됩니다. 이 방식은 다음과 같은 두 가지 주요 목적에 사용됩니다:
- 중복 제출 방지: 동일한 폼을 여러 번 제출하지 못하도록 차단
- CSRF(사이트 간 요청 위조) 방어: 타 사이트에서 위조된 요청이 실행되지 않도록 차단
서버는 사용자가 폼 페이지를 요청할 때 고유한 토큰을 생성하고, 이를 세션에 저장한 후 HTML 폼 내 숨김 필드(<input type="hidden">)로 전달합니다. 사용자가 폼을 제출하면, 서버는 전달받은 토큰과 세션에 저장된 값이 일치하는지 확인합니다.
기본 구현 예제
다음은 PHP를 이용해 토큰 기반 검증을 구현한 간단한 예입니다.
<?php
session_start();
// 토큰 생성 함수
function generateToken() {
$_SESSION['form_token'] = bin2hex(random_bytes(32));
}
// 토큰 검증 함수
function validateToken($submittedToken) {
$isValid = isset($_SESSION['form_token']) && hash_equals($_SESSION['form_token'], $submittedToken);
// 검증 후 토큰 재생성 (중복 제출 방지)
generateToken();
return $isValid;
}
// 세션에 토큰이 없으면 새로 생성
if (!isset($_SESSION['form_token'])) {
generateToken();
}
// 폼 제출 처리
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (validateToken($_POST['token'] ?? '')) {
echo '정상적으로 제출되었습니다: ' . htmlspecialchars($_POST['data']);
} else {
echo '무효한 요청입니다.';
}
}
?>
<form method="post" action="">
<input type="hidden" name="token" value="<?= $_SESSION['form_token'] ?>">
<input type="text" name="data" value="기본값">
<button type="submit">제출</button>
</form>
보안 강화된 토큰 생성기
더 안전한 토큰을 생성하기 위해 특수문자 조합과 해시 알고리즘을 활용할 수 있습니다. 아래는 사용자 정의 문자 집합을 기반으로 강력한 토큰을 생성하는 함수입니다.
<?php
function createSecureToken($length = 32) {
$pool = array_merge(
range('A', 'Z'), range('a', 'z'), range(0, 9),
str_split('!@#$%^&*()_+-=[]{}|;:,./?')
);
$maxIndex = count($pool) - 1;
$token = '';
for ($i = 0; $i < $length; $i++) {
$token .= $pool[random_int(0, $maxIndex)];
}
return hash('sha256', $token . uniqid('', true));
}
?>
폼 처리 분리 구조
실제 프로젝트에서는 폼 출력, 토큰 생성, 처리 로직을 분리하는 것이 좋습니다.
form_display.php
<?php
require_once 'security_helpers.php';
session_start();
$csrfToken = createSecureToken();
$_SESSION['csrf_token'] = $csrfToken;
?>
<form action="process_form.php" method="post">
<input type="hidden" name="csrf_token" value="<?= $csrfToken ?>">
<input type="text" name="username" placeholder="사용자명">
<button type="submit">전송</button>
</form>
process_form.php
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
die('잘못된 접근입니다.');
}
$submittedToken = $_POST['csrf_token'] ?? '';
$sessionToken = $_SESSION['csrf_token'] ?? '';
if (!hash_equals($sessionToken, $submittedToken)) {
http_response_code(403);
die('접근이 거부되었습니다. 유효하지 않은 토큰입니다.');
}
// 토큰 사용 후 폐기
unset($_SESSION['csrf_token']);
echo '처리 완료: ' . htmlspecialchars($_POST['username']);
?>
추가 보안 수칙
토큰만으로는 충분하지 않으며, 다음 조치도 함께 적용해야 합니다:
- XSS 공격 방지를 위해 출력 시
htmlspecialchars()사용 - 민감한 작업은 GET 대신 POST 요청으로 처리
- HTTPS 환경에서
HttpOnly및Secure속성으로 쿠키 보호 - Referer 헤더 검증 (보완적 수단)
- CSRF 토큰은 각 폼마다 고유하게 유지
이러한 방법들은 보안성과 성능 사이의 균형을 고려하여 상황에 맞게 조합하여 사용해야 하며, 특히 금융, 인증 등 민감한 작업에는 반드시 다중 보안 장치를 적용하는 것이 권장됩니다.