同源策略
- 同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个 ip 地址,也非同源
- 同源策略 SOP(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,其初衷是为了浏览器的安全性,通过以下三种限制,保证浏览器不易受到 XSS、CSFR 等攻击。
- 同源策略限制了以下行为:
- (1)Cookie、LocalStorage 和 IndexDB 无法读取
- (2)DOM 和 Js 对象无法获得
- (3)AJAX 请求不能发送
- 同源策略请求:ajax/fetch
- 例:下面哪些不符合同源策略:(ABCDEF)
- A、
http://www.domain.com/a.js
和http://www.domain.com/lab/c.js
- 同一域名,不同文件或路径 允许通信
- B、
http://www.domain.com:8000/a.js
和http://www.domain.com/b.js
- 同一域名,不同端口 不允许通信
- C、
http://www.domain.com/a.js
和https://www.domain.com/b.js
- 同一域名,不同协议 不允许通信
- D、
http://www.domain.com/a.js
和http://domain.com/c.js
- //主域相同,子域不同 不允许通信
- E、
http://www.domain1.com/a.js
和http://www.domain2.com/b.js
- 不同域名 不允许通信
- F、
http://www.domain.com/a.js
和http://192.168.4.12/b.js
- 域名和域名对应相同 ip 不允许通信
- A、
跨域(非同源策略请求)
- 出于浏览器的同源策略限制,浏览器会拒绝跨域请求。(严格的说,浏览器并不是拒绝所有的跨域请求,实际上拒绝的是跨域的读操作。)
- 浏览器的同源限制策略是这样执行的:
- 通常浏览器允许进行跨域写操作(Cross-origin writes),如链接,重定向
- 通常浏览器允许跨域资源嵌入(Cross-origin embedding),如 img、script 标签
- 通常浏览器不允许跨域读操作(Cross-origin reads)
- 其他跨域情况:调用第三方平台的数据接口(跨域请求方式情况可能比同源更多)
跨域处理方式
1. JSONP
-
script
-
img
-
link
-
iframe
-
…
=>不存在跨域请求的限制,同源跨域均能拿到<!-- jsonp.html --> <!-- 跨域 成功--> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <!-- 同源 成功 --> <script src="./1.jsonp.js"></script
-
原理图
//客户端 $.ajax({ url: "http://127.0.0.1:8001/list", method: "get", dataType: "jsonp", //=>执行的是JSONP的请求 success: res => { console.log(res); } }); //服务器 node 文件名 执行 let express = require("express"), app = express(); //引入并执行 app.listen(8001, () => { console.log("OK!"); }); //监听端口8001 app.get("/list", (req, res) => { let { callback = Function.prototype } = req.query; //若callback为空执行异名空函数,若非空拿到func() let data = { code: 0, message: "hhh" }; res.send(`${callback}(${JSON.stringify(data)})`); });
- 优点:兼容性好(兼容低版本 IE)
- 缺点:
- JSONP 只支持 GET 请求(url 劫持不安全)
- XMLHttpRequest 相对于 JSONP 有着更好的错误处理机制
2. CORS 跨域资源共享
// 客户端正常发请求
<script src="../node_modules/axios/dist/axios.js"></script>
<script>
axios.get('http://127.0.0.1:3001/user/list')
.then(result=>{
console.log(result);
})
</script>
// 服务端
/* sever.js */
/*-MIDDLE MARE- 中间件*/
app.use((reg,res,next)=>{
const{
ALLOW_ORIGIN,
CREDENTIALS,
HEADERS,
ALLOW_METHODS
}=CONFIG.CROS;
res.header('Access-Control-Allow-Origin',ALLOW_ORIGIN);//允许头
res.header('Access-Control-Allow-Credentials',CREDENTIALS);
res.header('Access-Control-Allow-Headers',HEADERS);
res.header('Access-Control-Allow-Methods',ALLOW_METHODS);
req.method==='OPTIONS'?res.send('CURRENT SERVICE SUPPORT CROSS DOMAIN'});//真正发送之前发送的试探性请求,有结果再走真正的正式请求
app.use(session(CONFIG.SESSION));
app.use(bodyParser.urlencoded({
extended:false
]));
/* config.js */
module.exports={
//=>WEB服务端口号
PORT:3001,
//=>CROS跨域相关信息
CORS:{
ALLOW_ORIGIN:'http://127.0.0.1:5500',//允许的头
ALLOW_METHODS:'PUT,POST,GET,DELETE,OPTIONS,HEAD',//允许的请求方式
HEADERS:'Content-Type,Content-Length,Authorization,Accept ,X-Requested-With',//允许的请求头
CREDENTIALS:true//在跨域请求中是否允许携带资源凭证Cookie
},
//=>SESSION存储相关信息
SESSION:{
secret:'ZFPX',
saveUnintialized:false,
resave:false,
cookies:{
maxAge:1000*60*60*24*30
}
}
};
- 客户端(发送 ajax/fetch 请求)
axios.defaults.baseURL = "http://127.0.0.1:8888";
axios.defaults.withCredentials = true;
axios.defaults.headers["Content-Type"] = "application/x-www-form-urlencoded";
axios,
(defaults.transformRequest = function(data) {
if (!data) return data;
let result = ``;
for (let attr in data) {
if (!data.hasOwnProperty(attr)) break;
result += `&${attr}=${data[attr]}`;
}
return result.substring(1);
});
axios.interceptors.response.use(
function onFulfilled(response) {
return response.data;
},
function onRejected(reason) {
return Promise.reject(reason);
}
);
axios.defaults.validateStatus = function(status) {
return /^(2|3)\d{2}$/.test(status);
};
- 服务器端设置相关的头信息(需要处理 options 试探性请求)
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "http://localhost:8000"); //缺点:1.源只能写一个 2.*(就不能再允许携带Cookie了)具体地址
res.header("Access-Control-Allow-Credentials", true);
res.header(
"Content-Type,Content-Length,Authorization,Accept,X-Requested-With"
);
res.header(
"Access-Control-Allow-Methods",
"PUT,POST,GET,DELETE,HEAD,OPTIONS"
);
if (req.method === "OPTIONS") {
res.send("OK!");
return;
}
next();
});
3. http proxy =>webpack webpack-dev-server(常用)
import axios from "axios";
axios.get("/user/list").then(res => {
console.log(res);
});
// webpack.config.js
devServer:{
port:3000,//端口号
progress:true,//显示打包进度
contentBase:'./build',//指定静态资源访问的目录
proxy:{
'/':{
target:'http://127.0.0.1:3001',
changeOrigin:true//改变源,为true后dev-sever会做中层代理,相当于拿node写了个中间件
}
}
}
4. nginx 反向代理(后端服务器处理)
www.baidu.cn->www.baidu.com
#proxy服务器
server{
listen 80;
sever_name www.baidu.com;
location / {
proxy_pass www.baidu.cn;#反向代理
proxy_cookie_demo www.baidu.cn www.baidu.com;#处理cookie
add_header Access-Control-Allow-Origin www.baidu.cn;
add_header Access-Control-Allow-Credentials true;
}
}
5. postMessage
- window.postMessage() 方法可以安全地实现跨源通信,它提供了一种受控机制来规避同源策略限制。
- 语法:otherWindow.postMessage(message, targetOrigin, [transfer]);
- otherWindow(窗口)
- 其他窗口的一个引用,比如 iframe 的 contentWindow(嵌入的子窗口)属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames。
- message(要发的消息)
- 将要发送到其他 window 的数据。它将会被结构化克隆算法序列化.
- targetOrigin(接收的域)
- 通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串’*’(表示无限制)或者一个 URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。这个机制用来控制消息可以发送到哪些窗口
- transfer
- 和 message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
- otherWindow(窗口)
// server1.js node server1
let express = require("express"),
app = express();
app.listen(1001, () => {
console.log("OK!");
});
app.use(express.static("./"));
// server2.js node server2
let express = require("express"),
app = express();
app.listen(1002, () => {
console.log("OK!");
});
app.use(express.static("./"));
<!-- A.html -->
<iframe
id="iframe"
src="http://127.0.0.1:1002/MESSAGE/B.html"
frameborder="0"
style="display:none;"
></iframe>
<script>
iframe.onload = function() {
iframe.contentWindow.postMessage("hhh", "http://127.0.0.1:1002/");
};
// =>监听B传递的信息
window.onmessage = function(ev) {
console.log(ev.data);
};
</script>
<!-- B.html -->
<script>
// =>监听A发送过来的信息
window.onmessage = function(ev) {
console.log(ev.data);
// =>ev.source:A
ev.source.postMessage(ev.data + "@@@", "*");
};
</script>
B 输出 hhh A 输出 hhh@@@ A 客户端->B 服务器(+@@@)->A 客户端
6. WebSocket 协议(客户端与服务器实时通信的协议)跨域
- scoket.io 实现 客户端->服务器->客户端
<!-- 前端处理 -->
<script src="./socket.io.js"></script>
<script>
let socket=io('http://127.0.0.1:3001/');
//=>连接成功处理
socket.on('connect',function(){I
//=>监听服务端消息
socket.on('message',function(msg){
console.1og('data from server:'+msg);
});
//=>监听服务端关闭
socket.on('disconnect',function(){
console.log('server socket has closed!');
});
});
//=>发送消息给服务器端
socket.send("hhhh");
</script>
// 服务器端处理node express
//=>监听socket连接:server是服务器创建的服务 client客户端
socket.listen(server).on("connection", function(client) {
//=>接收信息
client.on("message", function(msg) {
// =>msg客户端传递的信息
client.send(msg + "@@");
});
//=>断开处理
client.on("disconnect", function() {
console.log("client socket has closed!");
});
});
7. document.domain+iframe
- 只能实现同一个主域不同子域之间的操作
- 父页面 A http://www.baidu.cn/A.html
<iframe src="http://www.baidu.cn/B.html"></iframe>
<script>
document.domain = "baidu.cn";
var user = "admin";
</script>
- 子页面 B http://www.baidu.cn/B.html
<script>
document.domain = "baidu.cn";
alert(window.parent.user);
</script>
8. window.name+iframe
- 服务器需要返回给 A 的信息都在 window.name 中存储着
- A 页面与 proxy 页面同源但与 B 不同源
页面 A
<iframe
src="http://127.0.0.1:1002/NAME/B.html"
id="iframe"
style="display:none;"
></iframe>
<script>
let count = 0;
iframe.onload = function() {
if (count === 0) {
// =>需要我们先把地址重新指向到同源中才可以
iframe.src = "http://127.0.0.1:1001/NAME/proxy.html";
count++;
return;
}
console.log(iframe.contentWindow.name);
};
</script>
let proxy = function(url, callback) {
let count = 8;
let iframe = document.createElement("iframe");
iframe.src = url;
iframe.onload = function() {
if (count === 0) {
iframe.contentWindow.location = "http://www.baidu.cn/proxy.html";
count++;
return;
}
callback(iframe.contentWindow.name);
};
document.body.appendChild(iframe);
};
页面 B(服务器端)
window.name='hhhhh';
9. location.hash+iframe
- A 和 C 同源,A 和 B 非同源
- A->B(发请求 hash 值,B 监听到)->C(监听到 B 传来的 HASH 值,操作同域的 A 的回调)->A
- 缺点:长度限制
页面 A
<iframe
id="iframe"
src="http://127.0.0.1:1002/B.html"
style="display:none;"
></iframe>
<script>
let iframe=document.getElementById('iframe');
//=>向B.html传hash值
iframe.onload=function(){
iframe.src='http://http://127.0.0.1:1002/B.html#msg=hhhhhh';
//=>开放给同域C.html的回调方法
function func(res){
alert(res);
}
</script>
页面 B
<iframe
id="iframe"
src="http://127.0.0.1:1001/C.html"
style="display:none;"
></iframe>
<script>
let iframe = document.getElementById("iframe");
//=>监听A传来的HASH值改变,再传给C.html
window.onhashchange = function() {
iframe.src = "http://127.0.0.1:1001/C.html" + location.hash;
};
</script>
页面 C
<script>
//=>监听B传来的HASH值
window.onhashchange = function() {
//=>再通过操作同域A的js回调,将结果传回
window.parent.parent.func(location.hash);
};
</script>
10. XMLHttpRequest
- 发送 json 类型数据
var request = new XMLHttpRequest();
request.open("post", "/address/dosomething");
var params = {
name: "张三",
age: 18
};
// 发json类型
request.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); //用POST的时候一定要有,缺少这句,后台无法获取参数
request.send(JSON.stringify(params));
request.onreadystatechange = function() {
if (request.readyState === 4 && request.status === 200) {
// dealData(request.responseText)
}
};
- 发送表单类型数据
var request = new XMLHttpRequest();
request.open("post", "/address/dosomething");
var params = "name=zhangsan&age=18";
// 发送表单类型
request.setRequestHeader(
"Content-Type",
"application/x-www-form-urlencoded;charset=UTF-8"
); //用POST的时候一定要有,缺少这句,后台无法获取参数
request.send(params);
request.onreadystatechange = function() {
if (request.readyState === 4 && request.status === 200) {
// dealData(request.responseText)
}
};
- 服务端(以 express4 框架为例)
const host = req.headers.host; // 服务器host(如:"100.84.164.64:4000")
const origin = req.headers.origin; // 发送http请求的机器的顶层域名(如:"http://192.168.22.40:4000")
res.set({
"Access-Control-Allow-Credentials": true, // 设置请求能携带cookie
"Access-Control-Allow-Origin": origin // 仅允许http://foo.example域名下的请求跨域,允许所有域名访问
//'Access-Control-Allow-Origin': '*', // 允许所有域名访问,不安全
//'Access-Control-Allow-Origin': 'http://foo.example', // 仅允许http://foo.example域名下的请求跨域
});
11.node 做中间件代理
- 安装代理模块
npm i http-proxy-middleware --save
const express = require("express");
const app = express();
/* 代理配置 start */
const proxy = require("http-proxy-middleware"); //引入代理模块
const proxyOptions = {
target: "http:/127.0.0.1:9999", //后端服务器地址
changeOrigin: true //处理跨域
};
const exampleProxy = proxy("/api/*", proxyOptions); //api前缀的请求都走代理
app.use(exampleProxy);
/* 代理配置 end */
const hostName = "127.0.0.1";
const port = 8080;
app.get("/", function(req, res) {
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<button id="btn1">请求服务器接口1</button>
<button id="btn2">请求服务器接口2</button>
<script src="https://cdn.bootcss.com/axios/0.19.0/axios.min.js"></script>
<script>
document.getElementById('btn1').addEventListener(
'click',
() => {
axios.get('/api/hello', {
params: {
key: 'hello'
}
});
},
false
);
document.getElementById('btn2').addEventListener(
'click',
() => {
axios.get('/api/word', {
params: {
key: 'word'
}
});
},
false
);
</script>
</body>
</html>`;
res.setHeader("Content-Type", "text/html");
res.send(html);
});
app.listen(port, hostName, function() {
console.log(`服务器运行在http://${hostName}:${port}`);
});