初探CORS

这篇博客的目的是探究一下CORS前后端的实现

CORS是什么?

CORS全拼是Cross-Origin Resource Sharing,翻译为跨域资源共享

解决了什么问题?

跨域资源共享机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行
我的理解是:

  • 资源共享
  • 资源能受资源提供者控制,保证安全,不被滥用

如何支持CORS?

目前所有浏览器端都已经支持,只需要后端服务配置一些HTTP响应头即可

动手实现demo

  • 使用node + express 提供一个HTML服务
  • 使用node的http模块提供一个跨域服务
使用的环境
node : v10.2.1
IDE :Visual\ Studio\ Code (强烈推荐)
  1. 创建根目录server
mkdir server && cd server
npm init -y
npm install express --save 
/* 我使用的版本是"express": "^4.16.3" */
  1. 创建src
mkdir src

当前的目录结构:

➜  server ls
node_modules      package-lock.json package.json      src
  1. src下创建HTML和HTML Server


    src.png
  1. 配置好基础demo
//index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title>HTTPCORS</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <h1> this is index.html </h1>
    <script type="text/javascript">
        var index = 0
function corsRequest(){
    var request = new XMLHttpRequest()
    request.onreadystatechange = function(){
        if(request.DONE && request.status == 200 && request.readyState == 4) {
            var responseText = request.responseText
            console.log('response = ' , responseText)
            updateText(responseText)
        }
    }
    request.open('GET','http://localhost:3001/',true)
    request.send()

}

function updateText(text){
    var root = document.getElementById('root')
    ++index
    var text = "[ " + index.toString() + " ] : " + text 
    root.innerHTML =  text
}
    </script>
    <div>
        <button onclick="corsRequest()">CORS请求</button>
    </div>
    <div id="root">

    </div>
</body>
</html>
//app.js

const path = require('path')
const fs = require('fs')
const express = require('express')

let app = express()
let indexPath = path.join(__dirname,"..","html",'index.html')
app.get("/",function(request,response){
    fs.readFile(indexPath,{encoding:'utf8'},(err,data)=>{
        response.set('Content-Length',data.length)
        response.set('Content-Type','text/html')
        response.writeHead(200)
        response.end(data)
    }) 
    
})
app.listen(3000)
  1. 启动服务,查看成果
➜  server cd src/htmlserver
➜  htmlserver node app.js
html.png
  1. 配置跨域测试服务
//server.js

const HTTP = require('http')
let server = HTTP.createServer((request,response)=>{
    console.log('CORC Server Recieve Request', "\n Method : " , request.method , "\n Headers : ", request.headers , )
    response.writeHead(200,{
        'Content-Type':'text/plain',
    })
    response.end('CORS Sever Success Response')
})
server.listen(3001)

启动服务

node server.js
testserver.png

到目前为止,前期配置可以了,下边来见识下CORS机制

初步demo

见证CORS

点击测试页面中的CORS请求

corserror.gif

居然有报错,我们在浏览器测试过是OK的,而且使用Chrome的Network Debug发现,localhost:3001这个请求成功。那为什么在HTML页面内部就不行了呢?

这就涉及到CORS机制了


划重点

浏览器检测到跨域服务没有遵循CORS,浏览器会自动过滤掉服务真实的响应

Access-Control-Allow-Origin

响应首部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:

Access-Control-Allow-Origin: <origin> | *

其中,origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。

//server.js

const HTTP = require('http')
let server = HTTP.createServer((request,response)=>{
    console.log('CORC Server Recieve Request', "\n Method : " , request.method , "\n Headers : ", request.headers , )
    response.writeHead(200,{
        'Content-Type':'text/plain',
        'access-control-allow-origin':"*", /* 允许任何来源 */
    })
    response.end('CORS Sever Success Response')
})
server.listen(3001)

再来测试:


corssuc.gif

preflight(预检请求)

在解释preflight之前,先来修改下index.html,感受下preflight,
设置跨域请求头Content-Type:application/json

//index.html
...
function corsRequest(){
    var request = new XMLHttpRequest()
    request.onreadystatechange = function(){
        if(request.DONE && request.status == 200 && request.readyState == 4) {
            var responseText = request.responseText
            console.log('response = ' , responseText)
            updateText(responseText)
        }
    }
    request.open('GET','http://localhost:3001/',true)
    request.setRequestHeader('Content-Type','application/json')
    request.send(JSON.stringify({value:'Hello Server'}))

}
...

刷新HTML,点击CORS请求按钮


preflighterror.png

又报错了,而且看Debug信息请求Method变成了OPTIONS,而不会指定的GET,这是为什么?

现在来看看preflight:
浏览器首先使用方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响

那么又是什么时候才需要发送预检请求呢?

当请求满足下述任一条件时,即应首先发送预检请求:

  • 使用了下面任一 HTTP 方法:
    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • 人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type的值不属于下列之一:
    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain

因为设置了Content-Type:application/json,所以浏览器发出了预检请求,就是Debug中看到OPTIONS请求了。

Access-Control-Request-Method

Access-Control-Request-Method 首部字段用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。

Access-Control-Request-Method: <method>

Access-Control-Request-Headers

Access-Control-Request-Headers 首部字段用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。

Access-Control-Request-Headers: <field-name>[, <field-name>]*

预检请求会使用上述两个请求头向服务端查询是否支持,服务端只需要使用相应的字段针对答复即可,该使用什么字段呢?

使用以下两个字段答复

Access-Control-Allow-Methods

Access-Control-Allow-Methods 首部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。

Access-Control-Allow-Methods: <method>[, <method>]*

Access-Control-Allow-Headers

Access-Control-Allow-Headers首部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段。

Access-Control-Allow-Headers: <field-name>[, <field-name>]*

修改后端跨域服务

//server.js

const HTTP = require('http')
let server = HTTP.createServer((request,response)=>{
    console.log('CORC Server Recieve Request', "\n Method : " , request.method , "\n Headers : ", request.headers , )
    if("OPTIONS" == request.method){
        /** 处理浏览器的预检请求 */
        response.writeHead(200,{
            'access-control-allow-methods':'GET,POST,OPTIONS',
            'access-control-allow-headers':'Content-Type',
            'access-control-allow-origin':"*"

        })
        response.end('CORS Sever OPTIONS Success Response')
    }else{
        response.writeHead(200,{
            'Content-Type':'text/plain',
            'access-control-allow-origin':"*"
        })
        response.end('CORS Sever Success Response')
    }
    
})
server.listen(3001)

后端处理预检请求,告诉它支持GET,POST,OPTIONS方法,Content-Type类型以及任何来源

再来测试:


prefilghtsuc.gif

按钮点击的时候,后端log输出:


corsserver.png

成功了~

第二步demo

跨域Cookie处理

web端的XMLHTTRequest和fetch默认都不会带上cookie信息,需要设置相应的参数以XMLHTTPRequest为例:

request.withCredentials = true /* true,带上cookie信息 */

修改HTML在中的代码,再测试:

function corsRequest(){
    document.cookie = "requestTimes=100"

    var request = new XMLHttpRequest()
    request.onreadystatechange = function(){
        if(request.DONE && request.status == 200 && request.readyState == 4) {
            var responseText = request.responseText
            console.log('response = ' , responseText)
            updateText(responseText)
        }
    }
    request.open('GET','http://localhost:3001/',true)
    request.setRequestHeader('Content-Type','application/json')
    request.withCredentials = true /* 主动设置,带上cookie信息 */
    request.send(JSON.stringify({value:'Hello Server'}))

}
1.png

这次的报错信息是说如果附带cookie,access-control-allow-origin就不能是通配型。不能使通配,服务端该怎么设置允许来源呢?

  • 已知来源,写死
  • 使用请求头中origin,获得来源

本例中就是用了origin

修改跨域服务

//server.js
const HTTP = require('http')
let requestTimes = 1;

let server = HTTP.createServer((request,response)=>{
    console.log('CORC Server Recieve Request', "\n Method : " , request.method , "\n Headers : ", request.headers , )
    if("OPTIONS" == request.method){
        /** 处理浏览器的预检请求 */
        response.writeHead(200,{
            'access-control-allow-methods':'GET,POST,OPTIONS',
            'access-control-allow-headers':'Content-Type',
            'access-control-allow-origin': request.headers['origin'],
        })
        response.end('CORS Sever OPTIONS Success Response')
    }else{
        response.writeHead(200,{
            'Content-Type':'text/plain',
            'access-control-allow-origin': request.headers['origin'],

        })
        response.end('CORS Sever Success Response')
    }
    
})
server.listen(3001)

重启服务,再次测试:


corserror1.png

又报错了,这次提示服务端必须设置access-control-allow-credentials:true

Access-Control-Allow-Credentials

Access-Control-Allow-Credentials头指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容。当用在对preflight预检测请求的响应中时,它指定了实际的请求是否可以使用credentials。

Access-Control-Allow-Credentials: true

修改跨域服务

//server.js
const HTTP = require('http')
let requestTimes = 1;

let server = HTTP.createServer((request,response)=>{
    console.log('CORC Server Recieve Request', "\n Method : " , request.method , "\n Headers : ", request.headers , )
    if("OPTIONS" == request.method){
        /** 处理浏览器的预检请求 */
        response.writeHead(200,{
            'access-control-allow-methods':'GET,POST,OPTIONS',
            'access-control-allow-headers':'Content-Type',
            'access-control-allow-origin': request.headers['origin'],
            'access-control-max-age':`${24*60*60}`,/** 一天,仅针对预检请求(preflight)有效 */
            'access-control-allow-credentials':true,
        })
        response.end('CORS Sever OPTIONS Success Response')
    }else{
        response.writeHead(200,{
            'Content-Type':'text/plain',
            'access-control-allow-origin': request.headers['origin'],
            'set-cookie':`requestTimes=${requestTimes++}`,
            'access-control-allow-credentials':true,
        })
        response.end('CORS Sever Success Response')
    }
    
})
server.listen(3001)

新增requestTimes,塞入cookie中,每次响应都++,用来观察cookie

重启服务,再次测试:


cookie.png

成功了,cookie也已经生效了

最终demo

再补充下

Access-Control-Max-Age

Access-Control-Max-Age头指定了preflight请求的结果能够被缓存多久。如demo中设置了一天,第二次再点击按钮时,不在发送preflight

Access-Control-Max-Age: <delta-seconds>

delta-seconds 参数表示preflight请求的结果在多少秒内有效。

最后总结下:

  • 浏览器在一定条件下会发送preflight预检请求,预检成功后,再发送真实请求,后端服务会收到两次请求
  • preflightOPTIONS请求,包含Access-Control-Request-Method,Access-Control-Request-Headers请求头。preflight是浏览器自动生成的,不需要手动设置请求头。
  • 跨域服务针对OPTIONS请求至少需要添加access-control-allow-methods,access-control-allow-headers,access-control-allow-origin等响应头。需要服务端处理
  • 如果需要附带cookie,跨域服务针对OPTIONS请求还需要添加access-control-allow-credentials响应头,并且access-control-allow-origin不能为通配型需要服务端处理
  • 真实服务响应头需要添加access-control-allow-origin,如果需要附带cookie,真实服务响应头还需要添加access-control-allow-credentials,并且access-control-allow-origin不能为通配型需要服务端处理
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容