为什么不建议使用jsonp

翻译自 文章

相信大家都已经了解什么是跨域请求,知道它们为什么会发生和如何避免它们。有时候,当讨论跨域请求得我时候,人们习惯使用JSONP(JSON with Pdding)作为跨域请求的解决方案,在这篇文章中,我们将会解释一下为什么JSONP不是最好的解决方案和为什么它不应该被使用。

CORS 101 - 一个快速的概括

当从一个网页应用发起请求资源,而这个应用所在域名不属于我们控制之下的,我们就会收到一条提示Failed to load resource: Origin * is not allowed by Access-Control-Allow-Origin.。这意味着浏览器阻止我们访问给定资源的请求 - 该资源是一个API的端。

一个实现的例子

现在就用一个例子来演示一下什么是跨域,我们有两个Node.js文件,一个是API服务端和一个是HTTP服务端:

1
2
3
4
5
6
7
8
9
10
11
12
// api.js
const express = require('express')
const app = express()
const port = 3000

const apiHandler = (requires, response) => {
response.json({ message: 'hello World'})
}

app.get('/api/hello', apiHandler)

app.listen(post, () => console.info(`API up on port ${port}`))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// http-server.js
const http = require('http')
const path = require('path')
const fs = require('fs')
const port = 8080

const httpServerHanlder = (request, response) => {
response.writeHead(200)
response.write(fs.readFileSync(path.join(`${process.cwd()}/index.html`)))
response.end()
}

http.createServer(httpServerHandler).listen(port)
console.log(`HTTP Server is up on port ${port}`)

请注意,HTTP服务端是一个直截了当的实现,它用了fs.readFileSync函数,调用的时候会造成堵塞,在实际的生产环境中不应该使用。实际上,我们应该使用npm来管理。

现在我们有两个Node.js程序,一个是API服务端,运行在3000端口杀红,一个是HTTP服务端,运行在8080端口中。由于CORS背后的规则,这l两个服务会被认为是两个独立的实体,因此浏览器将阻止访问资源。

我们将会加入一个简单的HTML文件,使用JQuery制造一个AJAX请求来演示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!doctype html>

<html lang="en">
<head>
<meta charset="utf-8">
<title>JSONP</title>
<meta name="author" content="Tamas Piros">
<script src="https://code.jquery.com/jquery-3.3.1.js"></script>
</head>

<body>
<h1 id="message"></h1>
<script>
$.ajax({
url: 'http://localhost:3000/api/hello',
}).done(data => console.log(data));
</script>
</body>
</html>

打开浏览器,并且访问localhost:8080,就会看到前面所说的CORS提示信息了。而实际上,这个也是我们预期的结果。

JSONP

来看一下在这个情景下JSONP是怎么实现的。首先留意我们的index.html文件,有一样十分有趣的事情,我们实际上也引用了一个不在同域下的资源 —— JQuery库,而它是存在于一个CDN上的。

这就引出了一个很有趣的观点,就是我们可以通过<script></script>标签来访问资源。

我们需要对API服务返回给我们的数据作同样的修改。这就是Padding在JSONP中的含义。浏览器可以执行到这个函数。

来看看更新后的API服务端代码:

1
2
3
const apiHandler = (request, response) => {
response.jsonp({ message: 'Hello World!'})
}

同样的,更新index.html

1
2
3
4
$.ajax({
url: 'http://localhost:3000/api/htllo',
dataType: 'jsonp'
}).done(data => console.log(data))

当我们刷新页面,我们可以看到数据有输出。可以进一步把数据输出到页面中:

1
2
3
4
$.ajax({
url: 'http://localhost:3000/api/hello',
dataType: 'jsonp'
}).done(data => document.getElementById('message').textContent = data.message)

这个就是一个完整的例子。

但是我们今天要讨论的是为什么JSONP是个坏东西,并且为什么我们不要去用它。

只支持HTTP GET

JSONP只支持HTTP GET请求,其它的请求一概不支持。这是由于<script></script>标签只能发起HTTP GET请求。

没有错误处理

当调用AJAX请求的时候,我们可以获取到由服务端返回的错误body,然而,使用JSONP的话,我们要么得到的是CORS错误,要么得到的是404错误,不管是哪种,对于我们来说调试都是很困难的。

脆弱性

JSONP暴露了很多漏洞,它假定了传送回来的代码是可信的,进一步暴露了CSRF(跨站点请求伪造)漏洞。

综上所述,使用JSONP并不是一个很好的解决方案。

更多的选择

那么,如果不适用JSONP的话,我们还能用什么呢?请再看一遍“什么是CORS?”,里面提出了一系列的解决方案,包括使用代理和CORS包。

对于我们的例子来说,我们有两个选择去支持CORS:

1
2
3
4
5
6
7
8
//api.js
const apiHandler = (req, res) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Request-Method', '*')
res.header('Access-Control-Allow-Methods', 'OPTIONS, GET')
res.header('Access-Control-llow-Headers', '*')
res.json({ message: 'Hello World!'})
}

调用Ajax的时候不要忘记了移除dataType: 'jsonp'
请注意直接使用(*)并不是一个好的方案,指定允许域名更加妥当。

另外的解决方案就是使用cors包,安装了之后,可以把它作为一个中间件引入:

1
2
3
4
5
// api.js
const cors = require('cors')
app.use(cors())
// or
app.get('/api/hello', cors(), apiHandler)

总结

毫无疑问,JSONP可以被视为克服导致CORS错误的某些情况的一种有用方法,但是它有更多的负面副作用,以及安全漏洞,使用代理或相关的CORS包更好的应对跨域问题。