PHP에서 랜덤값 충돌 문제 해결하기

프리터코더·2025년 6월 1일
0

php 문제 해결

목록 보기
53/79

랜덤값 생성 시 중복이나 예측 가능한 값이 생성되면 보안 문제나 데이터 무결성 문제가 발생할 수 있습니다. 안전하고 고유한 랜덤값을 생성하는 방법들을 알아보겠습니다.

1. 암호학적으로 안전한 랜덤 문자열

<?php
function generateSecureRandomString($length = 32) {
    // PHP 7.0+ random_bytes 사용
    $bytes = random_bytes($length);
    return bin2hex($bytes);
}

function generateRandomToken($length = 16) {
    // base64 인코딩으로 더 짧은 문자열
    $bytes = random_bytes($length);
    return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
}

// 사용 예시
echo "보안 문자열: " . generateSecureRandomString(16) . "\n";
echo "토큰: " . generateRandomToken(12) . "\n";
?>

2. UUID 생성으로 고유성 보장

<?php
function generateUUID() {
    $data = random_bytes(16);
    
    // 버전과 variant 비트 설정
    $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // version 4
    $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // variant bits
    
    return sprintf('%08s-%04s-%04s-%04s-%12s',
        bin2hex(substr($data, 0, 4)),
        bin2hex(substr($data, 4, 2)),
        bin2hex(substr($data, 6, 2)),
        bin2hex(substr($data, 8, 2)),
        bin2hex(substr($data, 10, 6))
    );
}

function generateShortUUID() {
    // 더 짧은 고유 ID (22자)
    return rtrim(strtr(base64_encode(random_bytes(16)), '+/', '-_'), '=');
}

echo "UUID: " . generateUUID() . "\n";
echo "Short UUID: " . generateShortUUID() . "\n";
?>

3. 데이터베이스 중복 체크와 재시도

<?php
function generateUniqueId($pdo, $table, $column, $length = 8) {
    $max_attempts = 10;
    $attempt = 0;
    
    do {
        $id = generateRandomToken($length);
        
        $stmt = $pdo->prepare("SELECT COUNT(*) FROM {$table} WHERE {$column} = ?");
        $stmt->execute([$id]);
        $exists = $stmt->fetchColumn() > 0;
        
        $attempt++;
        
        if ($attempt >= $max_attempts) {
            throw new Exception("고유 ID 생성 실패: 최대 시도 횟수 초과");
        }
        
    } while ($exists);
    
    return $id;
}

// 사용 예시
try {
    $pdo = new PDO($dsn, $username, $password);
    $unique_code = generateUniqueId($pdo, 'users', 'user_code', 10);
    echo "고유 코드: " . $unique_code . "\n";
} catch (Exception $e) {
    echo "오류: " . $e->getMessage() . "\n";
}
?>

4. 시간 기반 고유 ID 생성

<?php
function generateTimeBasedId() {
    // 마이크로초 포함 타임스탬프 + 랜덤값
    $timestamp = microtime(true);
    $random = random_bytes(4);
    
    return base_convert($timestamp * 1000000, 10, 36) . bin2hex($random);
}

function generateSnowflakeId() {
    // Twitter Snowflake 방식 (간단화)
    static $sequence = 0;
    static $last_timestamp = 0;
    
    $timestamp = round(microtime(true) * 1000);
    
    if ($timestamp === $last_timestamp) {
        $sequence = ($sequence + 1) & 0xFFF; // 12비트 시퀀스
        if ($sequence === 0) {
            // 같은 밀리초에 시퀀스 오버플로우 시 대기
            usleep(1000);
            $timestamp = round(microtime(true) * 1000);
        }
    } else {
        $sequence = 0;
    }
    
    $last_timestamp = $timestamp;
    
    // 타임스탬프(41비트) + 머신ID(10비트) + 시퀀스(12비트)
    $machine_id = 1; // 실제로는 서버별 고유값
    $id = ($timestamp << 22) | ($machine_id << 12) | $sequence;
    
    return $id;
}

echo "시간 기반 ID: " . generateTimeBasedId() . "\n";
echo "Snowflake ID: " . generateSnowflakeId() . "\n";
?>

5. 랜덤 ID 생성기 클래스

<?php
class RandomIdGenerator {
    private $pdo;
    private $charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    
    public function __construct($pdo = null) {
        $this->pdo = $pdo;
    }
    
    public function generateCode($length = 8, $exclude_similar = true) {
        $charset = $this->charset;
        
        if ($exclude_similar) {
            // 혼동하기 쉬운 문자 제외
            $charset = str_replace(['0', 'O', '1', 'I'], '', $charset);
        }
        
        $code = '';
        $max = strlen($charset) - 1;
        
        for ($i = 0; $i < $length; $i++) {
            $code .= $charset[random_int(0, $max)];
        }
        
        return $code;
    }
    
    public function generateUniqueCode($table, $column, $length = 8, $max_attempts = 20) {
        if (!$this->pdo) {
            throw new Exception("데이터베이스 연결이 필요합니다");
        }
        
        for ($i = 0; $i < $max_attempts; $i++) {
            $code = $this->generateCode($length);
            
            if (!$this->codeExists($table, $column, $code)) {
                return $code;
            }
        }
        
        throw new Exception("고유 코드 생성 실패");
    }
    
    private function codeExists($table, $column, $code) {
        $stmt = $this->pdo->prepare("SELECT 1 FROM {$table} WHERE {$column} = ? LIMIT 1");
        $stmt->execute([$code]);
        return $stmt->fetchColumn() !== false;
    }
    
    public function generateNumericId($length = 6) {
        $min = pow(10, $length - 1);
        $max = pow(10, $length) - 1;
        return random_int($min, $max);
    }
}

// 사용 예시
$generator = new RandomIdGenerator($pdo);
echo "코드: " . $generator->generateCode(8) . "\n";
echo "숫자 ID: " . $generator->generateNumericId(6) . "\n";
?>

6. 세션 토큰 생성

<?php
function generateSessionToken() {
    // 32바이트 = 256비트 보안 강도
    return bin2hex(random_bytes(32));
}

function generateCSRFToken() {
    if (!isset($_SESSION)) {
        session_start();
    }
    
    $token = bin2hex(random_bytes(32));
    $_SESSION['csrf_token'] = $token;
    $_SESSION['csrf_time'] = time();
    
    return $token;
}

function validateCSRFToken($token) {
    if (!isset($_SESSION)) {
        session_start();
    }
    
    // 토큰 존재 확인
    if (!isset($_SESSION['csrf_token']) || !isset($_SESSION['csrf_time'])) {
        return false;
    }
    
    // 시간 만료 확인 (1시간)
    if (time() - $_SESSION['csrf_time'] > 3600) {
        unset($_SESSION['csrf_token'], $_SESSION['csrf_time']);
        return false;
    }
    
    // 토큰 일치 확인 (타이밍 공격 방지)
    return hash_equals($_SESSION['csrf_token'], $token);
}

// 사용 예시
$session_token = generateSessionToken();
$csrf_token = generateCSRFToken();

echo "세션 토큰: " . $session_token . "\n";
echo "CSRF 토큰: " . $csrf_token . "\n";
?>

7. 파일명 랜덤화

<?php
function generateRandomFilename($original_filename, $preserve_extension = true) {
    $random_name = bin2hex(random_bytes(16));
    
    if ($preserve_extension && pathinfo($original_filename, PATHINFO_EXTENSION)) {
        $extension = pathinfo($original_filename, PATHINFO_EXTENSION);
        return $random_name . '.' . $extension;
    }
    
    return $random_name;
}

function generateSafeFilename($original_filename) {
    // 원본 파일명 일부 + 타임스탬프 + 랜덤값
    $name = pathinfo($original_filename, PATHINFO_FILENAME);
    $extension = pathinfo($original_filename, PATHINFO_EXTENSION);
    
    // 파일명 정리 (특수문자 제거)
    $safe_name = preg_replace('/[^a-zA-Z0-9_-]/', '', $name);
    $safe_name = substr($safe_name, 0, 20); // 길이 제한
    
    $timestamp = date('YmdHis');
    $random = bin2hex(random_bytes(4));
    
    return $safe_name . '_' . $timestamp . '_' . $random . '.' . $extension;
}

// 사용 예시
$original = "사용자업로드파일.jpg";
echo "랜덤 파일명: " . generateRandomFilename($original) . "\n";
echo "안전한 파일명: " . generateSafeFilename($original) . "\n";
?>

8. 랜덤값 품질 테스트

<?php
function testRandomQuality($generator_func, $count = 10000) {
    $values = [];
    $duplicates = 0;
    
    for ($i = 0; $i < $count; $i++) {
        $value = $generator_func();
        
        if (isset($values[$value])) {
            $duplicates++;
        } else {
            $values[$value] = true;
        }
    }
    
    $unique_rate = (($count - $duplicates) / $count) * 100;
    
    echo "테스트 결과:\n";
    echo "- 총 생성: {$count}개\n";
    echo "- 중복: {$duplicates}개\n";
    echo "- 고유율: " . number_format($unique_rate, 2) . "%\n";
    
    return $unique_rate > 99.9; // 99.9% 이상이면 양호
}

// 테스트 실행
echo "8자리 코드 테스트:\n";
testRandomQuality(function() {
    return bin2hex(random_bytes(4));
});

echo "\nUUID 테스트:\n";
testRandomQuality(function() {
    return generateUUID();
});
?>

핵심 포인트

  • random_bytes() 함수로 암호학적으로 안전한 랜덤값 생성
  • UUID 사용으로 전역적 고유성 보장
  • 데이터베이스 중복 체크와 재시도 로직 구현
  • 시간 기반 ID로 순서 보장과 고유성 동시 확보
  • CSRF 토큰은 세션별로 생성하고 시간 제한 설정
profile
일용직 개발자. freetercoder@gmail.com

0개의 댓글