CORS详解

  CORS(Cross-Origin Resource Sharing, 跨源资源共享)是W3C出的一个标准,其思想是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。因此,要想实现CORS进行跨域,需要服务器进行一些设置,同时前端也需要做一些配置和分析。本文简单的对服务端的配置和前端的一些设置进行分析。

服务端的配置

本文服务端的代码采用的是node,使用koa,koa-router和koa2-cors。

配置的主要代码如下:

app.use(cors({
    origin: function(ctx) {
        const regexp = new RegExp('/CORS');
        const regexpWith = new RegExp('/CORSWith');
        if (regexpWith.test(ctx.url)) {
            return `http://${packageData.url}:7000`;
        } else if(regexp.test(ctx.url)) {
            return '*'
        } else if(~String(ctx.url).indexOf('/imgs/')) {
            return `http://${packageData.url}:7000`;
        }
        return false;
    },
    exposeHeaders: ['WWW-Authenticate', 'Server-Authorization', 'Date'],
    maxAge: 100,
    credentials: true,
    allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'X-Custom-Header', 'anonymous'],
}));

主要是使用了koa2-cors进行了配置,下面对配置项进行简单介绍:

origin:

设置Access-Control-Allow-Origin,源码如下:

  Access-Control-Allow-Origin表示允许跨域的域名,可以设置为*也可以设置为具体的域,其中,*表示全部,即所有的域名下的请求都允许,但设置为*后,所有的请求都不会携带附带身份凭证(比如cookie);设置为具体的域则表示只有该域下的请求允许,别的域下的请求不被允许,设置为具体的域是请求中携带身份凭证的基础。

exposeHeaders:

设置Access-Control-Expose-Headers,源码如下:

  Access-Control-Expose-Headers表示允许脚本访问的返回头,请求成功后,脚本可以在XMLHttpRequest中访问这些头的信息,如在上面配置的代码中设置了Date,在浏览器端就可以通过js拿到服务器的时间了,下面是通过XMLHttpRequest实例的getResponseHeader()获取服务器时间的代码:

let xhr = new XMLHttpRequest();
    xhr.open('GET', `http://${url}/CORS/userInfo/12`, true);
    xhr.onload = function() {
        if(xhr.readyState == 4) {
            try {
                // 获取服务器时间
                console.log(xhr.getResponseHeader('Date'));
            } catch(ex) {
                new Error(ex);
            }
        }
    };
    xhr.send();

maxAge:

设置Access-Control-Max-Age,源码如下:

Access-Control-Max-Age用来指定本次预检请求的有效期,单位为秒,预检请求将在下面具体介绍,现在先过。

credentials:

设置Access-Control-Allow-Credentials,源码如下:

  Access-Control-Allow-Credentials为服务端标识浏览器请求CORS时是否可以附带身份凭证,对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为“*”。

allowMethods:

设置Access-Control-Allow-Methods,源码如下:

Access-Control-Allow-Methods用来设置检查网络请求的方式,如GET、POST等。

allowHeaders:

设置Access-Control-Request-Headers,源码如下:

Access-Control-Request-Headers用来将实际请求所携带的首部字段告诉服务器,在这里可以自定义头部信息,用来对浏览器的非简单请求进行预检判断。

前端的配置

前端的配置主要通过简单请求和非简单请求,携带身份凭证,canvas中画图使用的跨域图片三部分进行讲解

简单请求和非简单请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

简单请求:

简单请求满足以下条件:

1. 使用下列方法之一:

  • GET
  • HEAD
  • POST

2.HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type:值属于下列之一:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

简单请求如图所示:

浏览器与服务器之间请求只进行了一次。

非简单请求:

不满足简单请求条件的请求则要先进行预检请求,即使用OPTIONS方法发起一个预检请求到服务器,已获知服务器是否允许该实际请求。

非简单请求如下所示:

下面是PUT请求第一次返回的结果:

  通过PUT请求结果可以看出,当检测到PUT请求为非简单请求时,浏览器便会发送一个预检请求,目的是询问,自定义头部X-Custom-Header的PUT请求是否被允许,浏览器返回了所有可以请求的方法和自定义的头部(把所有可以的返回是为了避免多次预检请求),这时候预检请求成功了,便会发送真正的PUT请求。

关于预检请求,需要注意一下两点:

  1. 预检请求对js来说是透明的,js获取不到预检请求的任何信息。
  2. 预检请求并不是每次请求都发生,服务端设置的Access-Control-Max-Age头部指定了预检请求的有效期,在有效期内的非简单请求不需要再次发生预检请求。

携带身份凭证

大部分的请求是需要用户携带着用户信息的,比如在一个登录的系统中,用户会携带着相应的cookie或token,但CORS跨域默认是不带身份凭证的。

如果需要附带身份凭证,在发送请求时,通过将withCredentials属性设置为true,可以指定某个请求可以发送凭据。

下面提供针对XMLHttpRequest附带身份凭证的兼容性写法:

function createCORSRequest(method, url) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
        if(xhr.readyState == 4) {
            try {
                if((xhr.status >= 200 && xhr.status < 300) || xhr == 304) {
                    console.log(xhr.response);
                } else {
                    console.log('Request was unsuccessful: ' + xhr.status);
                }
            } catch(ex) {
                new Error(ex);
            }
        }
    };
    if('withCredentials' in xhr) {
        xhr.open(method,url, true);
    } else if(typeof XDomainRequest != 'undefined') {
        xhr = new XDomainRequest();
        xhr.open(method, url);
    } else {
        xhr = null;
    }
    return xhr;
}

附带身份凭证对服务端有两个要求:

  1. 服务端的Access-Control-Allow-Origin头部不能设置为*
  2. 服务端的Access-Control-Allow-Credentials头部设置为true

canvas中画图使用的跨域图片

  尽管不通过 CORS 就可以在画布中使用图片,但是这会污染画布。一旦画布被污染,你就无法读取其数据。例如,你不能再使用画布的 toBlob(), toDataURL() 或 getImageData() 方法,调用它们会抛出安全错误。

这种机制可以避免未经许可拉取远程网站信息而导致的用户隐私泄露。

对跨域图片进行修改的话,img需要添加crossOrigin属性,代码如下:

function drawCanvas(id, drawId, url) {
        let canvas = document.getElementById(id);
        let ctx = canvas.getContext('2d');

        var img = document.createElement('img');
        img.src = url;
        img.crossOrigin = 'anonymous';
        // 必须等到图片完全加载后才能对其进行操作。浏览器通常会在页面脚本执行的同时异步加载图片。如果试图在图片未完全加载之前就将其呈现到canvas上,那么canvas将不会显示任何图片
        if (img.complete) {
            canvas.width = img.width;
            canvas.height = img.height;
            ctx.drawImage(img, 0, 0 );
        } else {
            img.onload = function () {
                canvas.width = img.width;
                canvas.height = img.height;
                ctx.drawImage(img, 0, 0 );
                const src = canvas.toDataURL(" image/jpeg", 0.3);
                $(drawId).attr('src', src);
            };
        }
    }

服务器针对跨域修改的图片也要做两点限制:

  1. 服务端的Access-Control-Allow-Origin头部不能设置为*
  2. 服务端的Access-Control-Request-Headers头部添加一个自定义头部,其值为img的crossOrigin的值

常见的图片跨域修改的错误有两种:

  • Uncaught SecurityError: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
  • Image from origin 'http://127.0.0.1:4000' has been blocked from loading by Cross-Origin Resource Sharing policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:7000' is therefore not allowed access.

总结

至此,关于CORS的一些知识点已经分享完了,总结如下:

CORS是浏览器和服务器配合完成的跨域请求,个人认为,主要是服务端配置好后,浏览器根据服务端配置的自定义头部和提供的可以进行的CORS的方法来进行跨域操作。

基于以上总结,提供测试Demo:

  https://github.com/weiruifeng/fetchTest

参考资料:

  HTTP访问控制(CORS): https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

 

如有问题,欢迎大家指正。

 

posted @ 2017-04-16 09:56  线流五里牌  阅读(1184)  评论(0编辑  收藏  举报