Access-Control-Allow-Origin 通配符子域名、端口和协议

467
我正在尝试为所有子域、端口和协议启用CORS。 例如,我想能够从"http://sub.mywebsite.example:8080/"对"https://www.mywebsite.example/"运行XHR请求*。 通常,我想启用与以下匹配的(且仅限于)来源的请求: "//*.mywebsite.example:*/*"
15个回答

365

9
当从 file:// 方案发起 CORS 请求时,可以使用“null”作为响应以回应“null”来源。 - monsur
291
CORS规范不支持 OP 的“确切”用例,这是极度短视的。 - aroth
12
@aroth: 实际上规范允许实现使用他们想要的任何匹配语法; 对于一个实现不支持这种用例只会非常短视。换句话说,您指定给服务器的并不是ACA0值,后者只是协议细节。我的假设是存在一种安全方案要求或受益于回显来源,但一个天真的实现只会简单地说“好”或不支持。 - tne
6
2015年的更新:此答案应视为有害的,因为它不完整,可能会导致缓存问题。请参见我的下面的答案,以获取适当的实现(针对Apache)和解释:https://dev59.com/kmYr5IYBdhLWcg3wB16h#27990162。此外,正如@aroth指出的那样,规范实际上确实允许OP的确切用例:http://www.w3.org/TR/cors/#resource-implementation。正如本答案所指出的,这取决于服务器的实现。可以在三行代码中完成,就像上面提到的答案一样。 - Noyo
46
@Noyo - 我会澄清我的原意。CORS规范没有严格要求所有实现CORS的服务器必须为OP的确切用例提供自动内置支持,这是非常短视的。让每个用户使用自定义PHP代码、重写规则或其他方式来构建自己的shim是一种导致碎片化、错误和灾难的做法。服务器开发人员应该知道得更好;如果他们不知道,CORS规范应该强制要求他们这样做。 - aroth
显示剩余12条评论

273

参考 DaveRandom 的 答案,我也尝试着实验了一下并发现了一个更简单的 Apache 解决方案,它能够动态地将 Access-Control-Allow-Origin 设置为当前特定的协议 + 域名 + 端口,而无需使用任何重写规则:

SetEnvIf Origin ^(https?://.+\.mywebsite\.example(?::\d{1,5})?)$   CORS_ALLOW_ORIGIN=$1
Header append Access-Control-Allow-Origin  %{CORS_ALLOW_ORIGIN}e   env=CORS_ALLOW_ORIGIN
Header merge  Vary "Origin"

就这样了。

那些想要在父域名(例如mywebsite.example)以及其所有子域名上启用CORS的人只需将第一行中的正则表达式替换为以下内容:

^(https?://(?:.+\.)?mywebsite\.example(?::\d{1,5})?)$

注意:为了符合规范要求和正确的缓存行为,始终为启用CORS的资源添加 Vary: Origin 响应头,即使是非CORS请求和来自未被允许的来源(请参见示例原因)。


2
我们之前几乎有这个(不是Vary Origin),当访问者在多个子域之间跳转并使用相同的字体时,会出现糟糕的行为。 字体和access-control-origin标头也被缓存。 我已对此进行了一点更改:如果请求来自我们允许的域之一,则使用“Access-Control-Allow-Origin *”。也许这个问题可以通过先前没有的“Vary Origin”解决...现在也添加了它。 - Erik Melkersson
1
e是什么意思?从%{CORS_ALLOW_ORIGIN}e这一行中? - Nathan
2
无法在主域名'mywebsite.com'上运行。 - biology.info
1
@pgmann,在这种情况下没有必要转义 //,因为 Apache 配置文件不使用斜杠分隔的正则表达式。Regexr 抱怨是因为在那种情况下,斜杠具有作为定界符的特殊含义。 - Noyo
13
你应该把这段代码放在哪里?是在.htaccess文件中还是在Apache虚拟主机配置文件中? - Glen
显示剩余14条评论

66
请使用 @Noyo的解决方案,而不是这个。它更简单、更清晰,并在负载下可能更加高效。
我对这个问题进行了一些测试并得出了这个可重复使用的.htaccess(或httpd.conf)解决方案,适用于Apache:
<IfModule mod_rewrite.c>
<IfModule mod_headers.c>
    # Define the root domain that is allowed
    SetEnvIf Origin .+ ACCESS_CONTROL_ROOT=yourdomain.example

    # Check that the Origin: matches the defined root domain and capture it in
    # an environment var if it does
    RewriteEngine On
    RewriteCond %{ENV:ACCESS_CONTROL_ROOT} !=""
    RewriteCond %{ENV:ACCESS_CONTROL_ORIGIN} =""
    RewriteCond %{ENV:ACCESS_CONTROL_ROOT}&%{HTTP:Origin} ^([^&]+)&(https?://(?:.+?\.)?\1(?::\d{1,5})?)$
    RewriteRule .* - [E=ACCESS_CONTROL_ORIGIN:%2]

    # Set the response header to the captured value if there was a match
    Header set Access-Control-Allow-Origin %{ACCESS_CONTROL_ORIGIN}e env=ACCESS_CONTROL_ORIGIN
</IfModule>
</IfModule>

只需在块的顶部设置ACCESS_CONTROL_ROOT变量为您的根域名,如果匹配您的域名,则它将在Access-Control-Allow-Origin:响应头值中向客户端回显 Origin:请求头值。

请注意,您可以使用sub.mydomain.example作为ACCESS_CONTROL_ROOT,它将限制来源为sub.mydomain.example*.sub.mydomain.example(即它不必是域根)。可以通过修改正则表达式的URI匹配部分来控制允许变化的元素(协议、端口)。


51

我回答这个问题是因为被接受的答案无法实现以下功能:

  1. 正则表达式分组会造成性能损失,而且它并不是必要的。
  2. 不能匹配主域名,只能用于子域名。

例如:它不会为http://mywebsite.example发送CORS标头,但可用于http://somedomain.mywebsite.example/

SetEnvIf Origin "http(s)?://(.+\.)?mywebsite\.com(:\d{1,5})?$" CORS=$0

Header set Access-Control-Allow-Origin "%{CORS}e" env=CORS
Header merge  Vary "Origin"

为了启用您的网站,您只需在上面的 Apache 配置中将 mywebsite.example 替换为您自己的网站。

要允许多个站点:

SetEnvIf Origin "http(s)?://(.+\.)?(othersite\.com|mywebsite\.example)(:\d{1,5})?$" CORS=$0

部署后验证:

更改后,以下curl响应应该具有“Access-Control-Allow-Origin”头。

curl -X GET -H "Origin: http://site1.example" --verbose http://site2.example/query

我尝试了在 iFrame 中使用 CORS 头绕过方法来获取一些内容...但它没有起作用。我猜我得使用 ajax 来获取这些内容。 - MeSo2
1
值得一提的是:本答案涉及到 Apache HTTP 服务器的 .htaccess 文件。 - OfirD

17

我需要一个仅使用PHP的解决方案,以防万一其他人也需要。它接受允许的输入字符串,例如“*.example.com”,如果输入匹配,则返回请求头服务器名称。

function getCORSHeaderOrigin($allowed, $input)
{
    if ($allowed == '*') {
        return '*';
    }

    $allowed = preg_quote($allowed, '/');

    if (($wildcardPos = strpos($allowed, '*')) !== false) {
        $allowed = str_replace('*', '(.*)', $allowed);
    }

    $regexp = '/^' . $allowed . '$/';

    if (!preg_match($regexp, $input, $matches)) {
        return 'none';
    }

    return $input;
}

这里是一个phpunit数据提供程序的测试用例:

//    <description>                            <allowed>          <input>                   <expected>
array('Allow Subdomain',                       'www.example.com', 'www.example.com',        'www.example.com'),
array('Disallow wrong Subdomain',              'www.example.com', 'ws.example.com',         'none'),
array('Allow All',                             '*',               'ws.example.com',         '*'),
array('Allow Subdomain Wildcard',              '*.example.com',   'ws.example.com',         'ws.example.com'),
array('Disallow Wrong Subdomain no Wildcard',  '*.example.com',   'example.com',            'none'),
array('Allow Double Subdomain for Wildcard',   '*.example.com',   'a.b.example.com',        'a.b.example.com'),
array('Don\'t fall for incorrect position',    '*.example.com',   'a.example.com.evil.com', 'none'),
array('Allow Subdomain in the middle',         'a.*.example.com', 'a.bc.example.com',       'a.bc.example.com'),
array('Disallow wrong Subdomain',              'a.*.example.com', 'b.bc.example.com',       'none'),
array('Correctly handle dots in allowed',      'example.com',     'exampleXcom',            'none'),

1
+1,编辑使用preg_quote(),因为这是正确的方法(即使“.”是DNS名称中唯一有效的正则表达式元字符,preg_quote()更能描述预期的操作)。 - DaveRandom
1
需要澄清的是,根据规范,“none”不是标题的语义有效值(或者至少不会执行它所暗示的操作)。因此,在这种情况下,return null;可能更有意义,并且在这种情况下不应向客户端发送任何标题,因此调用者应该进行检查。 - DaveRandom
2
preg_quote()函数会对星号进行转义,因此例如str_replace()的操作会导致出现孤立的反斜杠符号 ""。 - Christoffer Bubach
1
这非常有用,我在CORS问题上花了很多时间,直到意识到我的站点在ajax中有“www”,但在永久链接结构中没有 - 您的解决方案帮助我理解了问题所在,并为我解决了它。 - SolaceBeforeDawn

4

.htaccess 中设置 Access-Control-Allow-Origin,只有以下内容有效:

SetEnvIf Origin "http(s)?://(.+\.)?domain\.example(:\d{1,5})?$" CRS=$0
Header always set Access-Control-Allow-Origin "%{CRS}e" env=CRS

我尝试了几个其他建议的关键字Header append, Header set, 但这些都没有像Stack Overflow上许多答案中建议的那样工作,不过我不知道这些关键字是否已经过时或者对nginx无效。

以下是我的完整解决方案:

SetEnvIf Origin "http(s)?://(.+\.)?domain\.example(:\d{1,5})?$" CRS=$0
Header always set Access-Control-Allow-Origin "%{CRS}e" env=CRS
Header merge Vary "Origin"

Header always set Access-Control-Allow-Methods "GET, POST"
Header always set Access-Control-Allow-Headers: *

# Cached for a day
Header always set Access-Control-Max-Age: 86400

RewriteEngine On

# Respond with 200OK for OPTIONS
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]

4

2

看起来原来的答案是针对 Apache 2.4 之前的版本的,对我没有用。以下是我所做的更改,使其在 2.4 版本中正常工作。这将适用于任何深度的 yourcompany.example 子域。

SetEnvIf Host ^((?:.+\.)*yourcompany\.example?)$    CORS_ALLOW_ORIGIN=$1
Header append Access-Control-Allow-Origin  %{REQUEST_SCHEME}e://%{CORS_ALLOW_ORIGIN}e    env=CORS_ALLOW_ORIGIN
Header merge  Vary "Origin"

2
我们在静态的“无cookie”域上读取来自“cookie域”(www.domain.example)的字体时,遇到了与Font Awesome类似的问题,而这篇文章是我们的救星。请参见此处:如何解决“缺少跨源资源共享(CORS)响应头”Webfont问题? 对于复制/粘贴类型(并为一些贡献者致以敬意),我从所有的贡献中拼凑出了以下内容,并将其添加到站点根目录下的.htaccess文件的顶部:
<IfModule mod_headers.c>
 <IfModule mod_rewrite.c>
    SetEnvIf Origin "http(s)?://(.+\.)?(othersite\.example|mywebsite\.example)(:\d{1,5})?$" CORS=$0
    Header set Access-Control-Allow-Origin "%{CORS}e" env=CORS
    Header merge  Vary "Origin"
 </IfModule>
</IfModule>

超级安全,超级优雅。喜欢它:您无需将服务器带宽开放给资源窃贼/热链接者。


0

对于Spring,您应该使用allowedOriginPatterns,它正好符合您的要求。

与setAllowedOrigins(java.util.List<java.lang.String>)相比,它支持更灵活的起源模式,其中在主机名中任何位置都可以使用“*”,并且还支持端口列表。 例如:

https://*.domain1.com -- domains ending with domain1.com
https://*.domain1.com:[8080,8081] -- domains ending with domain1.com on port 8080 or port 8081
https://*.domain1.com:[*] -- domains ending with domain1.com on any port, including the default port 

与 allowedOrigins 相反,该属性只支持 "",无法与 allowCredentials 一起使用。当匹配到 allowedOriginPattern 时,Access-Control-Allow-Origin 响应头会被设置为匹配的源,而不是 "" 或匹配模式。因此,可以将 allowedOriginPatterns 与 setAllowCredentials(java.lang.Boolean) 结合使用,且将 setAllowCredentials(java.lang.Boolean) 设置为 true。

例如:
@Bean
public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            List<String> allowedOrigins = List.of("https://*.example.com", "http://*.example2.com[80,8080]");
            registry.addMapping("/**").allowedOriginPatterns(allowedOrigins.toArray(new String[0]));

        }
    };
}

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