上传文件遇到的一个问题
作者:
| 更新日期:以前,我一直在模仿。现在看来是时候去学习背后的一些东西了。
本文首发于公众号:天空的代码世界,微信号:tiankonguse
前言
以前,一直没有去了解浏览器发送请求时的具体原理,只是简单的执行 jQuery 的 get, post 或者 ajax 三个函数。
直到上周, 同事在做一个项目, 需要上传图片, 于是找我帮忙看看为什么,我于是走上了一条不归路。
背景介绍
我们使用的是 jQuery 的 fileUpload 插件来上传图片的。
代码如下
<form id="fileupload" method="POST" enctype="multipart/form-data" target="fileupload-iframe">
<input type="file" name="files[]" multiple="">
<form>
<iframe frameborder="0" style="height: 0px; width: 0px; position: absolute;" id="fileupload-iframe" ></iframe>
<button type="submit" class="btn btn-primary" id="start">start</button>
<script>
(function () {
$("#fileupload").fileupload({
add : function(e, data) {
//do something
$("#start").click(function() {
data.submit();
return false;
});
}
});
})()
</script>
最初的时候我问跨域了吗? 他说没有。
然后我就想:既然没跨域, 那就是代码哪里写的不对的原因了。
于是一顿修改, 后来发现怎么调都不行。
然后总结一下是这个样子:
使用 jsnop 方式发送请求(服务端只接受 jsonp), POST 数据不能发送出去, 原因是 jsonp 使用的是 GET 请求。
使用 POST 请求方式发送, 数据可以发过去, 但是返回的数据会解析失败, 因为服务器返回的是 jsonp 数据。
于是得出结论: 这个是矛盾的, 根本解决不了。
然后我又问自己, 真的不可以吗?
于是我使用 php 快速写了一个文件上传的测试代码, 结果在我这怎么写都能正常上传。
这是我意识到可能他骗我了, 可能是跨域问题。
于是去看一下, 页面地址和服务器地址是不同的子域名,然后我告诉他不同的子域名也是跨域的。
好的, 现在确定是跨域了, 但是问题还是不能解决。 因为上面的矛盾与跨域没有关系, 服务器只接受 jsonp , 即使 post 数据过去, 返回也解析失败。
于是我跟 组长说:这个不可能实现, jsonp 是 get, 具体实现方式是在 iframe 里插入 <script>
标签实现的.
于是我把问题丢给了组长。
他认为现在的浏览器应该支持跨域了, 只是需要服务器端配置好, 请求的时候也配置好合适的参数才行。
我说我都配置过了, 都不行。
于是他在哪里研究了一番,后来他说可以了, 原来它把很多没有必要的东西删了。
删完之后是这个样子
<input id="fileupload" type="file" name="files[]" multiple="">
<button type="submit" class="btn btn-primary" id="start">start</button>
<script>
(function () {
$("#fileupload").fileupload({
add : function(e, data) {
//do something
$("#start").click(function() {
data.submit();
return false;
});
}
});
})()
</script>
为什么不要 form 和 iframe 呢?
因为那个方法是通过 在 iframe 里插入 <script>
标签实现的跨域的,既然不能选择那个方法, 那就不需要哪些 dom 了。
当然, 他在浏览器端和 服务器端都配置很对参数, 那些实际上默认就行了。
他做的最重要的一件事是当服务器把请求的包传回来的时候, 他把 jsnop 修改成 json 了。
好吧, 我看到这, 意识到即使是 jsonp, 我也有办法了, 我设置一下 dataType , 设为值 text 不就行了嘛?
之前我曾想着设置为 xml 试试, 但是一直没事, 现在看在使用 xml 试试的话应该也会成功的。
所以这个问题的解决方案就是服务端支持返回json格式的代码就行了,也就是修改服务器端的代码, 如果服务器段我们不能控制,那只好使用 text 或 xml 的方式先得到数据, 然后再手动转化为 json 吧?
好了, 背景介绍完了。
一个疑惑
其实, 我之前的问题不是这个, 我知道跨域 POST 的时候会有两个请求, 一个是 OPETION , 一个是 POST.
而我的请求一直没有那个 OPETION 请求包。
组长说他看了 CORS 的文档后解决问题的, 于是我也去看了一下这个文档, 然后找到了答案。
现在就来讲解一下 CORS, 答案就在里面。
CORS 的背景
Cross-site HTTP requests are HTTP requests for resources from a different domain than the domain of the resource making the request.
跨站 HTTP 请求(Cross-site HTTP request)是跨域的 HTTP 请求。
Cross-site HTTP requests initiated from within scripts have been subject to well-known restrictions, for well-understood security reasons.
出于安全考虑,浏览器会限制脚本中发起的跨站请求。
The Web Applications Working Group within the W3C has recommended the new Cross-Origin Resource Sharing (CORS) mechanism, which provides a way for web servers to support cross-site access controls, which enable secure cross-site data transfers.
W3C 的 Web 应用工作组推荐了一种新的机制,即跨源资源共享. 这种机制让Web应用服务器能支持跨站访问控制,从而使得安全地进行跨站数据传输成为可能.
这里需要了解一个词:跨域。
不同的子域名,或者端口不同都是跨域的。
具体可以参考 wiki.
access control scenarios
Simple requests
A simple cross-site request is one that:
Only uses GET, HEAD or POST. If POST is used to send data to the server, the Content-Type of the data sent to the server with the HTTP POST request is one of application/x-www-form-urlencoded, multipart/form-data, or text/plain.
Does not set custom headers with the HTTP Request (such as X-Modified, etc.)
什么意思呢?
只使用 GET, HEAD 或者 POST 请求方法。
如果使用 POST 向服务器端传送数据,则数据类型(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一种。
当然, 服务器段还要配置 Access-Control-Allow-Origin:*
来接受任何客户端的请求, 由于我的服务端已经默认配置了, 所以我不需要担心这个问题了。
Preflighted requests
不同于上面讨论的简单请求,“预请求”要求必须先发送一个 OPTIONS 请求给目的站点,来查明这个跨站请求对于目的站点是不是安全可接受的。
这样做,是因为跨站请求可能会对目的站点的数据造成破坏。
当请求具备以下条件,就会被当成预请求处理:
It uses methods other than GET, HEAD or POST.
Also, if POST is used to send request data with a Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain, e.g.
if the POST request sends an XML payload to the server using application/xml or text/xml, then the request is preflighted.
It sets custom headers in the request (e.g. the request uses a header such as X-PINGOTHER)
重点在这里: 使用 POST,但请求数据为 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 以外的数据类型。
而我的请求在 form 里, 幸运的是 form 还有 multipart/form-data 这个字段, 于是我永远也不可能触发 Preflighted requests。
悲剧呀。
看到这, 图片不能成功上传的第二个原因就在这个 multipart/form-data
上了, 把它删了就可以了。
Requests with credentials
XMLHttpRequest和访问控制功能,最有趣的特性就是,发送凭证请求(HTTP Cookies和验证信息)的功能。
一般而言,对于跨站请求,浏览器是不会发送凭证信息的。
但如果将XMLHttpRequest的一个特殊标志位设置为true,浏览器就将允许该请求的发送。
如果服务器端的响应中,如果没有返回Access-Control-Allow-Credentials: true的响应头,那么浏览器将不会把响应结果传递给发出请求的脚步程序,以保证信息的安全。
解决问题
文档看到这, 应该就可以解决问题了。
需要做三件事:
- 不使用 jsnp 发送数据, 返回的数据需要时 json 格式
- 服务器段配置
Access-Control-Allow-Origin
, 代表接受这个客户端 - 请求的包头不能有 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain。
参考文档
本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。