面试题信息:
题目分类: 网络&安全
面试频率:
涉及面试题:
- 了解跨域么?
- 跨域是由什么引起的呢?
- 怎么解决跨域问题呢?
- 浏览器为什么要阻止跨域请求?如何解决跨域?每次跨域请求都需要到达服务端吗?
面试知识点
什么是跨域
跨域问题其实就是浏览器的同源策略所导致的。
「同源策略」是一个重要的安全策略,它用于限制一个[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标准,全称是"跨域资源共享"(Cross-origin resource sharing)。
它允许浏览器向跨源服务器,发出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
简单请求实现
后端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请求就不会再展示了。
3、postMessage
「window.postMessage()」 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机 (两个页面的模数 [Document.domain]设置为相同的值) 时,这两个脚本才能相互通信。「window.postMessage()」 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。
用途
1.页面和其打开的新窗口的数据传递
2.多窗口之间消息传递
3.页面与嵌套的 iframe 消息传递
实现
a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
<script>
function load(){
let frame = document.getElementById('frame');
frame.contentWindow.postMessage('你好','http://localhost:4000/');
//接收
window.onmessage= function(e){
console.log(e.data)
}
}
</script>
b.html
window.onmessage = function(e){
console.log(e.data);
//发送
e.source.postMessage('hello',e.origin)
}
服务器都用node启动
4、document.domain
域名的关系
一级域名:www.baidu.com
二级域名:viode.baidu.com
原理
domain 属性可返回下载当前文档的服务器域名,domain 属性可以解决因同源安全策略带来的不同文档的属性共享问题。主要解决的是子域与父域之间的传值。就是告诉当前页面他们是同域的。
实现
1.修改host文件,模拟一级域名和二级域名
2.a.html
<iframe src="http://b.yq.cn:3000/b.html" frameborder="0" id="iframe" onload="load()"></iframe>
<script>
document.domain = 'yq.cn'
function load(){
let iframe = document.getElementById('iframe');
console.log(iframe.contentWindow.a)
}
</script>
3.b.html
document.domain = 'yq.cn'
window.a="hello,a"
5、window.name
原理
window.name这个属性不是一个简单的全局属性 --- 只要在一个window下,无论url怎么变化,只要设置好了window.name,那么后续就一直都不会改变,同理,在iframe中,即使url在变化,iframe中的window.name也是一个固定的值,利用这个,就可以实现跨域了。
a和b是同域的 http://localhost:3000
c是独立的 http://localhost:4000
a 获取c的数据
a先引用c c把值放到window.name
把a引用的地址改到b
实现
a.html
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true //为了ifrmae 改到b同域的时候再去改src,走死循环,需要去拿值
function load(){
if(first){
let iframe = document.getElementById('iframe');
iframe.src = 'http://localhost:3000/b.html';
first = false
}else{
console.log(iframe.contentWindow.name)
}
}
</script>
c.html
window.name= "a,你好"
6、location.hash
原理
路径后面的hash值可以用来通信
a和b是同域的 http://localhost:3000
c是独立的 http://localhost:4000
a 获取c的数据
a给c传一个hash值,c收到hash值后,c把hash传给b,b将结果传给ahash值中
实现
a.html
<iframe src="http://localhost:4000/c.html#Iloveyou" frameborder="0" id="iframe"></iframe>
c.html
console.log(location.hash)
let iframe = document.createElement('iframe')
iframe.src = 'http://localhost:3000/b.html#Idontloveyou'
document.body.appendChild(iframe)
b.html
window.parent.parent.location.hash = location.hash
最后再a.html 加上hash改变的事件
<script>
window.onhashchange = function(){
console.log(location.hash)
}
</script>
7、http-proxy
有很多其他代理工具和依赖,这边介绍最常用的:
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 安装
百度一下这个网上很多(因为我是mac)
### 安装依赖模块
yum -y install gcc gcc-c++ autoconf pcre pcre-devel make automake
yum -y install wget httpd-tools vim
#### CentOS下YUM安装
vi /etc/yum.repos.d/nginx.repo
#### 文件增加代码
[nginx]
name=nginx repo
baseurl=http://nginx.org/packages/centos/7/$basearch/
gpgcheck=0
enabled=1
#### 执行安装命令
yum install nginx -y //安装nginx
nginx -v //查看安装的版本
nginx -V //查看编译时的参数
查看nginx安装的配置文件和目录
rpm -ql nginx
#### 主配置文件
/etc/nginx/nginx.conf 核心配置文件
/etc/nginx/conf.d/default.conf 默认http服务器配置文件
/etc/nginx/fastcgi_params fastcgi配置
/etc/nginx/scgi_params scgi配置
/etc/nginx/uwsgi_params uwsgi配置
#### nginx模块目录
/etc/nginx/modules 最基本的共享库和内核模块
/usr/share/doc/nginx-1.14.2 帮助文档
/usr/share/doc/nginx-1.14.0/COPYRIGHT 版权声明
/usr/share/man/man8/nginx.8.gz 手册
/var/cache/nginx nginx的缓存目录
/var/log/nginx nginx的日志目录
/usr/sbin/nginx 可执行命令
/usr/sbin/nginx-debug 调试执行可执行命令
#### 配置文件
etc/nginx/nginx.conf #主配置文件
/etc/nginx/conf.d/*.conf #包含conf.d目录下面的所有配置文件
/etc/nginx/conf.d/default.conf
nginx.conf配置文件实现跨域
用location 匹配路径遇到json结尾的访问 /data/json的目录文件, 设置 配置允许请求跨域
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 教程