简单跨域请求
- method:
-
- HEAD
- GET
- POST
- content-type
-
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- 不能超过所列请求头信息:
-
- Accept
- Accept-Language
- Content-Language
- HEAD
- GET
- POST
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
- Accept
- Accept-Language
- Content-Language
如果完全满足上面的条件,就是简单请求,只需要设置返回头Access-Control-Allow-Origin为“*”或者是对应请求头origin 就能实现跨域请求。
在node koa可以通过这么做
ctx.set('Access-Control-Allow-Origin', ctx.request.headers.origin);
复杂跨域请求
只要不符合上面任何一项条件,就是复杂的跨域请求
复杂的跨域请求情况:
- 除了简单的跨域请求的方法,比如PUT、DELETE
- 除了简单的跨域请求的Content-type类型。 比如application/json
- 自定义的header头
复杂请求会先进行options预请求, 对后台设置对应的允许返回头
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials
进行验证,通过了才发起复杂请求的实际请求。
请注意:如果前端ajax请求是不带有withCredentials=true的复杂请求,那么Access-Control-Allow-Methods,Access-Control-Allow-Origin 或 Access-Control-Allow-Headers可以设置为“*”
如果是简单跨域请求,那么前端发送请求,后台接受请求是有返回数据的,只是被浏览器拦截掉了,并且浏览器抛出错误; 如果是复杂跨域请求,那么在options预请求的时候,浏览器会跟上面的一样报错,然后不发送后面的实际请求;
cookie跨域
如果前端请求需要跨域携带cookie,那么需要设置xhr.withCredentials=true,而上面三个返回头都要设置对应的具体值,否则是不生效的,并且只有同时设置返回头Access-Control-Allow-Credentials:true, 那么cookie跨域传输才会成功。
cookie跨域也讲究同源
无论是前端设置cookie:document.cookie='name=jgchen;'
(以分号+空格 进行分割) 还是后端设置返回头set-cookie,都要客户端跟服务端配合, 两者缺一不可,浏览器请求设置withCredentials=true,同时服务端设置返回头 Access-Control-Allow-Credentials:true
提高复杂请求的效率
为了避免每次都进行复杂请求都有options请求,从而造成不必要的性能问题,返回头设置 Access-Control-Max-Age:秒数, 表示 preflight request (预检请求)的返回结果缓存时间,如Access-Control-Allow-Methods 和Access-Control-Allow-Headers能被浏览器缓存多久,验证过第一次之后,在这个时间内不再发起options预请求
前端如何获取返回头信息
前端如何拿到返回头信息呢?可以通过xhr.getResponseHeader方法进行获取。 但是此方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。
其他自定义返回头属性,如node设置的:ctx.response.setHeader(name,'jgchen'),
需要同时设置 ctx.response.setHeader('Access-Control-Expose-Headers', 'name') 后,前端才能拿到name这样的自定义属性,否则就会出现
Refused to get unsafe header "name" // 拒绝获取不安全的头信息“name”
简单跨域通过什么进行拦截?
请求头中的Origin:request请求头中的Origin指示了请求来自于哪个站点,并且只有在CORS的情况下origin才会在请求头中出现。
请求头中的host:Host 请求头指明了请求的服务器的域名(对于虚拟主机来说),以及(可选的)服务器监听的TCP端口号。如果没有给定端口号,会自动使用被请求服务的默认端口(比如请求一个HTTP的URL会自动使用80端口)
HTTP/1.1 的所有请求报文中必须包含一个Host头字段。如果一个 HTTP/1.1 请求缺少Host 头字段或者设置了超过一个的 Host 头字段,一个400(Bad Request)状态码会被返回。
响应头中的Access-Control-Allow-Origin:该响应头字段指定了该响应的资源是否被允许与前台请求头给定的origin共享。
所以简单跨域请求返回浏览器之后,虽然数据会返回,但是,浏览器会比对请求头中的Origin与响应头中的Access-Control-Allow-Origin是否是共享匹配。如果不匹配,浏览器的xhr会捕获错误并且在浏览器端控制台抛出错误,并不能拿到期望的数据。
复杂跨域请求如何检测跨域的?
预请求实现机制
对于复杂的请求跨域, 浏览器一旦检测此发送的请求头存属于复杂的跨域请求时, 首先会发送一个预请求options, 请求头中包函着以下重要的内容:
Access-Control-Request-Headers(如果有自定义头或者content-type类形不属于简单请求的类型的情况下才会出来)
Access-Control-Request-Method(除了简单的请求方法才会出现)
此时服务端需要让这两个请求头中对应的信息通过允许。通过在响应返回的时候对响应头做出响应处理:设置 Access-Control-Allow-Methods和Access-Control-Allow-Headers 为对应的字段,这样预请求才会成功,并且存下来,在接下里的实际请求中带上这些字段作为请求头。
在预检请求时并不会把请求数据和cookie信息带入请求信息中。 只有预请求成功之后,才会进行实际请求,带入上cookie跟请求数据
预请求虽然好,但是如何避免性能问题
通过以上的所有对复杂的跨域请求的分析清楚的认识到,那些复杂跨域请求方式每次都会发送预检请求,然后通过后再来发送实际的请求。我们可以把第一次预检请求的成功结果缓存下来,这样在后面的指定时间内发送这个请求,都不会再触发预检请求来,可以通过设置Access-Control-Max-Age 【表示 preflight request (预检请求)的返回结果的返回头(即 Access-Control-Allow-Methods 和Access-Control-Allow-Headers 提供的信息) 可以被缓存多久】
如何对预检请求设置有效期
ctx.setHeader('Access-Control-Max-Age', 600)
对预检请求设置10分钟的过期时间(时间可以根据项目情况进行自定义)
在这次预检请求返回成功的10分钟内,发送这个复杂跨域请求都不会触发预检请求。 但是对于每个浏览器的缓存时间机制都不一样。在本地调试的时候,有时候你会发现设置了预检的过期时间并不生效。注意一下可能开启了浏览器的Disable cache导致了此原因
通过代理劫持解决跨域问题
在各大框架中都通过脚手架启动node服务承载着项目。例如vue-cli中就利用了http-proxy-middle进行一个请求的代理拦截,向目标服务器发送请求来解决跨域问题。
代理劫持示例
假如前端起了个locahost:3000的服务
当前的前端请求http://localhost:3000/api/getUser接口
相关代码如下
<script>
let url = '/api/getUser';
let xhr = new XMLHttpRequest();
xhr.open('post', url, true);
xhr.setRequestHeader('content-type', 'application/json');
xhr.setRequestHeader('X-Customer-Header', 'value');
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(1)
console.log(xhr.response)
}
}
}
</script>
// 但是在前端使用的脚手架用到的http-proxy-middle模块,做了代理劫持
const proxyOption = {
target: 'http://localhost:4000',
pathRewrite: {
'^/api/' : '/' // 重写请求,api/解析为/
},
changeOrigin:true
};
app.use('/api', proxy(proxyOption))
// 后台启动4000端口服务
app.post('/getUser', (req, res, next) => {
res.send({
code: 1
})
})
当3000端口的前端发送ajax请求‘/api/getUser’的时候,本身就是在一个域名下,由于不会造成任何跨域问题,同时http-proxy-middle 启的cli server服务,app.use('/api/')捕获拦截,改写url地址向服务端4000端进行请求发送数据,此时就是cli server端与 api server端的请求通信。当4000端口的api server接收到请求之后把数据返回给3000端口的cli server端,同时再返回给请求的ajax客户端
用原生node api 实现代理劫持
const http=require('http')
app.use('/api', (req, res) => {
const reqHttp = http.request({
host: '127.0.0.1',
path: '/getUser',
port: '4000',
method: req.method,
headers: req.headers
}, (resHttp) => {
//http模块请求回调
let body = ''
resHttp.on('data', (chunk) => {
console.log(chunk.toString())
body += chunk
});
resHttp.on('end', () => {
res.end(body)
});
})
reqHttp.end()
});
以上代码本质上是模拟了代理劫持的方式,当拦截到url开头以/api起始的请求之后,通过node原生http模块的request方法向对应的后台发送请求,同时把浏览器请求过来的一些请求体,请求头等数据一并传给server端。通过http模块监听的结束方法最后把数据再返回到client浏览器端。这样形成了二次转发方式解决跨域问题。整体就是利用了服务端向服务发送请求不会有跨域策略的限制,就是所谓的同源策略。
因为浏览器会做options等预检的检测,跟跨域返回结果的拦截,而服务端并不会。
实战
前端示例
普通跨域,复杂跨域,cookie跨域,自定义请求头,获取自定义响应头,jsonp demo
function formateData(data) {
var arr = [];
//for(let val of [1,2,3]) 是对数组值进行遍历,只有数组可以用
//for (let key in [1,2,3]或者{a:1,b:2}) 数组取的是索引,对象取的是属性
for (let key in data) {
arr.push(encodeURIComponent(key) + '=' + data[key])
}
return arr.join('&');
}
/*
ajax 实现的大概思路是 取一个params的对象参数,对里面的data进行序列化处理,
formatData函数序列化处理便于GET请求携带变量参数 ,或者POST等其他请求的请求头设置为Content-type:application/x-www-form-urlencoded,请求体内容进行序列化
当POST方法时候 send(请求体内容)
当GET方法时候 xhr.open(方法,地址+参数,异步boolean)
设置onreadystatechange函数回调或者onload函数毁掉
*/
function ajax(params) {
params = params || {};
params.data = params.data || {};
var xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
params.method = (params.method || 'GET').toUpperCase();
xhr.withCredentials = true;
if (params.method === 'GET') {
xhr.open(params.method, params.url + '?' + formateData(params.data), true)
// document.cookie = 'name=jgchen; password=123; ';
xhr.send();
} else {
xhr.open(params.method, params.url, true);
// xhr.setRequestHeader("name", "jgchen")
/* 设置 Content-Type 为 application/x-www-form-urlencoded
以表单urlencoded的形式传递数据
使用 xhr.send('username=admin&password=root')这样的格式
*/
// xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
// xhr.send(formateData(params.data));
/*
设置Content-Type:为application/json,以json格式传递数据
这种会触发复杂跨域,需要后台配置ctx.set('Access-Control-Allow-Header','content-type')
对params.data不能调用formdata进行格式化,要进行JSON.stringfiy(params.data)
使用xhr.send({"username":"admin","password":"root"})
*/
xhr.setRequestHeader("Content-type", "application/json")
xhr.setRequestHeader("request-header", "test-my-header")
xhr.send(JSON.stringify(params.data));
}
// readyState有五种可能的值:
// 0 (未初始化): (XMLHttpRequest)对象已经创建,但还没有调用open()方法。
// 1 (载入):已经调用open() 方法,但尚未发送请求。
// 2 (载入完成): 请求已经发送完成。
// 3 (交互):可以接收到部分响应数据。
// 4 (完成):已经接收到了全部数据,并且连接已经关闭。
//当然我们可以用onload来代替onreadystatechange等于4的情况,因为onload只在状态为4的时候才被调用,代码如下:
// 这里有两种写法,第一种写法:当xhr.readyState===4的时候,会触发onload事件,直接通过onload事件 进行回调函数处理
//对于xhr.responseText 进行JSON。parse解析 并且 调用parms。success.call(xhr,res) 对于返回数据传递到成功回调函数
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status <= 206 || xhr.status === 304) {
var res;
if (params.success && params.success instanceof Function) {
res = JSON.parse(xhr.responseText);
console.log(xhr.getResponseHeader('keyword'))
params.success.call(xhr, res);
}
} else {
if (params.error && params.error instanceof Function) {
res = xhr.responseText;
params.error.call(xhr, res);
}
}
}
//第二种写法,当xhr.readyState===4时候,说明请求成功返回了,进行成功回调
// xhr.onreadystatechange = function () {
// // console.log(xhr.readyState)
// if (xhr.readyState === 4) {
// // 进行onload里面的处理函数
// }
// }
}
//跨域jsonp请求
//jsonp请求的第一步需要取一个params对象的jsonp属性的值作为callbackname的值,callback参数的值为callbackname
//然后取到head节点,创建script标签,
function jsonp(params) {
//先对params进行处理,防止为空
params = params || {};
params.data = params.data || {};
//后台传递数据时调用的函数名
var callbackName = params.jsonp;
// 拿到dom元素head,先不进行操作
var head = document.querySelector('head');
//创建script元素,先不进行操作
var script = document.createElement('script');
//传递给后台的data数据中,需要包含回调参数callback。
//callback的值是 一个回调函数的函数名,后台通过该回调函数名调用传递数据,这个参数名的key由双方约定,默认为callback
params.data['callback'] = callbackName;
//对data数据进行格式化
var data = formateData(params.data);
//设置script请求的url跟数据
script.src = `${params.url}?${data}`;
//全局函数 由script请求后台,被调用的函数,只有后台成功响应才会调用该函数
window[callbackName] = function (jsonData) {
//请求移除scipt标签
head.removeChild(script);
clearTimeout(script.timer);
window[callbackName] = null;
params.success && params.success(jsonData)
}
//请求超时的处理函数
if (params.time) {
script.timer = setTimeout(() => {
//请求超时对window下的[callbackName]函数进行清除,由于有可能下次callbackName发生改变了
window[callbackName] = null;
//移除script元素,无论请求成不成功
head.removeChild(script)
//这里不需要清除定时器了,clearTimeout(script.timer); 因为定时器调用之后就被清除了
//调用失败回调
params.error && params.error({
message: '超时'
})
}, time);
}
//往head元素插入script元素,这个时候,script就插入文档中了,请求并加载src
head.appendChild(script);
//无论是请求超时,还是请求成功,都要移除script元素,script元素只有在第一次插入页面文档的时候,才会请求src
//无论请求失败还是成功,都还是要移除window[callbackName]避免增加没用的全局方法,因为每次请求的callbackName可能是不同的
//之前有个无聊的问题:为啥jsonp只能是get请求呢?看了实现过程,知道其实是因为script的加载就是get方式的~
}
node 示例
const Koa = require('koa');
const Router = require('koa-router');
// const cors = require('koa2-cors');
const koaBody = require('koa-body');
const app = new Koa;
let home = Router();
/*
*/
app.use(async(ctx,next)=>{
ctx.set('Access-Control-Allow-Origin', ctx.request.headers.origin);
ctx.set('Access-Control-Allow-Methods', 'PUT');
ctx.set('Access-Control-Expose-Headers','keyword')
ctx.set('Access-Control-Allow-Credentials',true)
ctx.set('keyword','jgchen')
ctx.set('Access-Control-Allow-Headers','request-header,content-type');
await next();
})
// app.use(cors());
app.use(koaBody())
home.get('/', async (ctx) => {
return ctx.body = {
code: 200,
message: '这个是首页'
}
})
home.get('/ajax', async (ctx) => {
console.log(ctx.cookies.get('name'))
ctx.cookies.set('password','qqqqqq')
console.log('get called')
return ctx.body = {
code: 200,
data: ctx.request.query
}
})
home.post('/ajax', async (ctx) => {
console.log('post called')
return ctx.body = {
code: 200,
data: ctx.request.body
}
})
home.put('/ajax', async (ctx) => {
console.log('put called')
return ctx.body = {
code: 200,
data: ctx.request.body
}
})
home.get('/jsonp', async (ctx) => {
console.log('jsonp called')
let callbackName = ctx.request.query.callback;
let data = {
code: 200,
data: ctx.request.query
}
//返回体直接是函数调用,调用的实参是要后台要传递的数据~由于data是对象,需要先进行json格式化
// console.log(`${callbackName}(${JSON.stringify(data)})`)
//script标签加载会解析json内容 默认进行了json.parse
return ctx.body = `${callbackName}(${JSON.stringify(data)})`
})
app.use(home.routes());
app.use(home.allowedMethods())
app.listen(3000, () => {
console.log('start');
})
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。
在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。
本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。
除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。
在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!