作者: 西瓜甜
配套视频: 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 无法读取。
- Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享
-
DOM 无法获得。
- 如果两个网页不同源,就无法拿到对方的DOM。典型的例子是
iframe
窗口和window.open
方法打开的窗口,它们与父窗口无法通信
- 如果两个网页不同源,就无法拿到对方的DOM。典型的例子是
-
AJAX 请求不能发送。
-
同源政策规定,AJAX请求只能发给同源的网址,否则就报错。
No 'Access-Control-Allow-Origin' header is present on the requested resource.
-
接下来,只讨论 AJAX 请求的跨域问题(见下文)。
二、跨域
1 什么是跨域
当从 A 网站的网页代码中 请求访问 B 网站中的数据资源的行为就成为跨域。
3 为何会产生跨域
目前主流的假设网站的技术都是采用前后端分离。
前端只负责静态资源的提供,提供此资源的服务器也称为前端服务器
后端只负责动态资源的提供,提供此资源的服务器也称为后端服务器
静态资源包含 html 页面,css 文件,js 文件, 图片等
动态资源就是 数据库中的纯数据。
比如用户的购物车中的商品,或者电商提供的产品的库存数据等。
一个完成的页面需要静态资源和动态资源的组合。
通常前端服务器会通过自己静态页面中的 JS 代码向后端服务器请求数据,
之后把请求到的数据,填充到自己的静态页面中,这个过程也可称为渲染。
在次过程中就会产生跨域的行为。
3.1 部署实验环境
192.168.1.37 Centos7 /Docker nginx 前端服务器
192.168.1.38 Centos7 nginx uwsgi 后端服务器
(同时具备反向代理功能)
部署 Nginx 不在讨论,相信既然了解跨域的知识了,部署 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;
}
- 192.168.1.37 的子配置文件
/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
3.1.1 uwsgi 介绍
uwsgi 可以启动一个提供动态资源的服务器,有相应的监听端口,支持 socket 和 http 形式。
目前支持多种编程语言,这里我们使用 python。
我们之前说动态资源的数据是存放在数据库中的,编程语言可以访问数据库。但是这里不会真的去连接数据库,这里会用假数据代替。不影响理解跨域。
3.1.2 部署 uwsgi
- 在 192.168.1.38 服务器上执行如下操作
安装依赖软件
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
3.2 启动服务
3.2.1 启动 uwsgi
后端服务器上执行
cd /opt/webapp
# 启动 uwsgi
nohup uwsgi qf-uwsgi.ini &
# 启动 nginx
systemctl start nginx
# 检查监听端口
ss -ntal |grep 80
4 解决 AJAX 跨域请求
解决跨域的方法很多,这里近介绍 Nginx 方式。
接下来会已以实际例子来模拟由于跨域访问导致的浏览器报错,之后通过在 Nginx 代理服务器上设置相应的参数来解决跨域。
从而让运维人员搞清楚什么是宽域,运维人员如何在服务端解决跨域。
先来说说解决 AJAX 跨域的解决方法
- JSONP
- WebSocket
- CORS
4.1 JSONP
是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。是最早解决的方法,目前已不常用。
4.2 WebSocket
WebSocket是一种通信协议,使用ws://
(非加密)和wss://
(加密)作为协议前缀。该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。 也不常用。
4.3 CORS
CORS 是跨源资源共享(Cross-Origin Resource Sharing)的缩写。
它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET
请求,CORS允许任何类型的请求。
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要使用浏览器的用户参与。
浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
4.4 两种请求
浏览器将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
浏览器对这两种请求的处理,是不一样的。
下面仅分析简单请求
4.5 简单请求
对于简单请求,浏览器直接发出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
字段。
4.6 解决简单请求的跨源访问
从上面的总结中可以看出,服务端解决跨域问题的最简单的方法是在服务器的响应头中添加 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
重启成功后,再次刷新网页
会看到响应头信息
页面中也会展示处理响应的数据
4.7 响应头信息的说明
如果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
里面指定。上面的例子指定,Access-Control-Expose-Headers: FooBar
可以返回FooBar
字段的值。
(4) Access-Control-Allow-Headers
Access-Control-Allow-Headers
用于 preflight request(预检请求)中,列出了将会在正式请求的 Access-Control-Request-Headers 字段中出现的首部信息。
通常用于允许客户端(一般是前端的 ajax 请求)在请求头中添加的自定义内容。
语法:
Access-Control-Allow-Headers: <header-name>[, <header-name>]*
Access-Control-Allow-Headers: *
*
(wildcard)
对于没有凭据的请求(没有 HTTP cookie 或 HTTP 认证信息的请求),值 *
仅作为特殊的通配符值。
但在具有凭据的请求中,它被视为没有特殊语义的文字标头名称 *
。
请注意,Authorization
标头不能使用通配符,并且始终需要明确列出。
示例说明:
看到下图中有如下报错现象
点击任意一个请求,查看请求头都有哪些内容。
可以看到保存内容是: Request header field app-id is not allowed by Access-Control-Allow-Headers in preflight response.
其含义是,请求头中包含了 app-id
这个字段,在服务端(这里指的是Nginx)Access-Control-Allow-Headers 设置的值中没有,所有不被允许。
下图是 Nginx 中配置的 Access-Control-Allow-Headers 的内容:
将 App-Id 字段添加到 Access-Control-Allow-Headers 的值中:↓
参考:
MDN Web Docs