跨域

5,161 阅读11分钟

1、什么是跨域?

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域,不同域之间相互请求资源,就算作“跨域”。因为JavaScript出于安全考虑,有同源策略。

有一点必须要注意:跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

那么是出于什么安全考虑才会引入这种机制呢

其实主要是用来防止 CSRF 攻击的。简单点说,CSRF 攻击是利用用户的登录态发起恶意请求。

也就是说,没有同源策略的情况下,A 网站可以被任意其他来源的 Ajax 访问到内容。如果你当前 A 网站还存在登录态,那么对方就可以通过 Ajax 获得你的任何信息。当然跨域并不能完全阻止 CSRF。

然后我们来考虑一个问题,请求跨域了,那么请求到底发出去没有? 请求必然是发出去了,但是浏览器拦截了响应。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会。因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

2、如何跨域

常用的跨域方法有JSONPCORSpostmessage等,

(1) JSONP

JSONPJSON 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 89 需要通过 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.comb.test.com 适用于该方式。

只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域。 修改document.domain的方法只适用于不同子域的框架间的交互。

(4) postMessage

window.postMessage(message,targetOrigin) 方法是html5新引进的特性,可以使用它来向其它的window对象发送消息,无论这个window对象是属于同源或不同源,目前IE8+FireFoxChromeOpera等浏览器都已经支持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

windowname属性有个特征:

在一个窗口(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代码和做一些调整。

注明一下,整篇文章为学习笔记,多方参考总结,如有版权冲突,请留言,收到消息后会标明版权出处。