阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

使用代理解决跨站点请求和跨站点上传文件

125次阅读
没有评论

共计 7336 个字符,预计需要花费 19 分钟才能阅读完成。

在做 Web 系统的时候,我们经常会使用 Ajax 技术来实现异步加载数据的功能,开始的时候我们的系统只有一个,但随着业务的发展,我们的 Web 系统可能有很多个,而且每个系统可能都是一个独立的站点,那么当我们直接使用 Ajax 技术来访问其它站点时,就会出现意外。

假设我们直接使用 jQuery 提供的 Ajax 方法来把京东的首页给抓取下来,那么有如下代码:

$.ajax({
url: 'http://www.jd.com/',
type: 'get',
success: function (data) {
//todo:
}
});

但我们在浏览器上跑了这段代码之后,却发现浏览器报了一个错误:

使用代理解决跨站点请求和跨站点上传文件

我当前的站点是 http://localhost:6404,浏览器提示说我当前的站点不被允许访问京东的站点,原因是京东的响应头里面没有“Access-Control-Allow-Origin”字段,那么“Access-Control-Allow-Origin”又是什么?

出于安全的因素,浏览器会限制我们从脚本中发起跨站请求,所以 W3C 工作组退出了一种新的机制,即跨源资源共享(Cross-Origin Resource Sharing (CORS)),这种机制让 Web 应用服务器能支持跨站访问控制,从而使得安全地进行跨站数据传输成为可能。而“Access-Control-Allow-Origin”就是该机制里面所定义的字段。W3C 上有对该字段的准确定义:

使用代理解决跨站点请求和跨站点上传文件

上面说,该响应头决定一个资源是否可以被共享给请求头里面的“Origin”字段的值。那“Origin”是什么?我们再看看之前的 Ajax 请求头信息,里面确实有“Origin”字段:

使用代理解决跨站点请求和跨站点上传文件

请求头里面的“Origin”字段的值就是我当前的站点地址,我是从“http://localhost:6404”这个站点向“http://www.jd.com”这个站点发起的 Ajax 请求的。所以根据 W3C 的文档描述,如果京东的响应头的“Access-Control-Allow-Origin”字段里面有“http://localhost:6404”这个值,那么我就可以成功抓取到京东的首页。

由于我改不了京东的响应头“Access-Control-Allow-Origin”字段,所以我就自己建了一个新的站点来测试一下。

站点“http://localhost:6404”上发起的请求脚本:

$.ajax({
url: 'http://localhost:6408/index.ashx',
type: 'get',
success: function (data) {
alert(data);
}
});

站点“http://localhost:6408”用来响应请求的后端代码:

publicvoid ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
context.Response.Headers.Add("Access-Control-Allow-Origin", "http://localhost:6404");
context.Response.Write(String.Format("我是站点:{0}", "http://localhost:6408"));
}

然后,我们在浏览器上跑一下,得如下结果:

使用代理解决跨站点请求和跨站点上传文件

从结果上可以看出,跨站点请求已经成功。接下来,我们把响应头里面的“Access-Control-Allow-Origin”字段去掉:

publicvoid ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
//context.Response.Headers.Add("Access-Control-Allow-Origin", "http://localhost:6404");
context.Response.Write(String.Format("我是站点:{0}", "http://localhost:6408"));
}

然后看一下结果:

使用代理解决跨站点请求和跨站点上传文件

结果果然跟之前的是一样的,这也证明了“Access-Control-Allow-Origin”在跨站点请求里面扮演的重要角色。

虽然问题解决了,但是新的问题又来了,如果我有 1000 个站点都要用到跨站点请求,那么我难道要把这 1000 个站点都加到响应头里面?所以,这肯定是不行的,那么我如何在不改动响应头的情况下实现跨站点请求呢?

前面说过,导致这个问题的直接原因是由于“浏览器限制了我们的脚本”,那么这里就有两个前提,一个是浏览器,一个是脚本,如果我们能避开其中一个,是不是就可以实现跨站点请求呢?于是我就想到了在后台代码里面直接构建 Http 请求代理来干掉浏览器。

由于直接使用 HttpWebRequest 类要配置的东西太多,而且在异步 API 这块写起来太繁琐,所以我使用了一个轻量但功能强大的 HttpClient 类来实现这一功能。

首先,我们来设计前端脚本 API,为了更好的兼容性,我不打算重新写套 Ajax 请求的 API,而是继续采用 $.ajax(),只不过传递的参数不一样而已。所以,就有了下面这个脚本 API:

$.ajax({
//这个 url 就是代理的地址,这个地址我们可以自己去 web.config 中配置
url: '/cors',
type: 'get',
data: {
//no 是我们自定义的参数
"no": "001",
//authkey 是访问代理所需要提供的授权 key,这个参数是必需的
"authkey": "e4f58a805a6e1fd0f6bef58c86f9ceb3",
//target 是是告诉代理,要把我们自定义的数据提交到哪个地址,这个参数是必需的
"target": "http://localhost:6408/index.ashx"
},
success: function (data) {
alert(data);
}
});

这样一来,我可以在最小改动下实现跨站点请求。

整个请求过程可以通过下图来表示:

使用代理解决跨站点请求和跨站点上传文件

然后再来说说“/cors”这个地址是怎么来的,这个地址就是代理处理程序的地址,我们可以在 Web.config 文件里面的节点里进行配置:

  <system.webServer>
<handlers>
<addname="cors"path="/cors"allowPathInfo="true"verb="GET"type="ChuXin.Web.Cors.RequestDispatcher,ChuXin.Web"/>
</handlers>
</system.webServer>

其中,ChuXin.Web.Cors.RequestDispatcher 为代理处理程序类,其核心实现代码如下:

publicvoid Process(String target, String authkey, IReadOnlyDictionary<String,String> args = null)
{
//拼接到目标站点的请求 Url
var argList = from t0 in args
select t0.Key + "=" + t0.Value;
String url = String.Format("{0}?{1}", target, String.Join("&", argList));

HttpClient hc = newHttpClient();
HttpContext context = HttpContext.Current;
Object result = null;

// 使用 HttpClient 发起异步请求
hc.GetAsync(url, HttpCompletionOption.ResponseContentRead).ContinueWith(t =>
{
// 异步请求结束后判断是否有异常或错误
if (HasExceptions<HttpResponseMessage>(t, context)) {return; }

// 如果任务正常完成
if (t.IsCompleted) {
// 获取响应消息对象
var respMessage = t.Result;
if (respMessage != null && respMessage.Content != null) {
// 获取目标站点响应头的 Content-Type 属性,并赋值给当前代理的响应头
var contentTypes = respMessage.Content.Headers.GetValues("Content-Type");
context.Response.ContentType = String.Concat(contentTypes);
String contentType = null;
foreach (var ct in contentTypes) {
String[] parts = ct.Split(newChar[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length > 0) {contentType = parts[0].Trim();}
}
if (contentType.StartsWith("text/")) {
// 如果 content-type 为文本类型的,则先以异步方式读取二进制数据,
//然后转成 UTF8 编码字符串赋值给结果对象
respMessage.Content.ReadAsByteArrayAsync().ContinueWith(t1 =>
{
if (HasExceptions<Byte[]>(t1, context)) {return; }

if (t1.IsCompleted) {
result = Encoding.UTF8.GetString(t1.Result);
}
}).Wait();
}
else {
// 如果 content-type 为非文本类型的,则直接以异步方式读取并赋值给结果对象
respMessage.Content.ReadAsByteArrayAsync().ContinueWith(t1 =>
{
if (HasExceptions<Byte[]>(t1, context)) {return; }

if (t1.IsCompleted) {
result = t1.Result;
}
}).Wait();
}
}
}
}).Wait();
// 如果结果为字符串,则直接输出
if (result isString) {
context.Response.Write(result);
}
// 如果结果为二进制数据,则写入到输出流里面
if (result isByte[]) {
var outputStream = context.Response.OutputStream;
if (outputStream.CanWrite) {
var data = result asByte[];
outputStream.Write(data, 0, data.Length);
}
}
}

其中,HasExceptions()方法为我自定义的一个处理任务失败的情况的方法。

我们在浏览器里面跑一下看看,得到下图结果:

使用代理解决跨站点请求和跨站点上传文件

从上图看出,我们设想成功实现了。我们把请求头和响应头的详细信息调出来看看:

请求头:

使用代理解决跨站点请求和跨站点上传文件

响应头:

使用代理解决跨站点请求和跨站点上传文件
使用代理解决跨站点请求和跨站点上传文件

跨站点 Get 请求数据试验成功,那么跨站点文件上传呢?

我们发现 HttpClient 类有 Post 方式的请求,例如 PostAsync(String, HttpContent)方法,第一个参数还好理解,那第二个参数 HttpContent 是啥玩意儿呢?转到定义发现它是一个抽象类,于是乎我就想知道有哪些子类实现了它,于是打开反编译工具我们发现有下面几个类实现了它:

使用代理解决跨站点请求和跨站点上传文件

通过反编译工具,我们看到“FormUrlEncodedContent”类的描述为“A container for name/value tuples encoded using application/x-www-form-urlencoded MIME type.”,所以它是适用于“application/x-www-form-urlencoded”这种类型的,很明显文件不属于这种,StringContent 同样不适合。然后我们再看看“MultipartFormDataContent”类,它的描述为“Provides a container for content encoded using multipart/form-data MIME type.”,眼尖的同学一定已经发现了,它就是跟我们上传文件所需的 MIME 类型一致的类了,所以我们就使用它来封装我们要上传的文件数据。

所以核心代码如下:

publicvoid ProcessRequest(HttpContext context)
{
//获取目标站点地址
String target = context.Request.Unvalidated["target"];
if (String.IsNullOrWhiteSpace(target)) {return; }
target = HttpUtility.UrlDecode(target);

HttpClient client = newHttpClient();

// 定义请求的边界值
String boundary = String.Format("-------ChuXinWebBoundary{0}", DateTime.Now.Ticks.ToString("x"));
// 将多个文件添加到请求的主内容里面
var content = newMultipartFormDataContent(boundary);
var files = context.Request.Files;
for (Int32 i = 0; i < files.Count; i++) {
var file = files[i];
var buf = newByte[file.InputStream.Length];
if (file.InputStream.Read(buf, 0, buf.Length) > 0) {
// 创建一个二进制内容对象
var dataContent = newByteArrayContent(buf, 0, buf.Length);
// 指定该对象的 Content-Type
dataContent.Headers.Add("Content-Type", file.ContentType);
// 添加到请求主内容里面
content.Add(dataContent, "file" + i, file.FileName);
}
}

//form表单数据添加的主内容对象里面
var form = context.Request.Form;
foreach (var key in form.AllKeys) {
if (String.Compare(key, "target", true) == 0) {continue; }
var data = Encoding.UTF8.GetBytes(form[key]);
var dataContent = newByteArrayContent(data);
content.Add(dataContent, key);
}

String result = null;
// 执行异步 POST 提交请求
client.PostAsync(target, content).ContinueWith(t0 =>
{
if (t0.IsCompleted) {
var respMessage = t0.Result;
if (respMessage != null && respMessage.Content != null) {
// 请求完成后,异步读取目标站点的响应结果信息
respMessage.Content.ReadAsStringAsync().ContinueWith(t1 =>
{
if (t1.IsCompleted) {
result = t1.Result;
}
}).Wait();
}
}
}).Wait();
// 获取到响应结果信息后输出
context.Response.Write(result);
client.Dispose();
}

然后我们测试一下:

首先,请求发起站点(http://localhost:6404/index.html)要得有一个 form 表单:

<formaction="/cors"method="post"enctype="multipart/form-data">
<inputname="myfile"type="file"/><br/><br/>
<inputname="authkey"type="hidden"value="e4f58a805a6e1fd0f6bef58c86f9ceb3"/>
<inputname="target"type="hidden"value="http://localhost:6408/file/upload"/>
<inputname="dir"type="hidden"value="~/uploaddir/"/>
<inputtype="submit"value="提交"/>
</form>

然后,在请求发起站点的 web.config 文件中配置代理:

<system.webServer>
<handlers>
<addname="cors"path="/cors"verb="GET,POST"type="ChuXin.Web.Cors.RequestDispatcher,ChuXin.Web"/>
</handlers>
</system.webServer>

最后选个文件提交一把,得如下结果:

使用代理解决跨站点请求和跨站点上传文件

这个就是我上传的图片文件地址,它已成功上传到“http://localhost:6408”这个站点的“uploaddir”目录。

参考文献
  • https://www.w3.org/TR/cors/
  • https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

本文永久更新链接地址:http://www.linuxidc.com/Linux/2016-04/130110.htm

正文完
星哥说事-微信公众号
post-qrcode
 
星锅
版权声明:本站原创文章,由 星锅 2022-01-21发表,共计7336字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中