1.同源策略
(1)何为同源策略
为什么首先讲同源策略,是因为跨域产生的原因是违背了同源策略
参考MDN同源的解释
同源策略是一个重要的安全策略,它用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
(2)同源的定义
如果两个 URL 的 protocol、port (en-US) (如果有指定的话)和 host 都相同的话,则这两个 URL 是同源。这个方案也被称为“协议/主机/端口元组”,或者直接是 “元组”。(“元组” 是指一组项目构成的整体,双重/三重/四重/五重/等的通用形式)。
下表给出了与 URL http://store.company.com/dir/page.html
的源进行对比的示例:
URL | 结果 | 原因 |
---|---|---|
http://store.company.com/dir2/other.html | 同源 | 只有路径不同 |
http://store.company.com/dir/inner/another.html | 同源 | 只有路径不同 |
https://store.company.com/secure.html | 失败 | 协议不同 |
http://store.company.com:81/dir/etc.html | 失败 | 端口不同 ( http:// 默认端口是80) |
http://news.company.com/dir/other.html | 失败 | 主机不同 |
2.如何解决跨域?
跨域问题的出现,并不是服务端没有返回响应的数据,是因为浏览器做了限制
(1)JSONP
jsonp原理
利用<script>标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的JSON数据。JSONP请求一定需要对方的服务器做支持才可以。
备注:
JSONP和AJAX对比 JSONP和AJAX相同,都是客户端向服务端发送请求,从服务端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
整个JSONP的实现过程
JSONP优缺点 JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅仅支持get方法具有局限性,不安全可能会遭受XSS攻击。
- 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)
- 创建一个 <script src=>标签 ,把那个跨域的API数据接口地址,赋值给script的src, 还要在这个地址中向服务器传递该函数名(可以通过问号传参?callback=show)。
- 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是 show('我喜欢乒乓球')。
- 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。
// 封装jsonp
jsonp.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=s, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
params = JSON.parse(JSON.stringify(params));
let arrs = [];
for (let key in params) {
arrs.push(`${key}=${params[key]}`);
}
arrs.push(`callback=${callback}`);
script.src = `${url}?${arrs.join('&')}`;
document.body.appendChild(script);
window[callback] = function (data) {
resolve(data);
document.body.removeChild(script);
}
})
}
// 前端调用
jsonp({
url: 'http://localhost:3000/hobby',
params: {
wd: 'I Love play pingpang'
},
callback: 'show'
}).then(data => {
console.log(data)
})
</script>
</body>
</html>
//后端代码 利用express起一个后端服务
server.js
// 这里用到了 express
var express = require('express');
var router = express.Router();
var app = express();
router.get('/hobby',function(req,res,next) {
//要响应回去的数据
let data = {
username : 'zhangsan',
password : 123456
}
let {wd , callback} = req.query;
console.log(wd);
console.log(callback);
// 调用回调函数 , 并响应
res.end(`${callback}(${JSON.stringify(data)})`);
})
app.use(router);
app.listen(3000);
利用node server 启动服务
(2)CORS(Cross-origin resource sharing)
CORS需要浏览器和后端同时支持。IE8和 IE9需要通过 XDomainRequest来实现
浏览器会自动进行 CORS通信,实现CORS通信的关键是后端。只要后端实现了CORS,实现了跨域。
服务端设置 Access-Control-Allow-Origin 就可以开启CORS。该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
虽然设置CORS和前端没有什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。
简单请求
只要同时满足以下两个条件,就属于简单请求
条件1 : 使用下列方法之一:
- GET
- HEAD
- POST
条件2 :Content-Type 的值仅限于下列三者之一 :
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;
复杂请求
不符合以上条件的请求就是复杂请求,复杂请求的CORS请求,在正式通信之前,会增加一次HTTP查询,称为"预检"请求,该请求是option方法 , 通过该请求来知道服务端是否允许跨域请求。
当用 PUT 向后台请求时, 属于复杂请求,后台需如下配置:
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
res.end()
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
console.log(req.headers)
res.end('我喜欢打乒乓球')
})
// 前端代码
cors.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
let xhr = new XMLHttpRequest();
document.cookie = 'name=hw';
xhr.withCredentials = true; //前端设置是否带 cookie
xhr.open('PUT','http://localhost:4000/getData',true);
xhr.setRequestHeader('name','hw');
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
console.log(JSON.parse(xhr.response));
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send();
</script>
</body>
</html>
// 给前端起一个端口服务
let express = require('express');
let app = express();
app.use(express.static(__dirname))
app.listen(3000)
// 后端代码
cors.js
// 后端代码
let express = require('express')
let app = express()
let whitList = ['http://127.0.0.1:3000'] //设置白名单
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {
// 设置哪个源可以访问我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end() // OPTIONS请求不做任何处理
}
}
next()
})
app.put('/getData', function(req, res) {
let data = {
username : 'zhangsan',
password : 123456
}
console.log(req.headers)
res.setHeader('name', 'jw') //返回一个响应头,后台需设置
res.end(JSON.stringify(data))
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('hhhhaha')
})
app.listen(4000)
//这个时候在浏览球器地址栏中,可以输入127.0.0.1:3000/cors.html
可以看到有两个getData请求
PUT请求
response Headers部分
Request Headers部分
如此就实现了一个从3000端口到4000端口的跨域请求访问
OPTIONS请求
(3) nginx解决跨域
在众多的解决跨域方式中,都不可避免的都需要服务端进行支持,使用Nginx可以纯前端解决请求跨域问题
server
{
listen 3002;
server_name localhost;
location /ok {
proxy_pass http://localhost:3000;
# 指定允许跨域的方法,*代表所有
add_header Access-Control-Allow-Methods *;
# 预检命令的缓存,如果不缓存每次会发送两次请求
add_header Access-Control-Max-Age 3600;
# 带cookie请求需要加上这个字段,并设置为true
add_header Access-Control-Allow-Credentials true;
# 表示允许这个域跨域调用(客户端发送请求的域名和端口)
# $http_origin动态获取请求客户端请求的域 不用*的原因是带cookie的请求不支持*号
add_header Access-Control-Allow-Origin $http_origin;
# 表示请求头的字段 动态获取
add_header Access-Control-Allow-Headers
$http_access_control_request_headers;
# OPTIONS预检命令,预检命令通过时才发送请求
# 检查请求的类型是不是预检命令
if ($request_method = OPTIONS){
return 200;
}
}
}