浏览器跨域请求

在跨域情况下(源域名与目的域名不一样),浏览器可能会发起一个HTTP OPTIONS 的请求预检(preflighting)服务端是否支持CORS。如果服务端允许当前域名(源域名)CORS,则浏览器会再发起真正的请求。

跨域请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。只要同时满足以下两大条件,就属于简单请求。

  • 请求方法是以下三种方法之一:HEAD,GET,POST
  • HTTP的头信息不超出以下几种字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type(只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain)

凡是不同时满足上面两个条件,就属于非简单请求。

简单请求

对于简单请求,浏览器直接发出CORS请求。浏览器会自动在头信息(Request Headers)中,添加一个Origin 字段,来表明本次请求来自哪个域。

  • 如果Origin不在许可范围内,会报错:
1
No 'Access-Control-Allow-Origin' header is present on the requested resource.
  • 如果Origin指定的域名在许可范围内(必须是跨域的),Response Headers中会多出几个头信息字段:
1
2
3
Access-Control-Allow-Credentials:true //值为true表示允许发送cookie
Access-Control-Allow-Methods:GET, POST, OPTIONS
Access-Control-Allow-Origin:http://localhost:8080

这几个字端要么在服务器端直接返回,要么配置在nginx中,具体实现看下文

  • withCredentials属性

CORS默认不发送cookiehttp认证,如果要把Cookie发到服务器,就要指定Access-Control-Allow-Credentials:true;另外ajax中也要打开withCredentials属性

1
2
3
headers: {
withCredentials: true,
},

非简单请求

比如:请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

结果发现浏览器连续向同一地址请求了两次,而第一次请求什么值也没拿到

  • 第一次请求信息
1
2
3
4
5
6
7
8
9
10
General
Request URL:http:/mobile.test.ximalaya.com/v1/lesson
Request Method:OPTIONS
Status Code:200 OK
Response Headers
Access-Control-Request-Headers:content-type
Access-Control-Request-Method:GET, POST, OPTIONS
Access-Control-Allow-Origin:http://localhost:8080
Content-Length:0
  • 第二次请求信息
1
2
3
4
5
6
7
8
9
General
Request URL:http:/mobile.test.ximalaya.com/v1/lesson
Request Method:POST
Status Code:200 OK
Response Headers
Access-Control-Request-Headers:content-type
Access-Control-Request-Method:GET, POST, OPTIONS
Access-Control-Allow-Origin:http://localhost:8080

这是因为浏览器发现,这是一个非简单请求,就自动发出一个”预检”请求,要求服务器确认可以这样请求。”预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。”预检”请求之后,浏览器球会进行正常CORS请求。

案例

背景说明

前端H5通过ajax调用后端restful api,并且页面访问域名与api访问域名(mobile.test.ximalaya.com)不同,导致访问报错:

05C9331E-30FD-4685-AB61-02F308FC3695

出现这种情况的原因如下:

  • 本次ajax请求是“非简单请求”,所以请求前会发送一次预检请求(OPTIONS)
  • 服务器端后台接口没有允许OPTIONS请求,导致无法找到对应接口地址

原因分析

  1. 前端ajax请求代码
1
2
3
4
5
6
7
8
9
10
11
$.ajax({
url: 'http://mobile.test.ximalaya.com/weikemsg-web/v1/room/110001/discuss/send',
headers: {
'Content-Type':'application/json'
},
method: 'POST',
data: {id = 1},
success: function(data){
console.log('succes: '+data);
}
});

这里请求类型使用post,数据类型是json

  1. http请求信息

F003603E-2FE9-4009-BF3D-CAA5EE0201F9

request header信息中可以发现,实际请求类型是OPTIONS,注意:

  • Access-Control-Request-Method:在发出预检请求时带有这个头信息, 告诉服务器在实际请求时会使用的请求方式
  • Access-Control-Request-Headers:在发出预检请求时带有这个头信息, 告诉服务器在实际请求时会携带的自定义头信息. 如有多个, 可以用逗号分开.
  • Origin:表明发送请求或者预请求的域
  1. 失败分析
  • Status Code:401

这里的http返回状态码401(未授权),是因为服务端这边设置了登录验证过滤器,服务端返回结果如下:

F2AFC4E1-4CDE-4B6C-8237-A185565213D9

  • 跨域
1
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:9000' is therefore not allowed access

从控制台错误信息中,可以发现浏览器发送的OPTIONS预检请求不允许访问服务器,也就是不允许跨域访问

  1. 第一次尝试解决

Nginx添加如下配置:

1
2
3
4
5
6
location ^~ /weikemsg-web {
add_header 'Access-Control-Allow-Origin' "*"; // 允许所有域名访问
add_header 'Access-Control-Allow-Credentials' 'true'; // 允许携带Cookie
add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS'; // 支持访问方式
proxy_pass http://weikemsg-web/weikemsg-web;
}

同时用户先登录,保留Cookie,再次访问目的地址,结果如下:

01807CD6-DD4D-4F17-BF94-F5F75CE7E93E

结果没有任何变化,跨域访问没有成功,服务端还是没有拿到Cookie信息

  1. 第二次尝试解决

如果http请求需要带上Cookie,必须设置Access-Control-Allow-Credentialstrue,同时不能设置Access-Control-Allow-Origin*

重新配置Nginx:

1
2
3
4
5
6
location ^~ /weikemsg-web {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS';
proxy_pass http://weikemsg-web/weikemsg-web;
}

再次访问,但是结果没有任何变化

  1. 第三次尝试解决

F570147A-895F-4223-AAD5-4E92C885BAB9

重新观察请求头信息,浏览器发送OPTIONS请求并没有携带Cookie,该请求进入服务器后被判断未登录,所以一直返回401错误,首先需要解决的是处理OPTIONS请求

重新配置Nginx:

1
2
3
4
5
6
7
8
9
10
location ^~ /weikemsg-web {
if ($request_method = 'OPTIONS') {
return 204; // 等同于请求执行成功,但是没有数据,浏览器不用刷新页面.也不用导向新的页面
}
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS';
proxy_pass http://weikemsg-web/weikemsg-web;
}

再次跨域访问,结果如下:

5EC9BD61-4FF4-4ED3-A69F-E73E0E9D0475

676169C7-A9CF-4737-8B1B-25546C423DDB

跨域的问题已经解决,但是由于我的请求头中设置了'Content-Type':'application/json',需要服务端支持该类型

  1. 第三次尝试解决: 终极方案

重新配置Nginx

1
2
3
4
5
6
7
8
9
10
location ^~ /weikemsg-web {
if ($request_method = 'OPTIONS') {
return 204;
}
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type';
proxy_pass http://weikemsg-web/weikemsg-web;
}
  • Access-Control-Allow-Headers :指明了实际请求中允许携带的首部字段。

结果如下:

75C9B3DC-40DE-4F2D-BDE3-7E9B8E40E803

到此,跨域问题终于解决了

总结

Ajax跨域可以用2种方式解决:Nginx跨域配置,Java服务端配置

Nginx跨域配置(推荐)

  • 简单跨域请求

如果需要跨域的服务接口提供简单的http请求,可以在Nginx中配置如下:

1
2
3
4
5
6
location ^~ /microlesson-web {
add_header 'Access-Control-Allow-Origin' "*";
add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS';
proxy_pass http://192.168.3.58:9280/microlesson-web;
}

注意:如果http请求需要带上Cookie,必须设置Access-Control-Allow-Credentialstrue,同时不能设置Access-Control-Allow-Origin*

跨域发送Cookie还要求Access-Control-Allow-Origin不允许使用通配符。 事实上不仅不允许通配符,而且只能指定单一域名

If the credentials flag is true and the response includes zero or more than one Access-Control-Allow-Credentials header values return fail and terminate this algorithm. –W3C Cross-Origin Resource Sharing

修改如下:

1
2
3
4
5
6
7
8
location ^~ /microlesson-web {
add_header 'Access-Control-Allow-Origin' "$http_origin"; //允许http访问源地址
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With';
proxy_pass http://192.168.3.58:9280/microlesson-web;
}
  • 非简单跨域请求

项目中常见的非简单跨域请求一般是:post请求,以及json数据;Nginx配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
location ^~ /microlesson-web {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
#其他头部信息配置,省略...
return 204;
}
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'POST, GET, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With';
proxy_pass http://192.168.3.58:9280/microlesson-web;
}

关键的地方在于先判断请求类型如果是OPTIONS,设置Allow的响应头,重新处理这次请求。

  • 过滤请求域名

如果需要指定一些域名跨域访问服务,可以如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
location ^~ /microlesson-web {
set $cors '';
if ($http_origin ~* 'http?://(localhost|www\.ximalaya\.com)') {
set $cors 'true';
}
if ($cors = 'true') {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Mx-ReqToken,X-Requested-With';
}
proxy_pass http://192.168.3.58:9280/microlesson-web;
}

参考

HTTP访问控制(CORS)

为什么我的跨域 AJAX 发了两个请求?

Nginx配置支持CORS

Nginx跨域配置

跨域资源共享 CORS 详解

ajax跨域解决方案

CORS 跨域发送 Cookie

热评文章