跨域之CORS

2019-03-23  本文已影响0人  悄敲

关于跨域请求的方式,这篇文章有详细的介绍

这里只介绍下CORS。
背景:(1)由于安全原因,浏览器会限制脚本中发起的跨域HTTP请求,这是遵循同源策略的表现。
(2)跨域请求例子:

The frontend JavaScript code for a web application served from http://domain-a.com uses XMLHttpRequest to make a request for http://api.domain-b.com/data.json.

定义:"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
下面这段摘自MDN,对CORS的解释简洁明确

Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected resources from a server at a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, and port) than its own origin.

图1-同源与跨域的资源请求-from MDN

CORS 需要浏览器和后端同时支持。浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。服务器端的CORS设置参见

CORS存在两种请求:简单请求和复杂请求。

(一)简单请求:

同时满足以下两类条件,就是简单请求,否则就是复杂请求。
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin指定的源,不在许可范围内,服务器会返回一个与同源访问一致的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获,见下图。(注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。)

图2-简单跨域请求报错
图2中,直接在 WebStorm 打开 html 页面,WebStorm 自动为这个页面创建的服务器端口号为 63342,而跨域访问的目标域对应服务器中并未允许来自 http://localhost:63342的跨域请求,所以报错。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段(与CORS相关的都以 Access-Control- 开头)。
(注意图3对应的其实是复杂请求)

图3-服务器返回的响应头
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值(如http://localhost:3000),要么是一个*,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
为了在CORS请求中发送Cookie和HTTP认证信息,除了要在服务器中设置Access-Control-Allow-Credentials为 true ,还需要在请求页的XMLHttpRequest请求中设置 withCredentials 属性为 true。
注意:如果要发送Cookie,Access-Control-Allow-Origin就不能设为通配符*,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源策略,只有包括在服务器Access-Control-Allow-Origin设置的域名中的Cookie才会上传,其他域名的Cookie并不会上传,且(跨域)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。
,在AJAX请求中打开withCredentials属性。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段(例如 cookie中包含的name中的值),就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('myName')可以返回myName字段的值。

(二)复杂请求

复杂请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。
复杂CORS请求,可能会对服务器上的数据产生副作用,在正式通信之前,会增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP请求方法和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面是一个CORS实例。
服务器 server1 对应 http://localhost:3000/ (以下简称 origin A),其 Index 页面中有一个 XMLHttpRequest请求,该请求要去跨域访问服务器 server2 对应的http://localhost:4000/getData(以下简称 origin B)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index for server1</title>
</head>
<body>
<h2>the index page responsed by server1</h2>
<script>
    let xhr=new XMLHttpRequest();
    //原网页代码中的document.cookie无法读取服务器域名下的Cookie。
    document.cookie='myName=gz';
    // 当前请求为跨域类型时是否在请求中协带cookie。
    xhr.withCredentials=true;
    xhr.open('put','http://localhost:4000/getData',true)
    xhr.setRequestHeader('myName','gz');
    xhr.onreadystatechange=function () {
        if(xhr.readyState===4){
            if((xhr.status>=200 && xhr.status<300) || xhr.status===304){
                console.log(xhr.response);
                console.log('myName: '+xhr.getResponseHeader('myName'));
            }
        }
    }
    xhr.send();
</script>
</body>
</html>

(代码块-1 origin A的index页面)

let express=require('express');
let url=require('url');
let fs=require('fs');
let app=express();

const handleStatic=function(req,res,next){
    let pathName=url.parse(req.url).pathname;
    if(pathName==='/'){
        pathName='index.html';
    }
    fs.readFile("./public/"+pathName,null,function (err,data) {
        if(err) {
            //文件不在,直接next到后面的请求
            console.log('file does not exits in public dir.')
            next();
        }  else {
            res.writeHead(200,{"Content-type":"text/html;charset=utf-8"});
            res.write(data);
            res.end();
        }
    });
}
app.use(handleStatic);
app.get('/hello',function (req, res) {
    res.send('hello, send by server1');
});
app.listen(3000,function () {
    console.log("server1 start! Listening at port 3000.");
});

(代码块-2 origin A的app.js,对应服务器 server1的相关设置)

let express=require('express')
let app=express()
let whiteList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
    let origin = req.headers.origin
    if (whiteList.includes(origin)) {
        // 设置哪个源可以访问我
        res.setHeader('Access-Control-Allow-Origin', origin)
        // 允许携带哪个头访问我
        res.setHeader('Access-Control-Allow-Headers', 'myName')
        // 允许哪个方法访问我
        res.setHeader('Access-Control-Allow-Methods', 'PUT')
        // 允许携带cookie
        res.setHeader('Access-Control-Allow-Credentials', true)
        // 指定本次预检请求的有效期,单位为秒。在此期间,浏览器不用发出另一条预检请求。
        res.setHeader('Access-Control-Max-Age', 600)
        // 允许返回的头
        res.setHeader('Access-Control-Expose-Headers', 'myName')
        if(req.method==='PUT'){
            res.setHeader('myName', 'gz2') //返回一个响应头,后台需设置
        }
        if (req.method === 'OPTIONS') {
            res.end() // OPTIONS请求为预检请求,判断服务端是否允许跨域请求。不做任何其他处理
        }
    }
    next()
})
app.put('/getData', function(req, res) {
    console.log(req.headers)
    res.end('跨域请求资源成功 by put method')
})
app.get('/getData', function(req, res) {
    console.log(req.headers)
    res.end('跨域请求资源成功 by get method')
})
//app.use(express.static(__dirname))
app.listen(4000,()=>console.log('server2 running at port 4000.'))

(代码块-3 origin B的app.js,对应服务器 server2 的相关设置)

以上设置完毕,在浏览器输入 http://localhost:3000/
(1)浏览器向服务器 server2 发送预检请求,请求头如图4所示。

预检请求头

(2)预检得到肯定回复后发送的正式XMLHttpRequest请求的请求头如图5所示。

图5
(3)server2 对正式请求返回的响应头部分信息如图6所示
图7-对正式请求的返回头信息
从图中可以看出,'myName'的值已经在 服务器 server2中被修改为 'gz2',而不是在请求页面index.html设置的 document.cookie='myName=gz';
(4)跨域请求的结果如图7所示。
图6-跨域复杂请求成功

Reference:

1
2
3

上一篇 下一篇

猜你喜欢

热点阅读