初探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 (强烈推荐)
- 创建根目录server
mkdir server && cd server
npm init -y
npm install express --save
/* 我使用的版本是"express": "^4.16.3" */
- 创建src
mkdir src
当前的目录结构:
➜ server ls
node_modules package-lock.json package.json src
-
src下创建HTML和HTML Server
src.png
- 配置好基础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)
- 启动服务,查看成果
➜ server cd src/htmlserver
➜ htmlserver node app.js
html.png
- 配置跨域测试服务
//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机制
见证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
成功了~
跨域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也已经生效了
再补充下
Access-Control-Max-Age
Access-Control-Max-Age头指定了preflight请求的结果能够被缓存多久。如demo中设置了一天,第二次再点击按钮时,不在发送preflight
Access-Control-Max-Age: <delta-seconds>
delta-seconds
参数表示preflight请求的结果在多少秒内有效。
最后总结下:
- 浏览器在一定条件下会发送preflight预检请求,预检成功后,再发送真实请求,后端服务会收到两次请求
-
preflight是OPTIONS请求,包含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不能为通配型。
需要服务端处理