初探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




    
    
    HTTPCORS
    


    

this is index.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 参数的值指定了允许访问该资源的外域 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: 

Access-Control-Request-Headers

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

Access-Control-Request-Headers: [, ]*

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

使用以下两个字段答复

Access-Control-Allow-Methods

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

Access-Control-Allow-Methods: [, ]*

Access-Control-Allow-Headers

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

Access-Control-Allow-Headers: [, ]*

修改后端跨域服务

//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 参数表示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不能为通配型需要服务端处理

你可能感兴趣的:(初探CORS)