用node原生模块实现cors和jsonp

2,003 阅读5分钟

前言

做过前端开发的基本多多少少都遇到过一些跨域的问题,目前来说跨域最常见的两种解决方案就是cors和jsonp(这里不讨论iframe和其他一些解决跨域问题的方案)。

最近自己用nodejs的原生模块实现了一下这两种跨域方式的服务端,主要还是为了加深对于这两种方式的理解。

创建一个nodejs服务器

首先我们使用nodejs提供的原生http模块创建一个http的服务器。

const http = require('http')
const server = http.createServer((req, res) => {
	// 处理浏览器来的请求
    res.write('hello world')
    res.end()
})

server.listen(8001)

这就是一个简单的服务器了,监听了8001端口。

jsonp的实现

在了解如何实现jsonp之前我们先得了解jsonp到底是个啥?

我们的ajax请求通常会受到浏览器的跨域限制,但是script脚本里的js资源是不会受到同源限制的(其实凡是拥有”src”这个属性的标签都拥有跨域的能力,比如<\script>、<\img>、<\iframe>)。

所以我们虽然不能通过ajax的方式来请求服务器的数据,但是我们可以通过向服务器请求js脚本的方式来获取数据,jsonp就是这样一种方式。

jsonp的请求url中通常会包含自己的一个注册在全局的函数名,比如/jsonp?callback=globalCallback(可能叫callback,或者jsonpCallback,这个得看服务器具体要求的是什么),服务端会返回一个js脚本文件,这个文件里会调用你传给服务端的全局函数(globalCallback),函数的入参就是服务端想要返回给你的数据。

知道了原理我们接下来就来实现一下:

const http = require('http')
const URL = require('url')
const qs = require('querystring')
const server = http.createServer((req, res) => {
	// 从req对象获取请求的url和方式
	const { method, url } = req
    const urlObj = URL.parse(url)
    const queryObj = qs.parse(urlObj.query)
    if (method === 'GET' && urlObj.pathname === '/jsonp') {
    	// 判断浏览器是否有携带回调函数名
        if (!queryObj.callback) {
            res.statusCode = 404
            res.write('fail! jsonp need callback query')
        } else {
        	// 设置对应的content-type
            res.setHeader('content-type', 'application/javascript')
            // 将回调函数和要返回的数据组合在一起,写入response
            res.write(`${queryObj.callback}(${JSON.stringify(mockData)})`)
        }
    }
    
    res.end()
})

上面代码用到的urlquerystring都是node自带的包,主要是为了分析url,获取里面的参数,你也可以通过正则表达式以及其他手段获取。

至于浏览器端如何去调用这个接口,只需要通过一个script标签即可。

<script>
	function globalJsonpCallback(data) {
  		// 在这里可以用data进行你想要的操作
  	}
</script>
<script src="http://xxxx.com/jsonp/callback=globalJsonpCallback"></script>

npm社区也有现成的模块化方案,通过jsonp这个npm包你就可以方便的在你的js代码里去调用jsonp的接口,就像你调用正常的ajax一样,其余的这个npm包都已经帮你做好了。

cors的实现

关于cors的原理以及其他的这里就不讲了,我讲了也不一定有别人讲得好,这里放上一篇我觉得还不错的文章[译] 理解 CORS,这篇文章提到了浏览器为什么要实现同源策略以及cors的一些请求header。

当你已经登录 www.yourbank.com 的情况下,攻击者在各种网站上植入的脚本(比如通过 Google Ads 显示的广告)向 www.yourbank.com 发起的携带 你的身份凭证 的 AJAX 请求。

上面是文章中的例子,如果浏览器没有同源策略,脚本向www.yourbank.com发出的请求就会成功,可能就会带给你无法估量的损失。

因为cors可以分为简单请求和复杂请求(关于简单请求和复杂请求可以看这篇文章CORS跨域请求[简单请求与复杂请求])。

对于复杂请求浏览器会以OPTIONS形式发送一个预检请求来请求权限,预检请求通过后才会发送正式的请求。而对于简单请求则是直接发送真实的请求。

那为什么要增加这样一个预检请求呢?我曾经在面试的时候被问到过这个问题。

首先你需要明确的是无论是cors还是同源策略,都是浏览器的策略。服务器一般会根据cookie中的一些信息来校验用户的身份,身份通过了服务器就会进行接下来的操作。也就是说,即使我在B站点向A站点发送一个请求,A站点的服务器也会正确的响应我的请求(只要我的请求是对的),浏览器其实会收到服务器的响应,这代表服务器已经处理完你的请求了,但是浏览器接收到响应的时候发现如果出现跨域,则会去校验Access-Control-Allow-Origin等一系列http header来确定服务器是否允许来自当B站点的跨域请求,如果不允许,那么你js里的ajax调用就会失败。

注意上面的过程,如果没有OPTIONS预检请求,那么我可能直接发送一些对用户数据有破坏性的请求(比如转账、关注xxx),虽然在我浏览器的js代码中这个请求失败了,但是在服务器端,这个操作已经完成。

所以在有预检请求的情况下,预检请求就不会通过,后续的请求也就不会继续发送,也就不会对用户的数据造成影响了。

我们接下来看看如何用nodejs实现cors。

其实对于服务器来说,只要给请求设置对应的http response header就行了。


function setCorsHeader(res) {
    res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8082')
    res.setHeader('Access-Control-Allow-Credentials', true)
    res.setHeader('Access-Control-Allow-Headers', 'customer-header, customer-header2')
    res.setHeader('Access-Control-Allow-Methods', 'POST, PUT')
}

const server = http.createServer((req, res) => {
    const { method, url } = req

    if (method === 'OPTIONS') {
      	setCorsHeader(res)
    } else if (method === 'PUT' && urlObj.pathname === '/cors') {
        setCorsHeader(res)
        res.setHeader('content-type', 'application/json')
        // 返回数据
        res.write(JSON.stringify(mockData))
    } else {
      	res.statusCode = 404
    }
    res.end()
})

我们需要对对应的接口以及OPTIONS的请求,设置对应的Access-Control-Allow系列的header就好了(这里我就无脑全部设置了,有些header其实可以选择不设置,具体要根据实际业务场景来确定)。

小结

没有小结,拜拜~