Fork me on GitHub

关于跨域,看这一篇文章就够了

转载自网络,如有侵权请联系删除

背景

前后端分离的模式,前端应用通过跨域的方式直连后端机器。

跨域请求的类型

简单请求

  • request method 属于 GET, HEAD, POST 三种简单类型之一。

  • headers 只包含以下几种 (大小写敏感):

    • Cache-Control
    • Content-Language
    • Content-Type
    • Expires
    • Last-Modified
    • Pragma

非简单请求 (preflight)

  • 不符合以上简单请求规则的, 浏览器在发送请求前,会先向服务器发送一个 OPTIONS 请求, (称为 preflight), 只有当 preflight 收到 200/204 的回复时,才会发送真实的请求。

响应非简单请求

有些后端应用可能只有 GET 和 POST 两种请求方式,是否还需要能响应 preflight 请求呢?

答案是需要的,比如你的前端应用在多环境部署,可能需要在请求头中添加 X-Antcloud-Tenant header,或者是某些请求库,默认会添加 x-request-lib 这一请求头。因此,原来符合简单规则的请求,也有极大的可能会变成 preflight 请求。所以后端需要做的是,在收到 OPTIONS 请求时,返回 200/204 的状态码。

跨域相关的 headers

Request Headers:

  • Access-Control-Request-Headers

    • 浏览器自动添加,在 preflight 请求中,浏览器会加上真实请求中出现的 headers,后端可以根据该字段决定 Access-Control-Allow-Headers 的值。

Response Headers:

  • Access-Control-Allow-Origin

    • 允许跨域的的域名,不推荐使用通配符(*), 因为在使用通配符时,会跳过 Access-Control-Allow-Credentials 的配置,直接过滤掉请求携带的 cookie. 可能导致后端接口鉴权失败。
  • Access-Control-Allow-Credentials

    • 是否允许跨域请求携带 cookie,通常情况下,用户信息和登陆状态都储存在 cookie 中。如果不携带 cookie 则无法通过后端的接口鉴权,需要设置为 true.
  • Access-Control-Allow-Headers

    • 允许出现的非简单请求头。
  • Access-Control-Allow-Methods

    • 允许跨域的方法,如果没有特殊的安全考虑,可以针对 POST,GET,OPTIONS,DELETE,PUT,HEAD,PATCH 请求都放开,让所有符合 restful 规范的请求都能跨域。
  • Access-Control-Expose-Headers

    • 非必填,如果前端需要消费非简单请求。则需通过该配置指定额外允许前端获得的 headers。部分情况下,浏览器的返回请求中出现 provisonal header,也与此配置有关。
  • Access-Control-Max-Age

    • 非必填,preflight 请求的有效期。

通过 nginx 配置跨域

通过上面请求头字段的解释,我们可以通过 nginx 允许后端应用跨域。在后端项目的 conf/auto-conf/t-alipay-engine.conf 文件中,可以对后端路径增加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
location ^~ /api {
proxy_set_header Origin '';
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Headers $http_access_control_request_headers;
add_header Access-Control-Allow-Methods POST,GET,OPTIONS,DELETE,PUT,HEAD,PATCH;
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Expose-Headers $http_access_control_request_headers;

if ($request_method = 'OPTIONS') {
return 204;
}
if ($request_method != 'OPTIONS'){
proxy_pass [填入接口代理地址];
}
}

需要注意的是,目前 nginx 1.7.5 之前的版本,add_header 只对 2xx,3xx 的请求生效,5xx 的请求无法增加 header,仍会被浏览器跨域策略拦截,在 5xx 请求的 body 中包含的错误信息,前端无法获取到。

因此,通过这种策略配置的跨域,后端需要将错误信息(例如:参数错误、未登陆)包含在一个 200 的请求内返回。而不能通过 401、502 等 http code 返回,否则前端将无法获取接口出错时的内容。

通过 Java Filter 配置跨域

以下内容摘自 SOFA 官网,原链接

MVC 4.2.0 之前框架默认支持的方法是GET、HEAD、POST:相关代码,如果跨域方法不在这些方法列表中,可能报错,且MVC 4.2.0之前版本不支持修改CORS跨域相关配置,如果报错建议升级到4.2.2以上版本。

MVC 4.2.2 配置的Filter默认只会允许阿里域的域名访问,如果需要允许其他域名访问,可以在MERGE-INF/merge-smvc-scheme-default.xml文件中配置以下bean覆盖框架的默认bean:

1
2
3
4
5
6
7
8
<bean id="defaultCorsFilter" class="com.alipay.sofa.web.mvc.security.filter.DefaultCorsFilter">
<!-- 配置阿里域之外的白名单,只需要写url后缀即可 -->
<property name="corsHostWhiteList">
<list>
<value>baidu.com</value>
</list>
</property>
</bean>

不同 Java 技术栈通过 filter 配置的方式可能略有不同,以 HUANYU 为例,sofaboot 3.1.0:

1
2
3
4
5
<parent>
<groupId>com.alipay.sofa</groupId>
<artifactId>sofaboot-alipay-dependencies</artifactId>
<version>3.1.0</version>
</parent>

1)引入依赖的 JAR

1
2
3
4
5
<dependency>
<groupId>com.alipay.common</groupId>
<artifactId>alipay-common-security</artifactId>
<version>1.2.1</version>
</dependency>

2)在 merge-smvc-scheme-default.xml 文件中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<bean id="defaultCorsFilter" class="com.alipay.common.security.filter.DefaultCorsFilter">
<property name="whiteHostList">
<list>
<value><![CDATA[huanyu3.dev.alipay.net]]></value>
<value><![CDATA[huanyu2.dev.alipay.net]]></value>
<value><![CDATA[huanyu.dev.alipay.net]]></value>
<value><![CDATA[huanyu.test.alipay.net]]></value>
<value><![CDATA[paas.alipay.com]]></value>
<value><![CDATA[paas-pre.alipay.com]]></value>
<value><![CDATA[jz.alipay.com]]></value>
<value><![CDATA[jz-pre.alipay.com]]></value>
<value><![CDATA[mob-paas.alipay.com]]></value>
<value><![CDATA[mob-paas-pre.alipay.com]]></value>
</list>
</property>
<property name="customMethods" value="GET,POST"/>
<property name="customHeads" value="*"/>
<property name="customCredentials" value="true"/>
</bean>

手动配置跨域

如果上面的方法对你的技术栈不适用,同时 nginx 只能修改 200 请求的方式不能满足业务的需求,也可以通过 HttpServletResponse 手动添加上述跨域的 headers,在最终组装 http 返回结果前(如 Interceptor 中)调用,例如:

1
2
3
4
5
6
7
8
9
10
11
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HttpErrorResponseUtil {
public static void setResponeCorsHeader(HttpServletRequest request, HttpServletResponse response) {
response.addHeader("Access-Control-Allow-Credentials", "true");
response.addHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PUT,HEAD,PATCH");
response.addHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.addHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
}
}

FAQ

  • 如果明知会跨域,有没有办法禁用本地的安全策略,方便开发和调试?
1
open -n -a /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security

可以通过以上命令启动一个不安全的 chrome(Mac OS), 在本地开发时非常有用,此模式下所有请求都不会被跨域策略拦截,且不会出现 provisional header.

  • 跨域请求是否可以发起 redirect?

浏览器允许跨域的简单请求进行重定向,但是考虑到浏览器兼容性,不推荐进行多次的重定向。对于 preflight 请求,redirect 的地址不会被 follow.