1、什么是跨域?
当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域,不同域之间相互请求资源,就算作“跨域”。因为JavaScript出于安全考虑,有同源策略。
有一点必须要注意:跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
那么是出于什么安全考虑才会引入这种机制呢?
其实主要是用来防止 CSRF 攻击的。简单点说,CSRF 攻击是利用用户的登录态发起恶意请求。
也就是说,没有同源策略的情况下,A 网站可以被任意其他来源的 Ajax 访问到内容。如果你当前 A 网站还存在登录态,那么对方就可以通过 Ajax 获得你的任何信息。当然跨域并不能完全阻止 CSRF。
然后我们来考虑一个问题,请求跨域了,那么请求到底发出去没有? 请求必然是发出去了,但是浏览器拦截了响应。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会。因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。
2、如何跨域
常用的跨域方法有JSONP
,CORS
,postmessage
等,
(1) JSONP
JSONP
是JSON with padding
的简写,是应用JSON
的一种新方法,JSONP
看起来和JSON
差不多,只不过是被包含在函数调用的JSON
,像这样:callback({name: 'nany'})
。
JSONP跨域原理
通过<script>
标签引入一个js
文件,这个js
文件载成功后会执行我们在url
参数中指定的函数,并且会把我们需要的json
数据作为参数传入,jsonp
是需要服务器端配合的。
前端:
<script>
function getPrice(data){
console.log(data);
}
</script>
<script
type="text/javascript"
src="http://sdffw.b2b.com/getSupplyPrice?callback=getPrice&bcid=47296567">
</script>
后端:
const url = require('url');
require('http').createServer((req, res) => {
const data = {};
const callback = url.parse(req.url, true).query.callback ;
res.writeHead(200)
res.end(`${callback}(${JSON.stringify(data)})`)
*// 服务器收到请求后,解析参数,*
*// 将callback(data)以字符串的形式返还数据,前端页面会将callback(data)作为js执行*
*// 调用jsonpCallback(data)函数。*
}).listen(3000, '127.0.0.1');
callback
是前后台约定的查询参数,服务器端返回一个能执行的js
文件,这个js
文件是调用callback
对应的参数值即getPrice
执行,并且返回对应的数据,我们可以在getPrice
方法里面来处理返回的数据,最终返回的结果如下:
getPrice({
"data":{"priceType":"0","unit":"斤"},
"message":"价格获取成功!!!",
"state":"1"
})
在开发中可能会遇到多个JSONP
请求的回调函数名是相同的,这时候就需要自己封装一个JSONP
,以下是简单实现:
function jsonp(url, jsonpCallback, success) {
let script = document.createElement('script')
script.src = url
script.async = true
script.type = 'text/javascript'
window[jsonpCallback] = function(data) {
success && success(data)
}
document.body.appendChild(script)
}
jsonp('http://xxx', 'callback', function(value) {
console.log(value)
})
JSONP
跨域不像下面的CORS
跨域那样受同源政策的影响,而且兼容性也比较好,但JSONP
跨域也有其缺点,主要表现在:
- 它支持
GET
请求而不支持POST
等其它类行的HTTP
请求。 - 它只支持跨域
HTTP
请求这种情况,不能解决不同域的两个页面或iframe
之间进行数据通信的问题。 JSONP
从其他域中加载代码执行,如果该域不安全并且夹带一些恶意代码,会存在安全隐患 要确定JSONP
请求是否失败并不容易
(2) CORS
CORS
需要浏览器和后端同时支持。IE 8
和 9
需要通过 XDomainRequest
来实现。
浏览器会自动进行 CORS
通信,实现 CORS
通信的关键是后端。只要后端实现了 CORS
,就实现了跨域。服务端设置 Access-Control-Allow-Origin
就可以开启 CORS
。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
虽然设置 CORS
和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。
简单请求
以 Ajax
为例,当满足以下条件时,会触发简单请求
使用下列方法之一:
GET
HEAD
POST
Content-Type 的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
复杂请求
那么很显然,不符合以上条件的请求就肯定是复杂请求了。
对于复杂请求来说,首先会发起一个预检请求,该请求是option
方法的,通过该请求来知道服务端是否允许跨域请求。对于预检请求来说,如果你使用过Node
来设置CORS
的话,可能会遇到过这么一个坑。
以下以 express 框架举例:
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS')
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, Authorization, Access-Control-Allow-Credentials'
)
next()
})
该请求会验证你的Authorization
字段,没有的话就会报错。
当前端发起了复杂请求后,你会发现就算你代码是正确的,返回结果也永远是报错的。因为预检请求也会进入回调中,也会触发next()
方法,因为预检请求并不包含Authorization
字段,所以服务端会报错。
想解决这个问题很简单,只需要在回调中过滤option
方法即可。
res.statusCode = 204
res.setHeader('Content-Length', '0')
res.end()
CORS 的优缺点:
- 使用简单方便,更为安全
- 支持
POST
请求方式, CORS
是一种新型的跨域问题的解决方案,存在兼容问题,仅支持IE 10
以上
(3) 降域(document.domain)
这种方式只能用于二级域名相同的情况下,比如a.test.com
和 b.test.com
适用于该方式。
只需要给页面添加 document.domain = 'test.com'
表示二级域名都相同就可以实现跨域。
修改document.domain
的方法只适用于不同子域的框架间的交互。
(4) postMessage
window.postMessage(message,targetOrigin)
方法是html5
新引进的特性,可以使用它来向其它的window
对象发送消息,无论这个window
对象是属于同源或不同源,目前IE8+
、FireFox
、Chrome
、Opera
等浏览器都已经支持window.postMessage
方法。
调用postMessage
方法的window
对象是指要接收消息的那一个window
对象,该方法的第一个参数message
为要发送的消息,类型只能为字符串;第二个参数targetOrigin
用来限定接收消息的那个window
对象所在的域,如果不想限定域,可以使用通配符 *
。
需要接收消息的window
对象,可是通过监听自身的message
事件来获取传过来的消息,消息内容储存在该事件对象的data
属性中。
// 发送消息端
window.parent.postMessage('message', 'http://test.com')
// 接收消息端
var mc = new MessageChannel()
mc.addEventListener('message', event => {
var origin = event.origin || event.originalEvent.origin
if (origin === 'http://test.com') {
console.log('验证通过')
}
})
(5) window.name
window
的name
属性有个特征:
在一个窗口(window)
的生命周期内,窗口载入的所有的页面都是共享一个window.name
,每个页面对window.name
都有读写的权限,window.name
是持久存在一个窗口载入过的所有页面中的,并不会因新页面的载入而进行重置。
(6) WebSockets
Websockets原理:
在JS创建了web socket
之后,会有一个HTTP
请求发送到浏览器以发起连接。取得服务器响应后,建立的连接会使用HTTP
升级从HTTP
协议交换为web sockt
协议。
前端<script>
部分
let socket = new WebSocket("ws://localhost:8080");
socket.onopen = function() {
socket.send("秋风的笔记");
};
socket.onmessage = function(e) {
console.log(e.data);
};
后端部分
const WebSocket = require("ws");
const server = new WebSocket.Server({ port: 8080 });
server.on("connection", function(socket) {
socket.on("message", function(data) {
socket.send(data);
});
});
(7) 在webpack中可以配置proxy来快速获得接口代理的能力。
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: {
index: "./index.js"
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
devServer: {
port: 8000,
proxy: {
"/api": {
target: "http://localhost:8080"
}
}
},
plugins: [
new HtmlWebpackPlugin({
filename: "index.html",
template: "webpack.html"
})
]};
补一下脑:
1、CSRF攻击原理
CSRF(Cross site request forgery)
,即跨站请求伪造。我们知道XSS是跨站脚本攻击,就是在用户的浏览器中执行攻击者的脚本,来获得其cookie
等信息。而CSRF
确是借用用户的身份,向web server
发送请求,因为该请求不是用户本意,所以称为“跨站请求伪造”。
CSRF
一般的攻击过程是,攻击者向目标网站注入一个恶意的CSRF
攻击URL
地址(跨站url)
,当(登录)用户访问某特定网页时,如果用户点击了该URL
,那么攻击就触发了,我们可以在该恶意的url
对应的网页中,利用 来向目标网站发生一个get
请求,该请求会携带cookie
信息,所以也就借用了用户的身份,也就是伪造了一个请求,该请求可以是目标网站中的用户有权限访问的任意请求。也可以使用javascript
构造一个提交表单的post
请求。比如构造一个转账的post
请求。
所以CSRF
的攻击分为了两步,首先要注入恶意URL
地址,然后在该地址中写入攻击代码,利用 等标签或者使用Javascript
脚本
1.1 CSRF防御
(1) referer
因为伪造的请求一般是从第三方网站发起的,所以第一个防御方法就是判断referer
头,如果不是来自本网站的请求,就判定为CSRF
攻击。但是该方法只能防御跨站的CSRF
攻击,不能防御同站的CSRF
攻击(虽然同站的CSRF
更难)。
(2) 使用验证码
每一个重要的post
提交页面,使用一个验证码,因为第三方网站是无法获得验证码的。还有使用手机验证码,比如转账是使用的手机验证码。
(3) 使用 token
每一个网页包含一个web server
产生的token
,提交时,也将该token
提交到服务器,服务器进行判断,如果token
不对,就判定位CSRF
攻击。
将敏感操作get
改为post
,然后在表单中使用token
. 尽量使用post
也有利于防御CSRF
攻击。
2、你了解 CSP 吗?
CSP
全称 Content Security Policy
,内容安全策略。
为了页面内容安全(跨域获取资源,又能防止恶意代码)而制定的一系列防护策略。
通过csp我们可以制定一系列的策略,从而只允许我们页面向我们允许的域名发起跨域请求,而不符合我们策略的恶意攻击则被挡在门外。
2.1 如何使用csp策略:
参考文章 csp有两种方式指定: HTTP Header 和 HTML
2.1.1 通过 HTTP Header来定义
"Content-Security-Policy: 策略集"
csp语法
每一条策略都是都是由指令
和指令值
组成
例如: Content-Security-Policy:default-src 'self';
策略与策略之间用分号隔开
例如: Content-Security-Policy:default-src 'self';script-src 'www.a.com'
指令 | 说明 | |
---|---|---|
default-src | 定义针对所有类型(js/image/css/font/ajax/iframe/多媒体等)资源的默认加载策略,如果某类型资源没有单独定义策略,就使用默认的。 | |
script-src | 定义针对 JavaScript 的加载策略。 | |
style-src | 定义针对样式的加载策略。 | |
img-src | 定义针对图片的加载策略。 | |
font-src | 定义针对字体的加载策略。 | |
media-src | 定义针对多媒体的加载策略,例如:音频标签<audio> 和视频标签<video> | |
object-src | 定义针对插件的加载策略,例如:<object> 、<embed> 、<applet> | |
child-src | 定义针对框架的加载策略,例如: <frame> ,<iframe> 。 | |
connect-src | 定义针对 Ajax/WebSocket 。 | 等请求的加载策略。不允许的情况下,浏览器会模拟一个状态为400的响应。 |
sandbox | 定义针对 sandbox 的限制,相当于 <iframe> 的sandbox 属性。 | |
report-uri | 告诉浏览器如果请求的资源不被策略允许时,往哪个地址提交日志信息。 | |
form-action | 定义针对提交的 form 到特定来源的加载策略。 | |
referrer | 定义针对 referrer 的加载策略。 | |
reflected-xss | 定义针对 XSS 过滤器使用策略。 |
指令值 | 说明 |
---|---|
* | 允许加载任何内容 |
none | 不允许加载任何内容 |
self | 允许加载相同源的内容 |
www.a.com | 允许加载指定域名的资源 |
*.a.com | 允许加载a.com任何子域名的资源 |
a.com | 允许加载a.com的https资源 |
https: | 允许加载https资源 |
data: | 允许加载data: 协议,例如: base64编码的图片 |
unsafe-inline | 允许加载 inline 资源,例如style属性、onclick、inline js、inline css等 |
unsafe-eval | 允许加载动态 js 代码,例如 eval() |
2.1.2 通过 html meta标签使用:
<meta http-equiv="content-security-policy" content="策略集">
CSP虽然提供了强大的安全保护,但是他也造成了如下问题:
- Eval及相关函数被禁用
- 内嵌的JavaScript代码将不会执行
- 只能通过白名单来加载远程脚本
这些问题阻碍CSP的普及,如果要使用CSP技术保护自己的网站,开发者就不得不花费大量时间分离内嵌的JavaScript代码和做一些调整。
注明一下,整篇文章为学习笔记,多方参考总结,如有版权冲突,请留言,收到消息后会标明版权出处。