PHP AJAX 요청 시 CORS 오류 해결하기

프리터코더·2025년 5월 25일
0

php 문제 해결

목록 보기
24/79

웹 애플리케이션에서 AJAX 요청 시 발생하는 CORS(Cross-Origin Resource Sharing) 오류와 해결 방법을 알아보겠습니다.

주요 오류 메시지

Access to XMLHttpRequest at 'http://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy
No 'Access-Control-Allow-Origin' header is present on the requested resource

원인 분석

  1. 다른 도메인 요청: 프론트엔드와 API 서버 도메인이 다름
  2. 누락된 CORS 헤더: 서버에서 적절한 헤더를 설정하지 않음
  3. Preflight 요청 실패: OPTIONS 요청 처리 누락

해결책

1. 기본 CORS 헤더 설정

<?php
// 기본 CORS 헤더 설정
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With");

// OPTIONS 요청 처리 (Preflight)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(200);
    exit();
}

// API 로직
echo json_encode(['message' => 'CORS 설정 완료']);
?>

2. 보안을 고려한 CORS 설정

<?php
class CorsHandler {
    private $allowedOrigins = [
        'http://localhost:3000',
        'https://myapp.com',
        'https://www.myapp.com'
    ];
    
    private $allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];
    private $allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With'];
    
    public function handleCors() {
        $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
        
        // Origin 검증
        if (in_array($origin, $this->allowedOrigins)) {
            header("Access-Control-Allow-Origin: $origin");
        }
        
        header("Access-Control-Allow-Methods: " . implode(', ', $this->allowedMethods));
        header("Access-Control-Allow-Headers: " . implode(', ', $this->allowedHeaders));
        header("Access-Control-Allow-Credentials: true");
        header("Access-Control-Max-Age: 3600"); // 1시간 캐시
        
        // Preflight 요청 처리
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            http_response_code(200);
            exit();
        }
    }
}

// 사용법
$cors = new CorsHandler();
$cors->handleCors();
?>

3. 미들웨어 형태의 CORS 처리

<?php
class CorsMiddleware {
    public static function handle($request, $next) {
        // CORS 헤더 설정
        self::setCorsHeaders();
        
        // OPTIONS 요청은 여기서 종료
        if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
            http_response_code(200);
            return;
        }
        
        // 다음 미들웨어 또는 컨트롤러 실행
        return $next($request);
    }
    
    private static function setCorsHeaders() {
        $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
        $allowedOrigins = self::getAllowedOrigins();
        
        if (in_array($origin, $allowedOrigins) || self::isLocalDevelopment()) {
            header("Access-Control-Allow-Origin: $origin");
        }
        
        header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
        header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, X-CSRF-Token");
        header("Access-Control-Allow-Credentials: true");
    }
    
    private static function getAllowedOrigins() {
        return [
            'https://myapp.com',
            'https://www.myapp.com',
            'https://admin.myapp.com'
        ];
    }
    
    private static function isLocalDevelopment() {
        $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
        return strpos($origin, 'localhost') !== false || 
               strpos($origin, '127.0.0.1') !== false;
    }
}
?>

4. API 라우터와 통합

<?php
require_once 'middleware/cors_middleware.php';

class ApiRouter {
    public function __construct() {
        // CORS 미들웨어 적용
        CorsMiddleware::handle($_REQUEST, function($request) {
            $this->route();
        });
    }
    
    private function route() {
        $method = $_SERVER['REQUEST_METHOD'];
        $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
        
        switch ($path) {
            case '/api/users':
                if ($method === 'GET') {
                    $this->getUsers();
                } elseif ($method === 'POST') {
                    $this->createUser();
                }
                break;
                
            case '/api/login':
                if ($method === 'POST') {
                    $this->login();
                }
                break;
                
            default:
                http_response_code(404);
                echo json_encode(['error' => 'Not Found']);
        }
    }
    
    private function getUsers() {
        header('Content-Type: application/json');
        echo json_encode(['users' => ['user1', 'user2']]);
    }
    
    private function createUser() {
        $input = json_decode(file_get_contents('php://input'), true);
        header('Content-Type: application/json');
        echo json_encode(['message' => 'User created', 'data' => $input]);
    }
    
    private function login() {
        $input = json_decode(file_get_contents('php://input'), true);
        header('Content-Type: application/json');
        echo json_encode(['token' => 'sample_token']);
    }
}

new ApiRouter();
?>

5. 환경별 CORS 설정

<?php
class CorsConfig {
    public static function getConfig() {
        $env = $_ENV['APP_ENV'] ?? 'development';
        
        $configs = [
            'development' => [
                'allowed_origins' => ['*'],
                'allowed_methods' => ['*'],
                'allowed_headers' => ['*'],
                'allow_credentials' => true
            ],
            'staging' => [
                'allowed_origins' => [
                    'https://staging.myapp.com',
                    'http://localhost:3000'
                ],
                'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
                'allowed_headers' => ['Content-Type', 'Authorization'],
                'allow_credentials' => true
            ],
            'production' => [
                'allowed_origins' => [
                    'https://myapp.com',
                    'https://www.myapp.com'
                ],
                'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
                'allowed_headers' => ['Content-Type', 'Authorization'],
                'allow_credentials' => true
            ]
        ];
        
        return $configs[$env] ?? $configs['production'];
    }
}
?>

6. 클라이언트 측 AJAX 요청

// 올바른 AJAX 요청 예시
async function makeApiRequest() {
    try {
        const response = await fetch('http://api.example.com/users', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + token
            },
            credentials: 'include', // 쿠키 포함
            body: JSON.stringify({
                name: 'John Doe',
                email: 'john@example.com'
            })
        });
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        console.log('Success:', data);
        
    } catch (error) {
        console.error('CORS Error:', error);
    }
}

디버깅 방법

# 브라우저 개발자 도구에서 네트워크 탭 확인
# OPTIONS 요청이 성공하는지 확인
# Response Headers에 CORS 헤더가 있는지 확인

예방법

  • 개발 초기부터 CORS 정책 수립
  • 환경별 허용 도메인 관리
  • Preflight 요청 처리 필수
  • 보안을 위한 최소 권한 원칙 적용
  • 정기적인 CORS 설정 검토
profile
일용직 개발자. freetercoder@gmail.com

0개의 댓글