关于 CORS 的一个问题,大家怎么看

2020-09-02 17:28:15 +08:00
 mitu9527

昨天先去了解了下同源策略,然后又去看了一些和 CORS 相关的文章,几乎所有的文章都告诉我只要在响应头中加入几个 Access-Control-* 头部就可以了。比如像这样:

header('Access-Control-Allow-Origin:http://www.startphp.cn');
header('Access-Control-Allow-Methods:POST');
header('Access-Control-Allow-Headers:x-requested-with, content-type'); 

这段代码是我从别的地方复制过来的,不同文章的具体代码不一样,但都是差不多的,只不过多几个头部或者某个头部的值不一样罢了,没什么本质区别。

服务端这样做确实可以通知客户端,让其拒绝服务端返回的响应,但是服务端关于 CORS 的全部工作就这些么?我觉得不是!至少还应该加一些处理,起码应该在适当的时候结束程序的执行,如果服务端都准备告诉客户端拒绝自己的响应了,那发完响应头部,就没必要再执行后续的业务逻辑了,不是么?反过来说,所有的逻辑都执行完了,然后你再加几个头部去告诉客户端把自己拒绝掉,这不是多此一举么?

我知道客户端只要不发送 Origin 头部,就可以绕过服务端的 CORS 处理;我也知道要想提升安全性,必须得做鉴权才行,但就是觉得服务端的 CORS 处理这块还是应该稍微完善一点,不应该只是加几个头部就草草的结束了才对。如果你觉得我的这种想法是有问题且多余的,欢迎留言指正!

最后我列出我的一点代码,代码并没有做过测试,也更没在开发环境和生产环境中使用过,只是想说明一下我觉得完善的 CORS 处理大概应该是什么样子。具体的逻辑主要参考的是阮一峰的这篇文章:《跨域资源共享 CORS 详解》 。有兴趣的伙伴可以去看看。

<?php

// CORS 配置
$config = [
    'allow_origin' => ["http://b.example.com", "http://c.example.com"],
    'allow_method' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    'allow_headers' => ['X-Custom-Header', 'Accept'],
    'expose_headers' => ['Content-Range'],
    'allow_credentials' => true,
    'max_age' => 86400,
];

// CORS 预检请求
if (isCorsPreflightRequest()) {
    // 检查预检请求
    if (checkCorsPreflightRequest($config)) {
        // 通过
        addCorsPreflightRequestHeaders($config);
    } else {
        // 未通过,什么也不做
    }
    // 退出,不进入业务逻辑部分
    exit;
}

// CORS 简单请求
if (isCorsSimpleRequest()) {
    // 检查简单请求
    if (checkCorsSimpleRequest($config)) {
        // 通过
        addCorsSimpleRequestHeaders($config);
    } else {
        // 没通过,退出,不进入业务逻辑部分
        exit;
    }
}

function isCorsPreflightRequest(): bool
{
    return isset($_SERVER['HTTP_ORIGIN'], $_SERVER['REQUEST_METHOD']) &&
        'OPTIONS' === strtoupper($_SERVER['REQUEST_METHOD']);
}

function checkCorsPreflightRequest(array $config): bool
{
    return checkCorsOrigin($config) && checkCorsRequestMethod($config) && checkCorsRequestHeaders($config);
}

function checkCorsOrigin(array $config): bool
{
    return '*' === $config['allow_origin'] || in_array($_SERVER['HTTP_ORIGIN'], $config['allow_origin'], true);
}

function checkCorsRequestMethod(array $config): bool
{
    if (
        isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']) &&
        !in_array(strtoupper($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD']), $config['allow_method'], true)
    ) {
        return false;
    }
    return true;
}

function checkCorsRequestHeaders(array $config): bool
{
    if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) {
        $requestHeaders = explode(',', $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']);
        $allowHeaders = array_map(fn($header) => strtoupper($header), $config['allow_headers']);
        foreach ($requestHeaders as $requestHeader) {
            $requestHeader = strtoupper(trim($requestHeader));
            if (!in_array($requestHeader, $allowHeaders, true)) {
                return false;
            }
        }
    }
    return true;
}

function addCorsPreflightRequestHeaders(array $config): void
{
    if ('*' === $config['allow_origin']) {
        header('Access-Control-Allow-Origin: *');
    } else {
        header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
    }
    header('Access-Control-Allow-Methods: ' . implode(',', $config['allow_method']));
    header('Access-Control-Allow-Headers: ' . implode(',', $config['allow_headers']));
    header('Access-Control-Allow-Credentials: ' . ($config['allow_credentials'] ? 'true' : 'false'));
    header('Access-Control-Max-Age: ' . $config['max_age']);
}

function isCorsSimpleRequest(): bool
{
    return isset($_SERVER['HTTP_ORIGIN']);
}

function checkCorsSimpleRequest(array $config): bool
{
    return checkCorsOrigin($config);
}

function addCorsSimpleRequestHeaders(array $config, array $exposeHeaders = []): void
{
    if ('*' === $config['allow_origin']) {
        header('Access-Control-Allow-Origin: *');
    } else {
        header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
    }
    if (empty($exposeHeaders)) {
        header('Access-Control-Allow-Headers: ' . implode(',', $config['expose_headers']));
    } else {
        header('Access-Control-Allow-Headers: ' . implode(',', $exposeHeaders));
    }
    header('Access-Control-Allow-Credentials: ' . ($config['allow_credentials'] ? 'true' : 'false'));
}

// 假设下方是业务逻辑部分的代码
// 省略...

3384 次点击
所在节点    程序员
46 条回复
ssshooter
2020-09-02 17:38:19 +08:00
服务端真的没有什么可以做的...CORS 完全只是浏览器定下的拦截约定😂
mitu9527
2020-09-02 17:40:09 +08:00
@ssshooter 明白,不过我觉得在已经知道请求会被拒绝的情况下,就别再去执行后续的代码了,有些多余。
shintendo
2020-09-02 17:43:00 +08:00
浏览器可以关闭同源策略
mitu9527
2020-09-02 17:52:53 +08:00
@shintendo 我知道,但是我想说的不是这个。
asiufasd
2020-09-02 18:16:23 +08:00
我觉得你可以带着以下问题再次仔细阅读你列出的参考文章,我认为你没有从阅读的文章中得出正确的结论。
1. “服务端这样做确实可以通知客户端,让其拒绝服务端返回的响应” ;跨域请求失败的响应头到底是什么样的?会带“Access-Control-*”吗?
2. “如果服务端都准备告诉客户端拒绝自己的响应了,那发完响应头部,就没必要再执行后续的业务逻辑了” ;返回“Access-Control-* ”相应头是对应的什么请求,会执行具体的逻辑吗?(提示,options )
mitu9527
2020-09-02 18:22:28 +08:00
@asiufasd 我觉得我看明白了。你可以说完么,不要话说一半哈。
lalalaqwer
2020-09-02 18:40:30 +08:00
同源策略是浏览器的规则
事实上存在跨域情况时,浏览器会先发送 option 预检请求,只有得到了服务器肯定的答复才会发送真正的业务请求,你看到跨域被浏览器阻止的时候,服务端根本就没有接受到业务请求,不存在服务端浪费资源执行业务逻辑的情况
cnscorpions
2020-09-02 18:42:31 +08:00
@lalalaqwer 楼上解释不错
mitu9527
2020-09-02 18:44:45 +08:00
@lalalaqwer 那你的 OPTIONS 预检请求写在哪里?不都是 ajax 发送的地址么,预检请求后面没有业务逻辑代码么?你要是不去主动结束,不就会执行了么?
asiufasd
2020-09-02 18:48:35 +08:00
@mitu9527 本意是想让您自己再看一遍的,回答了那些问题我觉得就可以回答您的问题了。
我帮您整理好您看一下。
以下引用自您发出来的文章,
1. “如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段(详见下文),就知道出错了,从而抛出一个错误”,“如果服务器否定了"预检"请求,会返回一个正常的 HTTP 回应,但是没有任何 CORS 相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误”;
所以无论是简单请求还是非简单请求,跨域检测失败的响应头里都不会包含 CORS 相关的头信息字段,所以让客户端拒绝服务端,不需要特别返回响应头。
2. “非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求( preflight )。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。”,预检的响应头会包含你最开始列出的一些 Access-Control-*头(如果后端允许请求的话);
这个请求是 options 请求,预检请求,并不会执行具体的逻辑,只有后端预检通过后,客户端才会发出正式的 XMLHttpRequest 请求。不存在"所有的逻辑都执行完了,然后你再加几个头部去告诉客户端把自己拒绝掉 "的问题、
mitu9527
2020-09-02 18:49:39 +08:00
@asiufasd @lalalaqwer @cnscorpions 我不知道你们是不是 PHP 开发人员,如果是的话,你们把我代码里面的两个 exit; 语句去掉,看看会怎么样。
mitu9527
2020-09-02 18:53:58 +08:00
@asiufasd 我已经说了,我看明白了,你回复的内容我都看了,而且我都明白,但是我感觉你并不清楚我在说什么。就问你一个问题吧,预检请求处理和简单请求处理,不都是在某个业务逻辑代码的前面写的么,如果程序不去 exit,会执行不到业务逻辑代码?
111111111111
2020-09-02 18:55:36 +08:00
@mitu9527 把 exit 去掉,就算导致业务代码执行,和 php 没关系,也和 CORS 没关系,是你代码风格的问题,完全可以把业务代码写到 if 里面
asiufasd
2020-09-02 18:57:14 +08:00
@mitu9527 不要用自己错误的代码去证明跨域方案有问题
ck65
2020-09-02 18:58:40 +08:00
服务端的正确实现:OPTIONS 方法进来的请求不调用业务 handler,因此 preflight 请求不可能调用任何业务资源。
mitu9527
2020-09-02 18:59:45 +08:00
@111111111111 我没说和 PHP 有啥关系,我是在说,CORS 处理已经完成了,如果都已经知道拒绝了,何必再去执行剩余的代码,网上那种只加 header 的,你敢说没执行后面的代码?你说加 if,这个 if 该执行么?
anUglyDog
2020-09-02 19:00:48 +08:00
@mitu9527
1. 同源策略就是让开发者受限于浏览器安全约束,如果抛开 web 端,其实随便来个能发请求的终端都可以模拟浏览器,那你在跨域时不做服务端处理还有什么意义吗?
2. 跨域不成功意味着用户根本用不到这段代码,因为开发者没做完这个产品,所以不能到用户手上。
3. options 预检请求不通过,浏览器就不会发送 cookie,服务器验证不了用户,那还处理啥?
also24
2020-09-02 19:03:57 +08:00
尝试着帮楼主解释一下。

首先是大前提,楼主的思考方向,是在 『无框架协助』情况下,处理后端请求。
划重点:无框架

第 1 条,关于 OPTIONS 请求的处理:
由于无框架,所以不管你的请求 method 是 GET POST 还是 OPTIONS,都会进入整个请求处理逻辑。
自然的,OPTIONS 请求也直接进来了,如果你不将它单独处理,那确实可能造成问题。

第 2 条,关于浏览器对跨域请求的拦截:
@lalalaqwer #7 你的理解是有一定问题的,浏览器对于跨域请求,未必是在『发送请求』阶段拦截。
你可以找个 Chrome 浏览器测试一下,Chrome 只是拦截了返回内容,服务器上你是可以收到这条请求的。


由于以上两条,在『无框架』的场景下,确实有可能出现后端对跨域请求处理不合理,导致浏览器的 CORS 机制没有正确生效的情况。


但是为什么很多人都不认可楼主的结论呢?
第 1 条是因为『无框架』的场景实在是太少啦,现在基本上大事小事都会起框架,而框架往往都会对 OPTIONS 请求做自动处理。
第 2 条,则真的是很多人没有注意过,因为各类文档上往往只会提到 『浏览器拦截跨域请求』,却没有详细的说明是在哪一个阶段拦截的。
mitu9527
2020-09-02 19:03:57 +08:00
@ck65 OPTIONS 请求默认情况下就是发到目标地址,我知道当然可以放在一个专用的页面处理,但那是另一说。那简单请求呢?如果 Origin 是跨源的,服务端不该 exit 一下么?
Jirajine
2020-09-02 19:04:12 +08:00
这是你代码的问题,没检验请求方法就执行业务逻辑。
一般来说 web 框架就把这些处理了,option 请求根据路由根本匹配不到对应的 handler,自然也就不会触发业务逻辑。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/703603

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX