Slim Framework 매뉴얼 CORS 설정, POST 파일 업로드

CORS 설정 (Setting up CORS)

CORS – 교차 오리진 리소스 공유

CORS 지원 구현에 적합한 순서도 참조:

CORS 서버 순서도

CORS 지원 서비스는 http://www.test-cors.org/에서 테스트할 수 있습니다.

사양은 https://www.w3.org/TR/cors/에서 확인할 수 있습니다.

간단한 해결책

간단한 CORS 요청의 경우 서버는 응답에 다음 헤더만 추가하면 됩니다.

Access-Control-Allow-Origin: <domain>, ... 

다음 코드는 게으른(lazy) CORS를 활성화해야 합니다.

$app->options('/{routes:.+}', function ($request, $response, $args) {
    return $response;
});
​
$app->add(function ($request, $handler) {
    $response = $handler->handle($request);
    return $response
            ->withHeader('Access-Control-Allow-Origin', 'http://mysite')
            ->withHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept, Origin, Authorization')
            ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
});

다음 경로를 마지막 경로로 추가합니다.

<?php

use Slim\Exception\HttpNotFoundException;
​
/**
 * Catch-all route to serve a 404 Not Found page if none of the routes match
 * NOTE: make sure this route is defined last
 */
$app->map(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], '/{routes:.+}', function ($request, $response) {
    throw new HttpNotFoundException($request);
});

액세스-제어-허용-방법 (Access-Control-Allow-Methods)

다음의 미들웨어는 Slim의 라우터를 쿼리하고 특정 패턴의 메소드 목록을 가져오는 데 사용할 수 있다.

다음은 응용 프로그램의 전체 예입니다.

<?php
​
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;
use Slim\Routing\RouteContext;
​
require_once __DIR__ . '/../vendor/autoload.php';
​
$app = AppFactory::create();
​
$app->addBodyParsingMiddleware();
​
// This middleware will append the response header Access-Control-Allow-Methods with all allowed methods
$app->add(function (Request $request, RequestHandlerInterface $handler): Response {
    $routeContext = RouteContext::fromRequest($request);
    $routingResults = $routeContext->getRoutingResults();
    $methods = $routingResults->getAllowedMethods();
    $requestHeaders = $request->getHeaderLine('Access-Control-Request-Headers');
​
    $response = $handler->handle($request);
​
    $response = $response->withHeader('Access-Control-Allow-Origin', '*');
    $response = $response->withHeader('Access-Control-Allow-Methods', implode(',', $methods));
    $response = $response->withHeader('Access-Control-Allow-Headers', $requestHeaders);
​
    // Optional: Allow Ajax CORS requests with Authorization header
    // $response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
​
    return $response;
});
​
// The RoutingMiddleware should be added after our CORS middleware so routing is performed first
$app->addRoutingMiddleware();
​
// The routes
$app->get('/api/v0/users', function (Request $request, Response $response): Response {
    $response->getBody()->write('List all users');
​
    return $response;
});
​
$app->get('/api/v0/users/{id}', function (Request $request, Response $response, array $arguments): Response {
    $userId = (int)$arguments['id'];
    $response->getBody()->write(sprintf('Get user: %s', $userId));
​
    return $response;
});
​
$app->post('/api/v0/users', function (Request $request, Response $response): Response {
    // Retrieve the JSON data
    $parameters = (array)$request->getParsedBody();
​
    $response->getBody()->write('Create user');
​
    return $response;
});
​
$app->delete('/api/v0/users/{id}', function (Request $request, Response $response, array $arguments): Response {
    $userId = (int)$arguments['id'];
    $response->getBody()->write(sprintf('Delete user: %s', $userId));
​
    return $response;
});
​
// Allow preflight requests
// Due to the behaviour of browsers when sending a request,
// you must add the OPTIONS method. Read about preflight.
$app->options('/api/v0/users', function (Request $request, Response $response): Response {
    // Do nothing here. Just return the response.
    return $response;
});
​
// Allow additional preflight requests
$app->options('/api/v0/users/{id}', function (Request $request, Response $response): Response {
    return $response;
});
​
// Using groups
$app->group('/api/v0/users/{id:[0-9]+}', function (RouteCollectorProxy $group) {
    $group->put('', function (Request $request, Response $response, array $arguments): Response {
        // Your code here...
        $userId = (int)$arguments['id'];
        $response->getBody()->write(sprintf('Put user: %s', $userId));
​
        return $response;
    });
​
    $group->patch('', function (Request $request, Response $response, array $arguments): Response {
        $userId = (int)$arguments['id'];
        $response->getBody()->write(sprintf('Patch user: %s', $userId));
​
        return $response;
    });
​
    // Allow preflight requests
    $group->options('', function (Request $request, Response $response): Response {
        return $response;
    });
});
​
$app->run();

액세스-제어-허용-자격 증명 (Access-Control-Allow-Credentials)

요청에 자격 증명(쿠키, 권한 부여 헤더 또는 TLS 클라이언트 인증서)이 포함된 경우 응답 개체에 Access-Control-Allow-Credentials 헤더를 추가해야 할 수 있습니다.

$response = $response->withHeader('Access-Control-Allow-Credentials', 'true');

POST 양식을 사용하여 파일 업로드 (Uploading files using POST forms)

POST 요청의 양식을 사용하여 업로드된 파일은 요청 메서드 getUploadedFiles()를 사용하여 검색할 수 있습니다.

When uploading files using a POST request, make sure your file upload form has the attribute enctype="multipart/form-data" otherwise getUploadedFiles() will return an empty array.

POST 요청을 사용하여 파일을 업로드할 때 파일 업로드 양식이 enctype="multipart/form-data" 속성을 가지고 있는지 확인하십시오. 그렇지 않으면 ‘getUploadedFiles()’가 빈 배열을 반환합니다.

동일한 입력 이름에 대해 여러 파일이 업로드된 경우 HTML의 입력 이름 뒤에 대괄호를 추가하십시오. 그렇지 않으면 getUploadedFiles()에 의해 입력 이름에 대해 업로드된 파일 하나만 반환됩니다.

다음은 단일 및 다중 파일 업로드를 모두 포함하는 예제 HTML 양식입니다.

<!-- make sure the attribute enctype is set to multipart/form-data -->
<form method="post" enctype="multipart/form-data">
    <!-- upload of a single file -->
    <p>
        <label>Add file (single): </label><br/>
        <input type="file" name="example1"/>
    </p>
​
    <!-- multiple input fields for the same input name, use brackets -->
    <p>
        <label>Add files (up to 2): </label><br/>
        <input type="file" name="example2[]"/><br/>
        <input type="file" name="example2[]"/>
    </p>
​
    <!-- one file input field that allows multiple files to be uploaded, use brackets -->
    <p>
        <label>Add files (multiple): </label><br/>
        <input type="file" name="example3[]" multiple="multiple"/>
    </p>
​
    <p>
        <input type="submit"/>
    </p>
</form>

Figure 1: 파일 업로드를 위한 HTML 양식 예제

업로드한 파일은 ‘moveTo’ 방식으로 디렉터리로 이동할 수 있습니다. 아래는 위의 HTML 양식에서 업로드된 파일을 처리하는 예제 응용프로그램입니다.

<?php
​
use DI\ContainerBuilder;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UploadedFileInterface;
use Slim\Factory\AppFactory;
​
require __DIR__ . '/../vendor/autoload.php';
​
$containerBuilder = new ContainerBuilder();
$container = $containerBuilder->build();
​
$container->set('upload_directory', __DIR__ . '/uploads');
​
AppFactory::setContainer($container);
$app = AppFactory::create();
​
$app->post('/', function (ServerRequestInterface $request, ResponseInterface $response) {
    $directory = $this->get('upload_directory');
    $uploadedFiles = $request->getUploadedFiles();
​
    // handle single input with single file upload
    $uploadedFile = $uploadedFiles['example1'];
    if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
        $filename = moveUploadedFile($directory, $uploadedFile);
        $response->getBody()->write('Uploaded: ' . $filename . '<br/>');
    }
​
    // handle multiple inputs with the same key
    foreach ($uploadedFiles['example2'] as $uploadedFile) {
        if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
            $filename = moveUploadedFile($directory, $uploadedFile);
            $response->getBody()->write('Uploaded: ' . $filename . '<br/>');
        }
    }
​
    // handle single input with multiple file uploads
    foreach ($uploadedFiles['example3'] as $uploadedFile) {
        if ($uploadedFile->getError() === UPLOAD_ERR_OK) {
            $filename = moveUploadedFile($directory, $uploadedFile);
            $response->getBody()->write('Uploaded: ' . $filename . '<br/>');
        }
    }
​
    return $response;
});
​
/**
 * Moves the uploaded file to the upload directory and assigns it a unique name
 * to avoid overwriting an existing uploaded file.
 *
 * @param string $directory The directory to which the file is moved
 * @param UploadedFileInterface $uploadedFile The file uploaded file to move
 *
 * @return string The filename of moved file
 */
function moveUploadedFile(string $directory, UploadedFileInterface $uploadedFile)
{
    $extension = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
​
    // see http://php.net/manual/en/function.random-bytes.php
    $basename = bin2hex(random_bytes(8));
    $filename = sprintf('%s.%0.8s', $basename, $extension);
​
    $uploadedFile->moveTo($directory . DIRECTORY_SEPARATOR . $filename);
​
    return $filename;
}
​
$app->run();

Figure 2: 업로드된 파일을 처리하는 Slim 애플리케이션의 예