作者: 西瓜甜
配套视频: https://www.bilibili.com/video/BV1v7411Z7FU
Netscape Communications Corporation(最初是Mosaic Communications Corporation)网景是一家独立的美国计算机服务公司,总部位于加利福尼亚州的山景城,然后在弗吉尼亚州的杜勒斯。[2] 它的Netscape Web浏览器曾经一度占主导地位,但在所谓的第一次浏览器大战之后输给了Internet Explorer和其他竞争者,其市场份额从1990年代中期的90%以上下降到2002年的不足1%。 2006. Netscape创建了JavaScript编程语言,这是用于客户端网页脚本的最广泛使用的语言。该公司还开发了SSL,用于在其后继TLS接管之前保护在线通信的安全。
Netscape Navigator 2.0 是网景公司的旗舰产品,是第一个支持JavaScript 和 gif 动画的浏览器。
在1995年Netscape Navigator 2.02版本中引入了同源策略的概念,目前,所有浏览器都实行这个策略。。
在计算中,同源策略(有时缩写为SOP)是Web应用程序安全模型中的重要概念。可见同源策略是为了 Web 的安全出现的产物。
根据该策略,Web浏览器只允许第一个网页中包含的脚本(JS)访问第二个网页中的数据时,两个网页具有相同的来源。
何为相同的来源,两个网页的:
协议(http/https)相同
域名(IP)相同
端口 相同
举例来说 假设要某一台服务器提供的一个资源,这个资源的 url 是 http://www.sharkyun.com/api/json
协议是: http://
域名是: www.sharkyun.com
端口是: 80(默认端口可不写)
那从如下url 的网页访问 http://www.sharkyun.com/api/json
时的情况如下
http://www.sharkyun.com/user/:同源
http://sharkyun.com/dir/other.html:不同源(域名不同)
http://www.qfedu.com/topic/linux/:不同源(域名不同)
https://www.sharkyun.com/dir/other.html:不同源(协议不同)
http://www.sharkyun.com:81/dir/other.html:不同源(端口不同)
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。
此策略可防止一个页面上的恶意脚本(JavaScript 语言编写的脚本程序)通过该页面的文档对象模型来访问另一网页上的敏感数据。
由此可见,同源政策是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。
请记住,同源策略仅适用于 JavaScript 脚本,这一点非常重要。
换句话说,同源策略不适用于 HTML 标签,比如:
<img src="">
<link rel="stylesheet" type="text/css" href="">
<script type="text/javascript"></script>
<iframe src=""></iframe>
这意味着可以一个网站可以通过网页上相应的HTML标签进行跨源访问另外一个网站的诸如图片,CSS和 JS之类的静态资源。
Cookie、LocalStorage 和 IndexDB 无法读取。
DOM 无法获得。
iframe
窗口和window.open
方法打开的窗口,它们与父窗口无法通信AJAX 请求不能发送。
同源政策规定,AJAX请求只能发给同源的网址,否则就报错。
No 'Access-Control-Allow-Origin' header is present on the requested resource.
接下来,只讨论 AJAX 请求的跨域问题(见下文)。
当从 A 网站的网页代码中 请求访问 B 网站中的数据资源的行为就成为跨域。
目前主流的假设网站的技术都是采用前后端分离。
前端只负责静态资源的提供,提供此资源的服务器也称为前端服务器
后端只负责动态资源的提供,提供此资源的服务器也称为后端服务器
静态资源包含 html 页面,css 文件,js 文件, 图片等
动态资源就是 数据库中的纯数据。
比如用户的购物车中的商品,或者电商提供的产品的库存数据等。
一个完成的页面需要静态资源和动态资源的组合。
通常前端服务器会通过自己静态页面中的 JS 代码向后端服务器请求数据,
之后把请求到的数据,填充到自己的静态页面中,这个过程也可称为渲染。
在次过程中就会产生跨域的行为。
192.168.1.37 Centos7 /Docker nginx 前端服务器
192.168.1.38 Centos7 nginx uwsgi 后端服务器
? (同时具备反向代理功能)
部署 Nginx 不在讨论,相信既然了解跨域的知识了,部署 Nginx 应该不是问题。
下面只说一下每个服务器的配置文件和页面内容
/etc/nginx/nginx.conf
都一致
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
/etc/nginx/conf.d/default.conf
基本按照默认的即可
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
192.168.1.37 添加如下内容到 /usr/share/nginx/html/index.html
文件中,作为网站的首页内容。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title> 欢迎来到 shark yun</title>
<script
src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
</head>
<body>
<div id=json></div>
<!-- img 标签直接跨域访问静态资源-->
<img src="http://192.168.1.38/qfnz.jpg">
</body>
<script type="text/javascript">
// AJAX 跨域请求
$.ajax({
url: 'http://192.168.1.38/api/json',
type: 'GET',
dataType: 'json',
success: function(res){
// 转换为字符串
data=JSON.stringify(res)
// 添加数据到 页面的 div 标签中
$("#json").text(data);
},
error: function(res){
console.error(res);
}
});
</script>
</html>
192.168.1.38 的子配置文件 /etc/nginx/conf.d/default.conf
server {
listen 80;
server_name localhost;
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:8000;
}
location ~* \.(gif|jpg|jpeg|js)$ {
root /static; # 需要创建对应的目录
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
在 192.168.1.38 服务上创建静态资源存放的目录
mkdir /static # 注意和上面配置文件中的一致
传输示例图片到 192.168.1.38 服务器的 /static
目录中, 并命名为 qfnz.jpg
uwsgi 可以启动一个提供动态资源的服务器,有相应的监听端口,支持 socket 和 http 形式。
目前支持多种编程语言,这里我们使用 python。
我们之前说动态资源的数据是存放在数据库中的,编程语言可以访问数据库。但是这里不会真的去连接数据库,这里会用假数据代替。不影响理解跨域。
安装依赖软件
yum install epel-release python2-devel python2-pip
使用 pip
安装 uwsgi
pip install uwsgi
创建应用程序目录
mkdir /opt/webapp
进入应用程序目录并创建 应用程序文件 app.py
, 添加如下内容
[root@localhost webapp]# cat /opt/webapp/app.py
headers=('Content-Type', 'application/json;charset=utf-8')
def application(env, start_response):
if env['PATH_INFO'] == '/api/json':
start_response('200 OK', [headers])
data = '{"name": "shark", "age": 18}'
return [data]
继续在应用程序目录创建 uwsgi 的配置文件 qf-uwsgi.ini
, 添加如下内容
[root@localhost webapp]# cat /opt/webapp/qf-uwsgi.ini
[uwsgi]
# 监听本地端口 8000
socket = 0.0.0.0:8000
# 进入到应用程序(app)的主目录
chdir = /opt/webapp/
# 指定app 的启动文件
wsgi-file = app.py
#开启 4 个进程
processes = 4
# 每个进程开启 2 个线程
threads = 2
后端服务器上执行
cd /opt/webapp
# 启动 uwsgi
nohup uwsgi qf-uwsgi.ini &
# 启动 nginx
systemctl start nginx
# 检查监听端口
ss -ntal |grep 80
解决跨域的方法很多,这里近介绍 Nginx 方式。
接下来会已以实际例子来模拟由于跨域访问导致的浏览器报错,之后通过在 Nginx 代理服务器上设置相应的参数来解决跨域。
从而让运维人员搞清楚什么是宽域,运维人员如何在服务端解决跨域。
先来说说解决 AJAX 跨域的解决方法
是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。是最早解决的方法,目前已不常用。
WebSocket是一种通信协议,使用ws://
(非加密)和wss://
(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。 也不常用。
CORS 是跨源资源共享(Cross-Origin Resource Sharing)的缩写。
它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET
请求,CORS允许任何类型的请求。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要使用浏览器的用户参与。
浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
- 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
- HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
浏览器对这两种请求的处理,是不一样的。
下面仅分析简单请求
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin
字段。
目前可以在任意一个浏览器中输入 http://192.168.1.37
之后右键 点击 检查
在浏览器下方 点击 Network
再次刷新 浏览器, 并点击 json
这个 json
资源的请求就是第一次访问 前端服务器时返回的首页中的 JS 代码发送的一次跨域请求。
再次点击 Network
并在 右侧窗口的底部 会看到 Request Headers
(请求头)
会发现 在请求头中有一字段 Origin
这个字段的值表明的此次请求是从那发出来的,就是说明这次请求的源是哪儿:协议 + 域名 + 端口
可以看到图片中的源是 :http://192.168.1.37
这个地址正式我们这个页面的服务器地址。
但是此次请求的目标并不是 http://192.168.1.37 而是 192.168.1.38
可以从这个窗口的最上方内容中看到
从下图信息中可以看的出来,
这次请求的资源 json
的 url 为:http://192.168.1.38/api/json
域名是192.168.1.38
但是请求头中的 Origin
字段的值是 192.168.1.37
这就是跨源(跨域)访问
前面说了, CORS需要浏览器和服务器同时支持。
服务器接收到请求,从请求头中会拿到这个 Origin
的值。
服务器可以根据自己的配置,来确定是否要返回此次请求的数据。
如果Origin
指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应,此时的响应码可能是 200
。也就是不可以从响应的状态码来判断跨域请求是否成功。
当浏览器接收到服务器的响应信息,查看响应头。
会发现,这个响应的头信息没有包含Access-Control-Allow-Origin
字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest
的onerror
回调函数捕获。
报错信息如下
总结: 浏览器 CORS 跨源请求是否被允许,浏览器判断的是服务器响应头中是否含有 Access-Control-Allow-Origin
字段。
从上面的总结中可以看出,服务端解决跨域问题的最简单的方法是在服务器的响应头中添加 Access-Control-Allow-Origin
字段。
此时我们可以在后端服务器(192.168.1.38) 中的自配置文件default.conf
添加如下内容 :
add_header Access-Control-Allow-Origin *;
允许任何源发送请求
add_header Access-Control-Allow-Origin *;
也可以指定具体的一个源
add_header Access-Control-Allow-Origin http://192.168.1.37
server {
listen 80;
server_name localhost;
# 添加响应头信息
add_header Access-Control-Allow-Origin *;
location / {
include uwsgi_params;
uwsgi_pass 127.0.0.1:8000;
}
location ~* \.(gif|jpg|jpeg|js)$ {
root /static; # 需要创建对应的目录
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
接下来重启 nginx 服务
systemctl restart nginx
重启成功后,再次刷新网页
会看到响应头信息
页面中也会展示处理响应的数据
如果Origin
指定的域名在许可范围内,根据服务器端不同的设置,服务器返回的响应,可能会多出几个头信息字段。
Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8
上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-
开头。
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin
字段的值,要么是一个*
,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true
,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true
,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest
对象的getResponseHeader()
方法只能拿到6个基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。如果想拿到其他字段,就必须在Access-Control-Expose-Headers
里面指定。上面的例子指定,getResponseHeader('FooBar')
可以返回FooBar
字段的值。