Slim php:如何在不添加 "options" 路由的情况下处理 CORS 请求

Slim php: How to handle CORS request without adding "options" routes

我有一个 slim 应用程序,我需要配置 CORS,但是当我检查 CORS chapter in the documentation 时,如果我想处理请求方法,我必须添加一个新的 options 路由到每个端点(摘自文档):

$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;
});

// 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;
});

如何避免每次都创建新路由的痛苦?

经过一番挖掘,我能够通过创建自定义路由中间件来避免为每个路由添加新的处理程序。这个想法是它检查路由是否使用 Access-Control-Request-Method header 中发送的方法解析,如果有的话,更改可调用的路由以避免调用实际的处理程序。

class MyRoutingMiddleware extends \Slim\Middleware\RoutingMiddleware
{

    /**
     * @var \Psr\Http\Message\ResponseFactoryInterface
     */
    protected $responseFactory;

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        $request = $this->performRouting($request);
        if ($this->isPreflight($request)) {
            return new Response(200);
        }
        return $handler->handle($request);
    }

    protected function isPreflight(ServerRequestInterface $request): bool
    {
        return $request->getMethod() === "OPTIONS" &&
            $request->getHeaderLine("Access-Control-Request-Method") !== '';
    }

    protected function resolveRoutingResultsFromRequest(ServerRequestInterface $request): RoutingResults
    {
        $accessControlRequestMethod = $request->getHeaderLine("Access-Control-Request-Method");
        $isPreflight = $this->isPreflight($request);
        return $this->routeResolver->computeRoutingResults(
            $request->getUri()->getPath(),
            $isPreflight ? $accessControlRequestMethod : $request->getMethod()
        );
    }
}

然后我不再使用默认的 RoutingMiddleware :

//add cors middleware globally
$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 routes
$app->get('/api/v0/users', function (Request $request, Response $response): Response {
    $response->getBody()->write('List all users');

    return $response;
});
// No need to add the $app->options(...) anymore !
//use custom routing middleware
$app->add(new MyRoutingMiddleware($app->getRouteResolver(), $app->getRouteCollector()->getRouteParser()));

更新

如果在您的代码中抛出异常(例如 HttpNotFoundException),则不会进行 CORS 处理,这可能是个问题,尤其是对于 GET 请求(没有预检,但 headers 已检查),所以我不再使用 slim 文档中提供的中间件,而是创建了一个工厂,允许您在路由上单独处理 cors 并使用 ErrorHandler 处理错误(因此您的错误输出是有或没有 CORS 都一样):

class CorsMiddlewareFactory
{
    /**
     * @var \Slim\Handlers\ErrorHandler
     */
    protected $errorHandler;

    /**
     * @var bool
     */
    protected $logErrorDetails;

    /**
     * @var bool
     */
    protected $logErrors;

    /**
     * @var bool
     */
    protected $displayErrorDetails;

    /**
     * CorsMiddlewareFactory constructor.
     * @param \Slim\Handlers\ErrorHandler $errorHandler
     * @param bool $logErrorDetails
     * @param bool $logErrors
     * @param bool $displayErrorDetails
     */
    public function __construct(ErrorHandler $errorHandler, bool $logErrorDetails, bool $logErrors, bool $displayErrorDetails)
    {
        $this->errorHandler = $errorHandler;
        $this->logErrorDetails = $logErrorDetails;
        $this->logErrors = $logErrors;
        $this->displayErrorDetails = $displayErrorDetails;
    }


    public function create(array $origin = [], bool $allowCredentials = false, ?array $allowedHeaders = [], array $exposedHeaders = [])
    {
        $errorHandler = $this->errorHandler;
        $logErrorDetails = $this->logErrorDetails;
        $logErrors = $this->logErrors;
        $displayErrorDetails = $this->displayErrorDetails;
        return function (ServerRequestInterface $request, RequestHandlerInterface $handler)
        use ($allowCredentials, $allowedHeaders, $origin, $exposedHeaders, $errorHandler, $logErrorDetails, $logErrors, $displayErrorDetails): ResponseInterface {
            $routeContext = RouteContext::fromRequest($request);
            $routingResults = $routeContext->getRoutingResults();
            $methods = $routingResults->getAllowedMethods();
            if ($allowedHeaders === null) {
                $allowedHeaders = $request->getHeader('Access-Control-Request-Headers');
            }
            $isPreflight = $request->getMethod() === "OPTIONS" && !empty($request->getHeader("Access-Control-Request-Method"));
            try {
                $response = $handler->handle($request);
            } catch (\Throwable $exception) {
                $response = ($errorHandler)($request, $exception, $displayErrorDetails, $logErrors, $logErrorDetails);
            }
            $headers=[
                'Access-Control-Allow-Origin'=> implode(',', $origin),
                'Access-Control-Allow-Methods'=> implode(',', $methods),
                'Access-Control-Allow-Headers'=> implode(',', $allowedHeaders),
                'Access-Control-Allow-Credentials'=> $allowCredentials ? 'true' : 'false'
            ];
            foreach ($headers as $name => $value) {
                if(!$response->hasHeader($name)){
                    $response=$response->withHeader($name,$value);
                }
            }
            if ($isPreflight && !$response->hasHeader('Access-Control-Exposed-Headers')) {
                $response = $response->withHeader('Access-Control-Exposed-Headers', implode(',', $exposedHeaders));
            }
            return $response;
        };
    }
}

然后你只需要在你的应用程序中添加你的中间件(全局或个别路由):

// Create Error Handler
$errorHandler = new ErrorHandler($app->getCallableResolver(), $$app->getResponseFactory());
// handle CORS requests
$corsFactory = new CorsMiddlewareFactory($errorHandler, false, true, true);
// allow all by default, YOLO !
$app->add($corsFactory->create(['*'], true, ['*'], ['*']));
// can be overriden on invidual routes as well
$app->get("/my/route",function($request,$response){
    return $response;
})->add($corsFactory->create(['http://google.fr'], false, [], []));