使用Koa亲自体验跨域

跨域问题的存在是因为浏览器都遵循同源策略

同源策略

1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"。

  • 协议相同
  • 域名相同
  • 端口相同

举例来说,http://www.netease.com/a.html这个网址,协议是http://,域名是www.netease.com,端口是80(默认端口可以省略)。它的同源情况如下。

img

跨域

针对浏览器的Ajax请求跨域的主要解决方案有:JSONP、CORS。

AJAX

首先演示下正常情况下,通过Ajax进行跨域请求的情景:

  1. 通过koa启了两个本地server,一个port为3200,一个为3201

    app.js

    const koa = require('koa');
    const app = new koa();
    
    const Router = require('koa-router');
    const router = new Router();
    
    const serve = require('koa-static');
    
    const path = require('path');
    
    const staticPath = path.resolve(__dirname, 'static');
    
    // 设置静态服务
    const staticServe = serve(staticPath, {
      setHeaders: (res, path, stats) => {
        if (path.indexOf('jpg') > -1) {
          res.setHeader('Cache-Control', ['private', 'max-age=60']);
        }
      }
    });
    app.use(staticServe);
    
    router.get('/ajax', async (ctx, next) => {
      console.log('get request', ctx.request.header.referer);
      ctx.body = 'received';
    });
    
    app.use(router.routes());
    
    app.listen(3200);
    console.log('koa server is listening port 3200');
    

    app2.js

    const koa = require('koa');
    const app = new koa();
    const Router = require('koa-router');
    
    const router = new Router();
    
    router.get('/ajax', async (ctx, next) => {
      console.log('get request', ctx.request.header.referer);
      ctx.body = 'received';
    });
    
    app.use(router.routes());
    
    app.listen(3200);
    console.log('app2 server is listening port 3200');
    

    两个server都定义了一个GET请求接口/ajax。除监听port不同外,app.js还设置了静态服务。

    origin.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>cross-origin test</title>
    </head>
    <body style="width: 600px; margin: 200px auto; text-align: center">
      <button onclick="getAjax()">AJAX</button>
      <button onclick="getJsonP()">JSONP</button>
    </body>
    <script type="text/javascript">
    
      var baseUrl = 'http://localhost:3201';
    
      function getAjax() {
        var xhr = new XMLHttpRequest();            
        xhr.open('GET',  baseUrl + '/ajax', true);
        xhr.onreadystatechange = function() {
          // readyState == 4说明请求已完成
          if (xhr.readyState == 4 && xhr.status == 200 || xhr.status == 304) { 
            // 从服务器获得数据  
            alert(xhr.responseText);
          } else {
            console.log(xhr.status);
          }
        };
        xhr.send();
      }
    </script>
    </html>
    

    origin.html放在app.js server对应的静态服务目录下,通过XMLHTTPRequest简单实现了一个Ajax Get方法。

  2. 修改请求地址

    • http://localhost:3200/ajax

      服务器返200,且拿到返回值。

      image-20180802120119094
    • http://localhost:3201/ajax

      当ajax发送跨域请求时,控制台报错:
      Failed to load http://localhost:3201/ajax: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3200' is therefore not allowed access.

      这里有个奇怪的点,虽然控制台有报错,但AJAX请求收到了200的应答:

      image-20180802160018976

      这其实涉及到浏览器的CORS机制,具体在后面展开。

JSONP

  • 原理

    虽然浏览器同源策略限制了XMLHttpRequest请求不同域上的数据。但是,在页面上引入不同域的js脚本是可以的,而且script元素请求的脚本会被浏览器直接运行

  • 测试

    origin.html的脚本中添加:

    function getJsonP() {
        var script = document.createElement('script');
        script.src = baseUrl + '/jsonp?type=json&callback=onBack';
        document.head.appendChild(script);
    }
    
    function onBack(res) {
      alert('JSONP CALLBACK:  ', JSON.stringify(res)); 
    }
    

    getJsonP方法会在当前页面添加一个script,src属性指向跨域的GET请求:

    http://localhost:3201/jsonp?type=json&callback=onBack,通过query格式带上请求的参数。callback是关键,用于定义跨域请求回调的函数名称,这个值必须后台和脚本保持一致。

    app2.js中添加jsonp请求的路由:

    router.get('/jsonp', async (ctx, next) => {
      const req = ctx.request.query;
      console.log(req);
      const data = {
        data: req.type
      }
      ctx.body = req.callback + '('+ JSON.stringify(data) +')';
    })
    
    app.use(router.routes());
    

    针对jsonp请求,后台要做的是:

    1. 获取请求参数中的callback值,如本例中的onBack
    2. 将callback的值以function(args)的格式作为response。

    重启服务后,触发页面的getJsonP事件,效果:

    image-20180802152141423
  • 补充

    jquery,zepto这些js第三方库,其提供的ajax 方法都有对jsonp请求进行封装,如jquery发jsonp的ajax请求:

    function getJsonPByJquery() {
        $.ajax({
          url: baseUrl + '/jsonp',
          type: 'get',
          dataType: 'jsonp',  // 请求方式为jsonp
          jsonpCallback: "onBack",    // 自定义回调函数名
          data: {
            type: 'json'
          }
        });
      }
    

    执行的效果一致。

  • 优点

    JSONP方案的兼容性好,IE浏览器也支持。

  • 缺点

    • 因为是利用的<script>元素,所以只支持GET请求。
    • 缺乏错误处理机制

CORS

CORS即跨域资源分享(Cross-Origin Resource Sharing),是W3C制定的标准。

  • 特性

    CORS需要浏览器和服务器同时支持。

    • 大多主流浏览器都支持,IE 10以下不支持。
    • 只要服务器端实现了CORS接口,浏览器就能自动实现基于CORS的跨域请求。
  • 两种请求

    浏览器将CORS请求分成两类:简单请求和非简单请求。

    简单请求需要满足两个条件:

    1. 请求类型为HEADGETPOST之一;
    2. 请求头信息不超出以下几种:
      • Accept
      • Accept-Language
      • Content-Language
      • Last-Event-ID
      • Content-Type:只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain

简单请求

对于简单请求,浏览器会直接发出,同时在请求头中添加Origin字段。

Origin用来说明请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

回顾下直接Ajax测试跨域的请求报文:

image-20180802163305336

浏览器为这个简单的GET请求添加了Origin,而响应头信息中没有Access-Control-Allow-Origin,浏览器判断请求跨域,给出错误提示。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

origin.html中添加一个post请求:

function corsWithJson() {
    $.ajax({
      url: baseUrl + '/cors',
      type: 'post',
      contentType: 'application/json',
      data: {
        type: 'json',
      },
      success: function(data) {
        console.log(data);
      }
    })
  }

通过设置Content-Typeappliaction/json使其成为非简单请求:

image-20180802182946997

"预检"请求的方法为OPTIONS,服务器判断Origin为跨域,所以返回404。

除了Origin字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

(2)Access-Control-Request-Headers

该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是content-type

服务端设置CORS

CORS允许服务端在响应头中添加一些头信息来响应跨域请求。

app2.js引入koa2-cors

app.use(cors({
  origin: function (ctx) {
      if (ctx.url === '/cors') {
          return "*"; // 允许来自所有域名请求
      }
      return 'http://localhost:3201';
  },
  exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'],
  maxAge: 5,
  credentials: true,
  allowMethods: ['GET', 'POST', 'DELETE'], //设置允许的HTTP请求类型
  allowHeaders: ['Content-Type', 'Authorization', 'Accept'],
}));

重启服务后,浏览器重新发送POST请求。可以看到浏览器发送了两次请求。

OPTIONS请求报文:

image-20180802185912190

OPTIONS的响应头表示服务端设置了Access-Control-Allow-Origin:*,于是发送POST请求,得到服务器返回值。

image-20180802190147879

在OPTIONS的请求响应报文中,头信息里有一些CORS提供的其他字段:

Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Content-Type,Authorization,Accept
Access-Control-Allow-Methods: GET,POST,DELETE
Access-Control-Max-Age: 5

(1)Access-Control-Allow-Methods

该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

(2)Access-Control-Allow-Headers

如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

(3)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(4)Access-Control-Max-Age

该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。

CORS与JSONP的区别

CORS更为标准,功能也更强大。而JSONP只支持GET请求。CORS唯一不足在于IE10以下不支持。

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

推荐阅读更多精彩内容

  • 什么是跨域 跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实...
    HeroXin阅读 831评论 0 4
  • 什么是跨域 跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实...
    Yaoxue9阅读 1,286评论 0 6
  • 题目1.什么是同源策略? 同源策略(Same origin Policy): 浏览器出于安全方面的考虑,只允许与本...
    FLYSASA阅读 1,709评论 0 6
  • 前言:对于跨域请求,很早之前就有去了解过,但因为一直关注的都是服务器后端开发,故也就仅仅停留在概念的理解上而没有机...
    ken_ljq阅读 89,721评论 6 128
  • 什么是跨域 跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实...
    他方l阅读 1,059评论 0 2