PHP/Apache2响应中缺少CORS头信息

11
我的公司正在开发一个Zend Expressive项目,准备上线,但在我们的测试环境中,CORS预检请求似乎缺少响应头。这在我们的开发环境中不会发生。我们在管道中使用CorsMiddleware,但它看起来不是罪魁祸首。
问题:
在运行时,中间件检测到传入的预检请求,并将回复如下响应:
HTTP/1.1 200 OK
Date: Mon, 20 Aug 2018 15:09:03 GMT
Server: Apache
X-Powered-By: PHP/7.1.19
Access-Control-Allow-Origin: https://example.com
Vary: Origin
Access-Control-Allow-Headers: content-type
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html; charset=UTF-8

好的,这只适用于我们的开发服务器和php内置的Web服务器。尽管请求完全相同,除了主机名之外,但响应与我们的暂存服务器不同:

HTTP/1.1 200 OK
Date: Mon, 20 Aug 2018 15:11:29 GMT
Server: Apache
Keep-Alive: timeout=5, max=100
Cache-Control: max-age=0, no-cache
Content-Length: 0
Content-Type: text/html; charset=UTF-8

我们所尝试的

调查中间件

我们已经验证了CorsMiddleware运行得非常好,并且实际上设置了所需的标头。当我们修改CorsMiddleware的响应代码并将其设置为202而不是200时,我们现在确实获得了我们正在寻找的标头。将响应代码更改回200会使标头再次消失。

手动设置标头

使用以下示例:

header('Access-Control-Allow-Origin: https://example.com');
header('Access-Control-Allow-Headers: content-type');
header('Vary: Origin');
exit(0);

在将响应代码修改为204或任何不是200的值之前,它的行为相同。

查看正文

响应正文为空,不应包含任何内容,但是当我们向响应正文添加内容时,标头看起来好像没有问题。

因此,如果我添加正文内容,则存在标头。没有正文内容?没有CORS标头。这是Apache中的某些设置吗?我是否缺少PHP中的某些配置?我有什么遗漏吗?

进一步细节

所有请求均已使用httpie、Postman、curl和PhpStorm的http客户端进行测试。

以下是httpie示例:

http -v OPTIONS https://staging.****.com \
    'access-control-request-method:POST' \
    'origin:https://example.com' \
    'access-control-request-headers:content-type'

以下是curl示例:

curl "https://staging.****.com" \
--request OPTIONS \
--include \
--header "access-control-request-method: POST" \
--header "origin: https://example.com" \
--header "access-control-request-headers: content-type"

pipeline.php中的Cors配置(仅用于测试的通配符):

$app->pipe(new CorsMiddleware([
    "origin"         => [
        "*",
    ],
    "headers.allow"  => ['Content-Type'],
    "headers.expose" => [],
    "credentials"    => false,
    "cache"          => 0,

    // Get list of allowed methods from matched route or provide empty array.
    'methods' => function (ServerRequestInterface $request) {
        $result = $request->getAttribute(RouteResult::class);
        /** @var \Zend\Expressive\Router\Route $route */
        $route = $result->getMatchedRoute();

        return $route ? $route->getAllowedMethods() : [];
    },

    // Respond with a json response containing the error message when the CORS check fails.
    'error'   => function (
        ServerRequest $request,
        Response $response,
        $arguments
    ) {
        $data['status']  = 'error';
        $data['message'] = $arguments['message'];

        return $response->withHeader('Content-Type', 'application/json')
                        ->getBody()->write(json_encode($data));
    },
]);

预发布环境:

OS: Debian 9.5 server
Webserver: Apache/2.4.25 (Debian) (built: 2018-06-02T08:01:13)
PHP: PHP 7.1.20-1+0~20180725103315.2+stretch~1.gbpd5b650 (cli) (built: Jul 25 2018 10:33:20) ( NTS )

在staging上配置Apache2虚拟主机:

<IfModule mod_ssl.c>
<VirtualHost ****:443>
        ServerName staging.****.com
        DocumentRoot /var/www/com.****.staging/public

        ErrorLog /var/log/apache2/com.****.staging.error.log
        CustomLog /var/log/apache2/com.****.staging.access.log combined
        <Directory /var/www/com.****.staging>
                Options +SymLinksIfOwnerMatch
                AllowOverride All
                Order allow,deny
                allow from all
        </Directory>
SSLCertificateFile /etc/letsencrypt/live/staging.****.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/staging.****.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

在开发中使用的Apache2虚拟主机:

<VirtualHost *:443>
        ServerName      php71.****.com
        ServerAdmin     dev@****.com
        DocumentRoot    /var/www/

        <Directory /var/www/>
                Options Indexes FollowSymlinks
                AllowOverride All
                Require all granted
        </Directory>

        ErrorLog        ${APACHE_LOG_DIR}/error.ssl.log
        CustomLog       ${APACHE_LOG_DIR}/access.ssl.log combined

        SSLEngine On
        SSLCertificateFile /etc/ssl/certs/****.crt
        SSLCertificateKeyFile /etc/ssl/certs/****.key
</VirtualHost>

对于指责Cloudflare的所有人:

请尝试使用httpie直接访问此链接。该链接未使用Cloudflare:

http -v OPTIONS http://37.97.135.33/cors.php \
    'access-control-request-method:POST' \
    'origin:https://example.com' \
    'access-control-request-headers:content-type'

在浏览器中检查源代码:http://37.97.135.33/cors.php?source=1


你能展示两个环境的虚拟主机吗?Apache 能否覆盖标头? - Jim Wright
2
我会查看Apache的mod_headers设置,以了解暂存服务器的情况。它很可能被配置为剥离“不安全”的头部信息,通常是像x-powered-by这样的内容,从你的结果来看似乎是这种情况。如果暂存服务器在代理后面运行,也有可能配置上游代理执行相同的操作。 - Crisp
1
我相信我注意到你的开发响应显示php为7.1.19,但是暂存似乎在7.1.20。你有一种实用/可行的方法来将暂存设置为7.1.19(如开发环境)以确保7.1.20中的某些内容不会引起问题吗? - Félix Adriyel Gagnon-Grenier
2
你看过这个吗?https://bz.apache.org/bugzilla/show_bug.cgi?id=51223 - delboy1978uk
@delboy1978uk 非常有趣!但如果我理解正确,这只会发生在304状态码上? - halfpastfour.am
显示剩余8条评论
3个回答

2
根据您的评论和所阅读的内容,你的“生产”服务器似乎在一个代理后面,更确切地说是CloudFlare。关于你工作环境的详细信息已经提供,但对于不能工作的生产环境却没有任何说明。您的设置似乎正确,如果它在没有代理的开发环境中工作,那就意味着代理正在修改头文件。关于这个与CloudFlare有关的快速搜索已经给出足够的提示,表明CloudFlare可能会导致你的问题。我强烈建议你在CloudFlare中启用“开发模式”,这样它将绕过缓存,你可以看到所有来自或发送到源服务器的东西。以下文章应该能帮助您理解和解决您的问题:https://support.cloudflare.com/hc/en-us/articles/203063414-Why-can-t-I-see-my-CORS-headers-
更新:似乎您的问题来自于Apache Mod Pagespeed,关闭它后您的头文件始终存在。仍不清楚为什么该mod会删除您的头文件,但这是另一个问题和时间的问题。

谢谢您的回复,但是您显然误解了。生产环境在Cloudflare后面。而预发布环境并没有使用任何代理。 - halfpastfour.am
那么你的意思是 STAGING 工作正常,而 PRODUCTION 不行,对吗? - Mecanik
我打错字了...我想说的是"production"而不是"staging",在我说"从这里我读到的所有内容,包括你的评论,似乎你的"production"服务器在一个代理后面,更确切地说是CloudFlare。" - Mecanik
直接链接未使用Cloudflare。 - halfpastfour.am
让我们在聊天中继续这个讨论 - halfpastfour.am
显示剩余3条评论

1
您的配置清楚地表明标题确实已经生成,因此不是代码或中间件的问题。
我认为标题被某些东西删除了 - 请检查Apache的mod_headers和配置,以防出现流氓unset指令。
另一个可能性较小的可能性是,您通过某种负载均衡器或代理查看暂存服务器,该负载均衡器或代理重写标题并将CORS留出(要验证这一点,您可能需要拦截Apache的传出流量)。
我自己也犯过这两个错误。

1
合理的可能解释是,mod_headers/其他模块只删除状态码为200的响应头,而允许202/204通过。如果是这种情况,我很想知道原因。 - Tobias K.
1
感谢您的输入。我刚刚检查了一下,mod_headers甚至没有启用。生产环境在Cloudflare后面,但是暂存环境直接提供服务,没有任何负载均衡器或代理。 - halfpastfour.am
2
你能否通过一个简单的PHP页面来检查这些头部信息并退出,中间没有任何其他内容?理想情况下,你可以使用netcat临时替换Apache,使用一个虚假的“Web服务器”。 - LSerni
@LSerni 我已经做了。请查看原帖,我已添加了一个直接链接到正在发生的情况,包括查看源代码的链接。 - halfpastfour.am

0
请确保您在Zend Expressive中拥有正确的配置。例如,下面的代码将允许任何调用域访问CORS。
use Psr\Http\Message\ServerRequestInterface;
use Tuupola\Middleware\CorsMiddleware;
use Zend\Expressive\Router\RouteResult;

$app->pipe(new CorsMiddleware([
    "origin" => ["*"],
    "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"]
}
]));

你能分享一下Zend Expressive的CORS配置代码吗?我相信你有一个配置问题导致了这个问题。 - Rinsad Ahmed
你有在 public 文件夹内的 htaccess 文件做过任何更改吗? - Rinsad Ahmed
“.htaccess”文件未被修改,否则问题可能会在两个环境中都出现。CORS配置已添加到OP中。 - halfpastfour.am
为什么不将方法更改为 "methods" => ["GET", "POST", "PUT", "PATCH", "DELETE"],然后发送您的反馈? - Rinsad Ahmed
检查一下原帖,我在那里提供了一个简洁但可工作的示例,并附上了我的反馈意见。 - halfpastfour.am
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接