跨域问题的产生
随着微服务、分布式等架构体系的发展,很多人在传统 web 开发上会遇到很多跨域的问题,特别是你的系统对接的服务不是你们自己公司的服务的时候,就更难办了。例如,用户要求所有厂家的文件服务都对应某一家厂家提供的服务,而这时这家厂家提供的仅仅是单纯的 HTTP 服务,只给了你一个地址 URL ,外加一个范例贴图。如果你没有接触过跨域问题,想着大不了就是前端 form 提交的 action 地址变一下就可以了,那么前面会有个坑等着你,而且是看得见,摸不着的。下面是具体描述:这里采用的是 jquery form 表单提交,
1
2
3
4
5
6
7
8
9
$ ( '#upload_form' ). form ( 'submit' ,{
// 文件服务调用发送
url : 'http://192.168.1.101:8091/weedfs/upload' ,
async : false ,
success : function ( data ) {
debugger ;
var obj = eval ( '(' + data + ')' );
}
});
原来 URL 地址是本地服务,后改为第三方文件服务地址,提交后,始终进入不了 success 回调里,打开 Chrome 调试,设了断点,显示的是这个样子:
你是不是很奇怪,怎么有返回,但就是回调函数没有值,我们再深挖一下,顺着回调往上走,我们可以追踪到 jquery 是怎么创建一个 iframe 去执行文件上传的,
调试的时候这里 f.contents() 获取的是null,导致 .find(“body”) 为 undefined ,所以回调没有值,但是点 Elements 查看后发现里面是有值的,可以看一下,返回的 responseStr 已经在 iframe 里了
1
2
3
4
5
6
7
8
9
10
11
<iframe id= "easyui_frame_1472175408181" name= "easyui_frame_1472175408181" src= "about:blank" style= "position: absolute; top: -1000px; left: -1000px;" >
<html>
<head></head>
<body>
<pre style= "word-wrap: break-word; white-space: pre-wrap;" >
{"responseUpload":{"fid":"Z3JvdXAxL00wMC8wMC8wOS9DbThNRGxlX25PcUVYMkhWQUFBQUFMSTZHTm83MTgucG5n","error":null,"fileName":"QQ20160805-0@2x.png",
"fileUrl":"http://192.168.1.101:8091/weedfs/download/Z3JvdXAxL00wMC8wMC8wOS9DbThNRGxlX25PcUVYMkhWQUFBQUFMSTZHTm83MTgucG5n","size":"219678"}}
</pre>
</body>
</html>
</iframe>
这就解释了为什么 repsonse 有值,而回调没有值的问题,因为返回值已经传过来了,但是是在跨域的 iframe 里,通过 f.contents() 方法是获取不了的,后来我也去网上找了很多,大部分都是改造 response header ,而且还有浏览器兼容性的问题,况且文件服务不是我们的,不可能去让厂家改的,磨了好半天,找到了一个解决问题的思路:请求提交到本地不变,文件存储过后,再由后台打包提交重新发起一次 HTTP POST 请求去对接文件服务 URL ,在后台获取返回值,解析以后再传到前台。
这样做还有个好处,即文件服务地址没有暴露在前端,而是配置在后端字典表,保证了安全,既然思路有了,就说一下怎么实现吧。
实现原理
为了更直观,我直接上代码了,其实不是很难,核心代码就20几行。
首先是 controller 层调用(使用的是spring mvc架构),下面贴出代码片段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RequestMapping ( value = "uploadFileTmpRemote" , method = RequestMethod . POST )
@ResponseBody
public Map < String , String > uploadFileTmpRemote ( MultipartHttpServletRequest request ) throws Exception {
Map < String , String > messageMap = new HashMap < String , String >();
// 获取字典表文件服务地址
String httpRemoteUrl = AppConfig . DICT_MAP_VALUE . get ( "httpRemoteUrl" ). get ( "1" );
if ( httpRemoteUrl != null && ! "" . equals ( httpRemoteUrl )) {
List < MultipartFile > files = request . getFiles ( "fileList" );
RemoteFileUpload remoteFileUpload = new RemoteFileUpload ();
messageMap = remoteFileUpload . executeRequest ( files , httpRemoteUrl );
}
return messageMap ;
}
接下来是 RemoteFileUpload 核心实现,先在 maven 配置文件 pom.xml 添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId> org.apache.httpcomponents</groupId>
<artifactId> httpclient</artifactId>
<version> 4.5.2</version>
</dependency>
<dependency>
<groupId> org.apache.httpcomponents</groupId>
<artifactId> httpmime</artifactId>
<version> 4.5.2</version>
</dependency>
<dependency>
<groupId> org.apache.httpcomponents</groupId>
<artifactId> httpcore</artifactId>
<version> 4.4.4</version>
</dependency>
<dependency>
<groupId> com.alibaba</groupId>
<artifactId> fastjson</artifactId>
<version> 1.2.7</version>
</dependency>
自动下载成功后,便可运行下面代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package com . summer . web . util ;
import java.io.ByteArrayOutputStream ;
import java.io.File ;
import java.io.InputStream ;
import java.math.BigDecimal ;
import java.util.Date ;
import java.util.HashMap ;
import java.util.List ;
import java.util.Map ;
import org.apache.http.HttpEntity ;
import org.apache.http.HttpHost ;
import org.apache.http.HttpResponse ;
import org.apache.http.client.config.RequestConfig ;
import org.apache.http.client.methods.HttpPost ;
import org.apache.http.entity.mime.HttpMultipartMode ;
import org.apache.http.entity.mime.MultipartEntityBuilder ;
import org.apache.http.entity.mime.content.FileBody ;
import org.apache.http.impl.client.CloseableHttpClient ;
import org.apache.http.impl.client.HttpClients ;
import org.springframework.web.multipart.MultipartFile ;
import com.alibaba.fastjson.JSONObject ;
public class RemoteFileUpload {
/*
* @param files: form表单上传后获得的files(因为使用的是spring上传组件,所以是MultipartFile类型)
* @param httpUrl: 文件服务url地址
*/
public Map < String , String > executeRequest ( List < MultipartFile > files , String httpUrl ) throws Exception {
Map < String , String > messageMap = new HashMap < String , String >();
CloseableHttpClient httpclient = null ;
try {
for ( MultipartFile multipartFile : files ) {
if ( multipartFile . isEmpty ()) {
continue ;
}
String nameFull = multipartFile . getOriginalFilename ();
String size = Long . toString ( multipartFile . getSize ());
String name = nameFull . substring ( 0 , nameFull . lastIndexOf ( "." ));
String type = nameFull . substring ( nameFull . lastIndexOf ( "." ) + 1 );
String tmpName = Long . toString ( new Date (). getTime ()) + "." + type ;
multipartFile . transferTo ( new File ( AppConfig . tempPath , tmpName ));
httpclient = HttpClients . createDefault ();
// 文件服务调用发送
HttpPost httppost = new HttpPost ( httpUrl );
// 判断是否有代理配置,如果有去字典表读取,如果请求无需代理,可以忽略这段
if ( AppConfig . DICT_MAP_VALUE . get ( "proxyHttp" ) != null && AppConfig . DICT_MAP_VALUE . get ( "localHostName" ) != null ) {
if ( AppConfig . localHostName . indexOf ( AppConfig . DICT_MAP_VALUE . get ( "localHostName" ). get ( "1" )) != - 1 ){
String [] proxyHttp = AppConfig . DICT_MAP_VALUE . get ( "proxyHttp" ). get ( "1" ). split ( ":" );
HttpHost proxy = new HttpHost ( proxyHttp [ 0 ], Integer . parseInt ( proxyHttp [ 1 ]), "http" );
RequestConfig config = RequestConfig . custom (). setProxy ( proxy ). build ();
httppost . setConfig ( config );
}
}
FileBody bin = new FileBody ( new File ( AppConfig . tempPath , tmpName ));
MultipartEntityBuilder multipartEntity = MultipartEntityBuilder . create ();
multipartEntity . setMode ( HttpMultipartMode . BROWSER_COMPATIBLE );
multipartEntity . addPart ( "file" , bin );
httppost . setEntity ( multipartEntity . build ());
HttpResponse response = httpclient . execute ( httppost );
HttpEntity entity = response . getEntity ();
InputStream inStream = entity . getContent ();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream ();
byte [] buffer = new byte [ 1024 ];
int len = - 1 ;
while (( len = inStream . read ( buffer )) != - 1 ) {
outSteam . write ( buffer , 0 , len );
}
outSteam . close ();
JSONObject jsonObj = JSONObject . parseObject ( new String ( outSteam . toByteArray ()));
Map < String , String > responsMap = ( Map ) jsonObj . get ( "responseUpload" );
messageMap . put ( "name" , name );
messageMap . put ( "type" , type );
messageMap . put ( "size" , new BigDecimal ( size ). divide ( new BigDecimal ( "1000" ))
. setScale ( 0 , BigDecimal . ROUND_HALF_UP ). toString () + "KB" );
messageMap . put ( "tmpName" , responsMap . get ( "fileName" ));
messageMap . put ( "fileUrl" , responsMap . get ( "fileUrl" ));
}
} finally {
httpclient . close ();
}
return messageMap ;
}
}
以上代码已经很清晰的给出了解决方案,而且如果你需代理还可以简单的配置一下即可,如果还有什么不清楚的地方,可以留言
Thanks for
在这里诚挚的感谢破船兄的指点,他给了我很大的帮助,这里我参考了他的博客才得以完成我的博客系统的搭建,利用Octopress搭建一个Github博客 ,这是位iOS大神,大家可以去搜罗一些干货。