前端ajax跨域请求方案沙里淘金

1. 所谓跨域

跨域是一种浏览器同源安全策略,也即浏览器单方面限制脚本的跨域访问。很多人可能误认为资源跨域时无法请求,实质上请求是可以正常发起的(指通常情况下,部分浏览器存在部分特例),后端也可能正常进行了处理,只是在返回时被浏览器所拦截。可以论证这一点的著名案例就是CSRF跨站攻击。

另外,所谓跨域都是在讨论浏览器行为,包括各种webview容器,其中犹以 XmlHttpRequest 为主。正是由于javascript跑在浏览器之上,所以ajax的跨域成了痛点。

2. 跨域形成

请求的url与当前页面不同即产生跨域,除常理上的站点直接性不同(百度域名下访问谷歌资源),同个站点也可以产生跨域:

  1. 协议跨域,例如从 http 站点访问 https 站点。
  2. 主机跨域,例如从 a.baidu.com 访问 b.baidu.com
  3. 端口跨域,例如从80端口的站点访问8080端口的站点。

请求域名和直接请求该域名对应的ip之间也算跨域。

内部判断规则:url首部匹配

window.location.protocol + window.location.host

简单性的将协议、主机名和端口号抽出进行对比,不同即跨域,所以也是不会去转化为ip地址的。

3. 跨域方案之Jsonp

谈起Jsonp在跨域处理方案中也算鼎鼎大名,这是一种非官方的解决方案,源于浏览器允许一些带src属性的标签跨域,例如iframe、script、img等。而Jsonp即是利用了script加载外部脚本的功能。

例如常规下的请求

get => http://a.test.com/users

=>>

[{
    username : '沐心chen',
    sex : '男',
    address : '广东深圳'
},{
    username : '李彦宏',
    sex : '男',
    address : '山西阳泉'
}]

由于浏览器的同源策略被阻止,此时前端使用script脚本去加载:

<script src="http://a.test.com/users"></script>

显然可以成功请求到,只是单纯的json数据无法使用。此时如果后端介入,返回之前包装成如下形式:

jsonp([{
    username : '沐心chen',
    sex : '男',
    address : '广东深圳'
},{
    username : '李彦宏',
    sex : '男',
    address : '山西阳泉'
}])

对于js而言,这就是一个普通的函数调用

jsonp(...params)

那么只要前端定义jsonp这个函数,它就会被执行并传入json数据。

var jsonp = function(data){
    //输出json
    console.dir(data);
}

jsonp跨域的流程走完,只是单纯到这一步还不行,因为它将导致后端无法正确处理非jsonp的请求,所以通常会约定一个参数callback,带上回调的函数名。

<script src="http://a.test.com/users?callback=jsonp"></script>

后端得到callback参数时,使用该值包装json数据,否则正常处理。

需要注意的是,处理jsonp的函数必须在window下,也即

window.jsonp = function(data){
    console.dir(data);
}

方案虽然可行,但也同时意味着jsonp只能发起get请求,对于post就无能为力了。

知道了原理,使用起来相对还是麻烦,那么如何用js简单封装一个jsonp方案呢?

var getJsonp = function(url, success){
    //声明window下的jsonp函数
    window.jsonp = function(data){
        //jsonp函数被执行将data转发到success函数
        success(data);
    }
    var src = '';
    //判断地址是否带其它参数决定callback怎么拼接
    if(url.IndexOf('?') != -1){
        src = url + '&callback=jsonp';
    }else{
        src = url + '?callback=jsonp';
    }
    //动态创建script标签
    var script = document.createElement('script');
    script.type = "text/javascript";
    script.src = src;
    document.head.appendChild(script);
}

//用法
getJsonp('http://test.com/users', function(data){
    console.log('得到jsonp数据:',JSON.stringify(data));
});

上面只是一个简单的封装思路,如果需要做的更好可以允许指定callback,还可以在回调函数之后销毁script脚本,这些留给大家去发挥。

浏览器支持:几乎所有

4. 跨域解决方案之CORS

CORS,也即 Cross-Origin Resource Sharing(跨域资源共享),它需要现代浏览器的支持,是一种更安全的官方解决方案。

CORS使得以下常见场景得到支持:

  1. 使用 XMLHttpRequest 或 Fetch 发起跨站 HTTP 请求。
  2. web 字体(css 中通过 @font-face 使用跨站字体资源)
  3. 使用 drawImage 绘制 Images/video 画面到 canvas

CORS有以下三种常见的访问控制场景:

  1. 简单请求

    • 只使用 GET 、HEAD 或者 POST 发起请求,如果使用 POST ,那么其数据类型( Content-Type )只能是 application/x-www-form-urlencodedmultipart/form-datatext/plain中的一种。
    • 不使用自定义请求头

    这种请求跟正常的ajax请求几乎没有差异,只是浏览器会在请求头中自动添加一个origin属性,内容为本页面地址。例如我们使用 XMLhttprequest 正常发起一个 GET 请求,源站点为my.com,目标站点为test.com,浏览器实际发出的请求头如下:

    GET /resources/public-data/ HTTP/1.1
    ...
    Origin: http://my.com
    

    此时浏览器维持判断,当服务端返回的响应头中,存在跨域访问控制属性并匹配本次请求,则跨域成功(正常接收数据)。

    HTTP/1.1 200 OK
    ...
    Access-Control-Allow-Origin: http://my.com
    

    这种跨域请求非常简单,只需要后端在返回的响应头中添加Access-Control-Allow-Origin属性并将被允许的站点填入即可(多个站点逗号隔开,允许所有站点则设为*

  2. 预请求

    预请求不同于简单请求,它首先会发送一个 OPTIONS 请求到目标站点,以查明该请求是否安全可接受,以防止请求对目标站点的数据造成破坏。当请求具备以下条件,就会被当成预请求处理:

    • 请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为 application/x-www-form-urlencoded , multipart/form-data 或者 text/plain 以外的数据类型。比如说,用 POST 发送数据类型为 application/xml 数据的请求。
    • 使用自定义请求头

    例:

    var request = new XMLHttpRequest();
    var url = 'http://test.com/users';
    var body = 'test';
    
    function coAccess(){
        if(request)
        {
          request.open('POST', url, true);
          request.setRequestHeader('X-CUSTOMER-HEADER', '沐心chen');
          request.setRequestHeader('Content-Type', 'application/xml');
          request.onreadystatechange = function(state){
              ...
          };
          request.send(body); 
        }
        ...
    

    上面发送了一个 POST 请求,请求数据类型为application/xml,并携带一个自定义请求头X-CUSTOMER_HEADER,符合预请求的规范。

    此时浏览器与后端的交互过程如下:

    //浏览器预先发起OPTIONS请求
    ,自动添加Origin、Access-Control-Request-Method和Access-Control-Request-Headers
    OPTIONS /resources/post-here/ HTTP/1.1
    ...
    Origin: http://my.com
    Access-Control-Request-Method: POST
    Access-Control-Request-Headers: X-CUSTOMER-HEADER
    
    //后端接收OPTIONS请求,返回响应头中包含Access-Control-Allow-*策略和Access-Control-Max-Age时限
    HTTP/1.1 200 OK
    ...
    Access-Control-Allow-Origin: http://my.com
    Access-Control-Allow-Methods: POST, GET, DELETE, UPATE, PATCH, OPTIONS
    Access-Control-Allow-Headers: X-CUSTOMER-HEADER
    Access-Control-Max-Age: 1728000
    Vary: Accept-Encoding, Origin
    
    //浏览器判断本次请求被允许,真实发起原先的POST请求
    POST /resources/post-here/ HTTP/1.1
    ...
    X-CUSTOMER-HEADER: 沐心chen
    Origin: http://my.com
    
    //服务器返回数据
    HTTP/1.1 200 OK
    ...
    Access-Control-Allow-Origin: http://my.com
    Vary: Accept-Encoding, Origin
    

    OPTIONS是一个理论上不应该对服务端数据造成影响的请求方式。响应头Access-Control-Allow-Methods表明服务器可以接受POST, GET, DELETE, UPATE, PATCH, OPTIONS的请求方法,而Access-Control-Max-Age则告诉浏览器本次预请求的有效期为20天,在这段时间内针对该站点的请求都不需要再预先发起OPTIONS请求。

  3. 带凭证的请求

    跨站请求一般而言,是不会携带cookie和其它凭证的,但 CORS 允许这样做。

    var request = new XMLHttpRequest();
    var url = 'http://test.com/users';
    
    function coAccess(){
        if(request)
        {
          request.open('GET', url, true);
          request.withCredentials = true;
          request.onreadystatechange = function(state){
              ...
          };
          request.send(body); 
        }
        ...
    

    我们在request中将withCredentials设置为true,使得该请求携带cookie和凭证,此时服务端必须在响应头中声明Access-Control-Allow-Credentialstrue,否则响应体将被浏览器忽略。

    //浏览器发起请求,携带cookie信息
    GET /resources/access-control-with-credentials/ HTTP/1.1
    ...
    Origin: http://my.com
    Cookie: rememberMe=沐心chen
    
    //服务端返回,设置了更多cookie
    HTTP/1.1 200 OK
    ...
    Access-Control-Allow-Origin: http://my.com
    Access-Control-Allow-Credentials: true
    Vary: Accept-Encoding, Origin
    Set-Cookie:rememberYou=沐心chen
    

    值得一提的是,带凭证的请求要求服务端具体设置Access-Control-Allow-Origin的值而不允许使用*,否则响应也会被浏览器忽略。如果一切正常,跨域访问将同时允许cookie的读和写。

上面一直没提的一个响应头属性是 Vary,顺带提及一下,如果我们的跨域方案不需要cookie参与,那么Access-Control-Allow-Origin 是允许设置为 * 的,但如果我们具体的去设置它的允许域名,则需要后端在响应头再设置一个 Vary 参数,值为 Accept-Encoding, Origin ,它告诉浏览器,响应是根据请求头里的Origin的值来返回不同的内容的。

尽管 CORS 需要现代浏览器的支持,但几乎不用关心这个问题,因为大部分目前仍存活的浏览器都有作出实现,对于前端来说可能最多是设置允许携带凭证,其它的工作就解放到后端了。

浏览器支持:

Destop Mobile
IE8+ Android2.1
Chrome4+ Safari3.2
firefox3.5+ 其它
Opera12+ ..
Safari4+ ..

5. 结言

本篇文章仅针对前端跨域解决方案,虽然不同站点间传输数据方案不少,但只有真正的痛点需要拿出来剖析,零零散散的不再赘述,如需了解更多可以自行搜索。后端对跨域的处理也许抽时间还会独立写一篇文章解读,感兴趣的朋友走个关注,眼熟ID,不胜感激。

发现挺多站点自行转载文章,也并没有注明出处,写文章并不容易,希望以后可以在转载的同时注明一下来源,稍微尊重别人的劳动成果,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容