涉及面试题:
- 了解跨域么?
- 跨域是由什么引起的呢?
- 怎么解决跨域问题呢?
- 浏览器为什么要阻止跨域请求?如何解决跨域?每次跨域请求都需要到达服务端吗?
面试知识点
什么是跨域
跨域问题其实就是浏览器的同源策略所导致的。
「同源策略」是一个重要的安全策略,它用于限制一个[origin]的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。 --来源 MDN
当跨域的时候就会收到如下图这样的报错:
同源策略
什么是同源
「protocol(协议)、domain(域名)、port(端口)三者一致。」 一致的情况下我们叫同源。
同源示例
为什么浏览器不支持跨域
假设如今有a.com和b.com两个域,若是没有这一安全策略,那么当用户在访问a.com时,a.com的一段脚本就能够在不加载b.com的页面而随意修改或者获取b.com上面的内容。这样将会致使b.com页面的页面发生混乱,甚至信息被获取,包括服务器端发来的session。这样的话,web世界将是一片混乱。也是由于浏览器的同源策略,保证来至不一样源的对象不会互相干扰,保证了咱们访问页面最基本的安全。
9种解决方案
1、jsonp
JSONP是JSON with Padding的略称。它是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,通过javascript callback的形式实现跨域访问(这仅仅是JSONP简单的实现形式)。 由于同源策略限制,XMLHttpRequest只允许请求同源的资源,为了实现跨域请求,可以通过script标签来实现跨域请求,然后服务器输出json数据并执行回调函数,从而解决跨域请求。 href、src 都不受同源策略的限制。
原理
首先在客户端注册一个callback,然后把callback的名字传给服务器。此时,服务器先生成json数据,然后以JavaScript的语法方式,生成function,function的名字就是传递上来带参数的jsonp,最后将json数据直接以入参的方式,放置在function中,这样子就生成JavaScript语法文档,返回给客户端。客户端浏览器,通过解析,并执行返回JavaScript文档,此时数据作为参数,传入到客户端预先定义好的callback函数中,简单地说,就是利用script标签没有跨域限制地漏洞来达到第三方通讯的目的。
实现
function jsonp({url,params,cb}){
return new Promise((resolve, reject)=>{
window[cb] = function(data){
console.log(data)
resolve(data);
document.body.removeChild(script);
}
params= {...params,cb}
let arrs = [];
for (let key in params){
arrs.push(`${key}=${params[key]}`)
}
let script = document.createElement('script');
script.src = `${url}?${arrs.join('&')}`;
script.onerror = () => reject('加载失败')
document.body.appendChild(script);
})
}
jsonp({
url:"http://localhost:3000/users",
params:{name:"jin",age:12},
cb:'show'
}).then(data=>{
console.log(data)
})
复制代码
后端接口实现:
let express = require('express');
let app = express();
app.get('/users', function(req, res, next) {
// 模拟的数据
let {name,age,cb} = req.query
let data = `"${name}现在${age}岁"`
res.send(`${cb}(${data})`);
});
app.listen(3000)
复制代码
优缺点
1.优点
- 它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制,JSONP可以跨越同源策略;
- 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持
- 在请求完毕后可以通过调用callback的方式回传结果。将回调方法的权限给了调用方。这个就相当于将controller层和view层终于分开了。我提供的jsonp服务只提供纯服务的数据,至于提供服务以 后的页面渲染和后续view操作都由调用者来自己定义就好了。如果有两个页面需要渲染同一份数据,你们只需要有不同的渲染逻辑就可以了,逻辑都可以使用同 一个jsonp服务。
2.缺点
- 它只支持GET请求而不支持POST等其它类型的HTTP请求
- 它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。
- jsonp在调用失败的时候不会返回各种HTTP状态码。
- 缺点是安全性。万一假如提供jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的。那么结果是什么?所有调用这个 jsonp的网站都会存在漏洞。于是无法把危险控制在一个域名下,所以在使用jsonp的时候必须要保证使用的jsonp服务必须是安全可信的。
2、cors
CORS是一个W3C标准,全称是"跨域资源共享"
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple reque)。
属于简单请求条件,满足两大条件:
(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
对于简单请求,浏览器向服务器发起的request header中,会有一个origin字段,表示当前的“源”,服务器返回的response header中,会有一个Access-Control-Allow-Origin字段,里面写明允许那些“源”,浏览器发现两者一致,或者服务器允许所有的“源”,那么跨域成功!
简单请求实现
后端node-express 代码: 页面服务器挂3000端口,页面启动地址http://localhost:3000/index.html
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
复制代码
请求发送4000端口实现跨域:
let express = require('express');
let app = express();
app.get('/getData', function(req, res, next) {
console.log(req.headers)
res.send("你拿不到数据了!");
});
app.listen(4000)
复制代码
正常发请发送到http://localhost:4000/getData
var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
httpRequest.open('GET', 'http://localhost:4000/getData', true);//第二步:打开连接
httpRequest.send();//第三步:发送请求 将请求参数写在URL中
httpRequest.onreadystatechange = function () {
if (httpRequest.readyState == 4) {
if( httpRequest.status >= 200 && httpRequest.status <300 || httpRequest.status === 304 ){
var json = httpRequest.responseText;//获取到json字符串,还需解析
console.log(json);
}
}
};
复制代码
因为端口号不同就跨域了,如下图:
其实请求是发过去了,但是确实被浏览器屏蔽了结果.如果Origin
指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin
字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest
的onerror
回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如下图:
非简单请求实现
请求代码如下:
var httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
document.cookie='name=jin'; //设置cookie
httpRequest.withCredentials = true;//允许请求携带cookie
httpRequest.open('POST', 'http://localhost:4000/getData', true);//第二步:打开连接
httpRequest.setRequestHeader('name','Jack');
httpRequest.send();//第三步:发送请求 将请求参数写在URL中
httpRequest.onreadystatechange = function () {
if (httpRequest.readyState == 4) {
if( httpRequest.status >= 200 && httpRequest.status <300 || httpRequest.status === 304 ){
var json = httpRequest.responseText;
console.log(json);
}
}
};
复制代码
后端node代码:
let express = require('express');
let app = express();
app.all('*', function (req, res, next) {
let origin = req.headers.origin
//设置哪个源可以访问我
res.header("Access-Control-Allow-Origin",origin);
// 允许携带哪个头访问我
res.header("Access-Control-Allow-Headers", "name");
// 允许哪个方法访问我
res.header("Access-Control-Allow-Methods", "POST");
// 允许携带cookie
res.set("Access-Control-Allow-Credentials", true);
// 预检的存活时间
res.header("Access-Control-Max-Age", 6);
// 允许前端获取哪个头
res.header("Access-Control-Expose-Headers", "name");
// 请求头的格式
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
app.post('/getData', function(req, res, next) {
console.log(req.headers)
res.send("你拿不到数据了!");
});
app.listen(4000)
复制代码
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。 代码修改一下:
// 在请求头的设置中加上
if(req.method ==='OPTIONS'){
res.end();//OPTIONS请求不做任何处理
}
复制代码
这时候,OPTIONS请求就不会再展示了。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Proxy 代理对象使用详解即原理总结
我的理解就是:Proxy就是使用者和目标对象之间的一个中间商,使用者和对象之间的操作需要中间商代为完成,并且在完成使用者对对象的操作的前提下,中间商可以多加一点操作达到自己的目的。
重点
Proxy 只能代理对象。
代理指的是对一个对象基本语义的代理。读取和设置对象的属性都是基本语义,因为它们是一步就可以完成的操作。而调用一个对象内的函数就不是基本操作,因为它需要两步操作,第一步先拿到函数属性,第二步执行。
不能用 instanceof 判断代理对象的类型,因为 Proxy.prototype 是 undefined。
代理也可以代理另外一个代理形成多层拦截,如下例子:
let obj = {name:'孤城浪人'}
let proxy = new Proxy(obj,{
get(target,property){
console.log('第一层代理触发');
return target[property];
}
});
let proxy1 = new Proxy(proxy,{
get(target,property){
console.log('第二层代理触发');
return target[property];
}
});
console.log(proxy1.name);
有很多其他代理工具和依赖,这边介绍最常用的:
1)Webpack (4.x)
在webpack
中可以配置proxy
来快速获得接口代理的能力。
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: {
index: "./index.js"
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
devServer: {
port: 8000,
proxy: {
"/api": {
target: "http://localhost:8080"
}
}
},
plugins: [
new HtmlWebpackPlugin({
filename: "index.html",
template: "webpack.html"
})
]
};
复制代码
2) Vue-cli 2.x
// config/index.js
...
proxyTable: {
'/api': {
target: 'http://localhost:8080',
}
},
...
复制代码
3) Vue-cli 3.x
// vue.config.js 如果没有就新建
module.exports = {
devServer: {
port: 8000,
proxy: {
"/api": {
target: "http://localhost:8080"
}
}
}
};
————————————————
8、nginx
Nginx 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。Nginx 采用的是多进程(单线程)和多路IO复用模型
nginx应用场景
- 静态资源服务器
- 反向代理服务
- API接口服务(Lua&Javascript)
nginx优势
- 高并发高性能
- 可扩展性好
- 高可靠性
- 热布署
- 开源许可证
nginx 安装
nginx.conf配置文件实现跨域
用location 匹配路径遇到json结尾的访问 /data/json的目录文件, 设置add_header\color{#FF0000}{ add\_header}add_header 配置允许请求跨域
server { # 每个server对应一个网站
listen 80; # 监听的端口号
server_name localhost; #域名
access_log off;
# 有些指令可以支持正则表达式
location / { #匹配所有的路径
root /usr/share/nginx/html; #静态文件根目录
index index.html index.htm; #索引文档
}
location ~ .*\.json$ {
add_header Access-Control-Allow-Origin http://localhost:3000;
add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS;
root /data/json;
}
}
复制代码
nginx反向代理都可以实现跨域。代理的思路为,利用服务端请求不会跨域的特性,让接口和当前站点同域。
location ~ ^/api {
proxy_pass http://localhost:3000;
proxy_redirect default; #重定向
proxy_set_header Host $http_host; #向后传递头信息
proxy_set_header X-Real-IP $remote_addr; #把真实IP传给应用服务器
proxy_connect_timeout 30; #默认超时时间
proxy_send_timeout 60; # 发送超时
proxy_read_timeout 60; # 读取超时
proxy_buffering on; # 在proxy_buffering 开启的情况下,Nginx将会尽可能的读取所有的upstream端传输的数据到buffer,直到proxy_buffers设置的所有buffer们 被写满或者数据被读取完(EOF)
proxy_buffers 4 128k; # proxy_buffers由缓冲区数量和缓冲区大小组成的。总的大小为number*size
proxy_busy_buffers_size 256k; # proxy_busy_buffers_size不是独立的空间,他是proxy_buffers和proxy_buffer_size的一部分。nginx会在没有完全读完后端响应的时候就开始向客户端传送数据,所以它会划出一部分缓冲区来专门向客户端传送数据(这部分的大小是由proxy_busy_buffers_size来控制的,建议为proxy_buffers中单个缓冲区大小的2倍),然后它继续从后端取数据,缓冲区满了之后就写到磁盘的临时文件中。
proxy_buffer_size 32k; # 用来存储upstream端response的header
proxy_max_temp_file_size 256k; # response的内容很大的 话,Nginx会接收并把他们写入到temp_file里去,大小由proxy_max_temp_file_size控制。如果busy的buffer 传输完了会从temp_file里面接着读数据,直到传输完毕。
}
复制代码
9、websocket
WebSocket 是一种网络通信协议,很多高级功能都需要它。
特点:服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws
(如果加密,则为wss
),服务器网址就是 URL。
实现
html
//
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function(){
socket.send('I love you')
}
socket.onmessage = function (e){
console.log(e.data)
}
复制代码
服务器使用node,用ws的依赖
let express = require('express');
let app = express();
let WebSocket = require('ws');
let wss = new WebSocket.Server({port:3000})
wss.on('connection',function(ws){
ws.on('message',function(data){
console.log(data)
wx.send('server data');//服务器发送消息
})
})
app.listen(4000)
复制代码
这部分有兴趣的朋友可以看阮一峰老师的WebSocket
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++