前后端对跨域请求的处理

ajax跨域请求及Content-Type相关内容

文中出现的源码在这里

I、简单请求

简单请求:使用 GET/POST/HEAD 的请求方式,且没有人为设置 CORS 安全首部字段集合之外的其他首部字段,且 Content-Type 首部为这三个值:text/plain, multipart/form-data, application/x-www-form-urlencoded,这种请求就是简单请求;本篇只讨论 Content-Type 这个安全首部;

跨域涉及的相关定义及说明(包括简单请求)可参考这里

请求中的默认 Content-Type 即前端请求时没有特殊设置 Content-Type,由浏览器来决定该字段的值。

get 请求的默认 Content-Type 为 text/plain ;
post 请求的默认 Content-Type 为 application/x-www-form-urlencoded;
multipart/form-data 是用在 form 表单或以 form 表单格式提交数据的情况,本篇也不讨论;

II、跨域的四种情况

实际开发中,跨域请求依场景不同会有几种情况,查看以下几种情况可搭配 demo 进行理解:

1、不带Cookie,默认Content-Type

场景:

不需要拿 cookie 做校验,只要域名在白名单,服务器都返回正常的数据,比如获取地域数据这种接口;

后端:

只需设置 Access-Control-Allow-Origin 字段为 * 或当前请求的域(包括协议、域名和端口)即可;

设置成 * 所有域名都可以访问了,尽量不要这么做;
一般做法是后端维护一个域名白名单,拿请求头里的 Origin 到白名单里查,有的话才返回数据;

响应结束,浏览器会取响应头里 Access-Control-Allow-Origin 字段的值,看该值是否包含当前域名;

如果包含,触发 onreadystatechange 事件,同时把响应数据传给 XMLHttpRequest 对象;

如果不包含,也触发 onreadystatechange 事件,readyState 状态码直接改为4,XMLHttpRequest 对象没有接收到响应数据,浏览器 Console 里报错,

前端:

无需特殊处理;

此种情况对应 html/test1.html

2、带Cookie,默认Content-Type

场景:

这种情况一般是后端接口需要从 cookie 里取某些信息做校验

后端:

'Access-Control-Allow-Origin': 'http://test.com:8090', // 发请求页面的 协议+域名+端口
'Access-Control-Allow-Credentials': true,

比 1 多了 Access-Control-Allow-Credentials: true,且 Access-Control-Allow-Origin 不能为星号;

这种情况下,cookie 还是要遵循同源策略的:

  • 无法在当前页获取到其他域的 cookie,只能获取到当前域或其父域的 cookie;比如 a.com 中的页面,肯定获取不到 domain 为 b.com 的 cookie;a.test.com中,可以获取到 domain 为 test.com 的 cookie;
  • 主域在设置 cookie 的时候,比如设置为 test.com,则 cookie 的 domain 为 .test.com,默认前边就加了一个点;
  • 同源策略已经过滤掉了一些 cookie,如果接口域名和页面所在的域名不遵循同主域的规则,则请求时也无法带 cookie ;这点比较好理解,页面里获取不到接口域名所对应的 cookie,也就无法携带了;a.com 下的页面,访问b.com 下的接口时,不管怎么配置,cookie 均为空;

前端:

设置 XMLHttpRequest 对象的 withCredentials 属性为 true;

jquery ajax请求中,设置 xhrFields 下的 withCredentials 为 true;

  // 原生js
  var xhr = new XMLHttpRequest();
  xhr.open('GET', 'http://server.com:8091', true);
  xhr.withCredentials = true;
  // ……

  // jquery - ajax 
  $.ajax({
    url: 'http://server.com:8091',
    xhrFields: {
      withCredentials: true
    },
    // ……
  });

此种情况对应 html/test2.html

3、不带Cookie,自定义Content-Type

这里的自定义,指的是 Content-Type 非简单请求里规定的那三种格式,最常用的就是 appliction/json;这种格式虽然比较常用,但不在那三种默认格式中,设置了此值,请求就变成了“复杂请求”;

复杂请求在发送时,需要先向服务器发送一个预检请求,所以经常会在跨域页面中,看到一个ajax请求在开发者工具中变成了两次,其实第一次是预检请求,服务器只返回了响应头,没有响应体,第二次才是真正的请求;

预检请求,顾名思义就是预先检查服务器的请求,主要是用来通知服务器我接下来要发一个什么样的请求,你支不支持;
服务器根据请求头里的信息,检查自身能否支持,然后返回对应的头部,浏览器根据响应头的信息,决定要不要真的发起请求;

预检请求的类型是 OPTIONS,同域下复杂请求不需要预检(对应 html/test5.html);
对预检类的请求,服务器只需要返回响应头,不需返回响应体;
服务器对预检请求返回的响应头,同样需要按照跨域来处理,返回对应的字段;

详细的定义和说明还是 参考这篇文章

场景:

自定义传输的数据格式

后端:

'Access-Control-Allow-Origin': 'http://test.com:8080',
'Access-Control-Allow-Headers': 'Content-Type',

Access-Control-Allow-Headers:表示服务器允许请求中携带字段;

前端:

前端需要设置请求头中的 Content-Type:

// 原生
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://b.test.com:8091', true);
xhr.setRequestHeader("Content-Type", "application/json");

// jquery-ajax,可通过 headers 或在 beforeSend 中设置
$.ajax({
  url: 'http://b.test.com:8091',
  headers: {
    'content-type': 'application/json'
  },
  // ……
});
$.ajax({
  url: 'http://b.test.com:8091',
  beforeSend: function (xhr) {
    xhr.setRequestHeader('content-type', 'application/json');
  },
  // ……
});

此种情况对应 html/test3.html

4、带Cookie,自定义Content-Type

和2中的情况类似,只是数据类型做了变换;

后端:

'Access-Control-Allow-Origin': 'http://test.com:8090',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Credentials': true,

前端:

// 原生
var xhr = new XMLHttpRequest();
xhr.open('GET', ’http://test.jd.com:8091‘, true);
xhr.withCredentials = true;
xhr.setRequestHeader("Content-Type", "application/json");

// jquery
$.ajax({
  url: 'http://b.test.com:8091',
  headers: {
    'content-type': 'application/json'
  },
   xhrFields: {
     withCredentials: true
   },
  // ……
});

此种情况对应 html/test4.html

5、特殊情况

1、上面 “3、不带Cookie,自定义Content-Type”中,如果服务器只在预检请求中返回 Access-Control-Allow-Origin: Content-Type,接口同样可以正常返回,前端可以正常接收到数据;

2、上面“4、带Cookie,自定义Content-Type”中,如果服务器只在预检请求中返回 ‘Access-Control-Allow-Credentials’: true,浏览器中会报错,但是开发者工具中可以看到数据,xhr对象接收不到数据,但是服务器返回了数据;
这种情况是浏览器判断响应头与请求头不匹配,没有将值传给xhr对象;
因为数据返回了,如果改用其他方式,比如python模拟一个相同的请求,没有浏览器的限制规则,就能直接拿到响应数据了;
一定要保证预检和真实请求的响应头一致;

3、自定义 Content-Type 的情况,如果预检请求中返回了请求体,浏览器依旧会继续发送真实请求;在预检请求的响应中返回响应体会浪费资源、延长整个请求的时间;

特殊情况测试 demo 对应 html/test6.html

总结

跨域请求中,相关的响应头是下面三个:

Access-Control-Allow-Origin :控制能否跨域;
Access-Control-Allow-Credentials :控制能否带cookie;
Access-Control-Allow-Headers :控制请求中可以自定义的字段;

后端需要处理的,也就是这三个字段,没有特别麻烦,但实际工作中,好多后端都不知道怎么配置,再出现这种情况,直接给他这个文章地址就好了;

III、Content-Type和请求数据类型

Content-Type和请求数据类型的作用如下:

  • Content-Type:告诉服务器/后端框架,我传的数据是什么类型,你要用什么方法解析;它和请求数据类型并无直接关联,只是有默认的规约,大家都这么搭配;
  • 请求数据类型:前端格式化好放到请求体里的,只要没有涉及文件上传,HTTP请求体传输的时候都是纯文本,一般就 a=1&b=2 和 {“a”:1,”b”:2} 这两种情况;

Content-Type 类型是由后端开发决定的,他通过什么方式取前端就怎么传;但在后端不同的语言和框架下,获取参数的方法有所差异,加上后端对框架封装的取参数方法内部逻辑不了解,导致有些后端也无法提供准确的Content-Type和参数类型,这时候就需要前端来试了,一般的搭配是这样:

1、GET

Content-Type 不论是默认的 text/plain,还是自定义的 appliction/json,对后端参数接收没有任何影响,因为 get 请求参数在 url 中,url 属于请求头,而 Content-Type 中的Content 指的是请求体,get 的请求体是空的;

2、POST

post也非常简单,正常情况就两种,这里的正常情况指的是后端框架封装过的取参方法;

2.1、Content-Type 为默认值,请求数据类型为 FormData

Content-Type 为默认的 application/x-www-form-urlencoded,原生xhr对象请求发送的是字符串:

var xhr = new XMLHttpRequest();
xhr.open("POST", 'http://test.com/getAll', true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
var param = 'a=1&b=2';
xhr.send(param);


这种情况下,浏览器的 NetWork 中,请求的参数就是 Form Data 类型;

Form Data 算是一种规范,是浏览器和服务器默认支持的,按 a=1&b=2 这种格式传数据的时候,浏览器就把数据格式化并取个名字叫 Form Data;

2.2、Content-Type 为自定义,请求数据类型为对象字符串

Content-Type 为 application/json 时,原生xhr对象请求发送的是对象字符串,这种格式用的比较多,很多后端框架也实现了该格式的解析:

var xhr = new XMLHttpRequest();
xhr.open("POST", 'http://test.com/getAll', true);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
var param = JSON.stringify({a: 1, b: 2});
xhr.send(param);

这种情况下,浏览器的 NetWork 中,请求的参数是 Request Payload 类型;

Request Payload 是浏览器上的说法,只出现在 POST请求非默认 Content-Type 的情况下,浏览器只是给这种情况取了个名字,如果用抓包工具查看每个请求的原始请求信息,就比较清楚了;

总结

单从前端讨论 Content-Type 和请求数据类型意义不大,因为它们的值和格式是由后端决定的;后端框架一般默认都会支持上面两种情况;

前后端对接的时候主要还是看服务器的支持情况,比如后端框架中实现了a=1,b=2 这种数据格式的解析,但是 Content-Type 必须设置为 text/plain,那前端就按这个规则传就可以了;

又或者后端开发把框架中的 Content-Type 处理覆盖掉,只接收 a=1,b=2 这种格式,这时候,不管前端如何设置 Content-Type,后端就是不认,统一按照上述格式解析,这时候再传对象字符串,后端就取不到值了;

如果这篇文章对你有用,可以点击下面的按钮告诉我

0

发表回复