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

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

463次阅读
没有评论

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

星哥玩云

星哥玩云
星哥玩云
分享互联网知识
用户数
4
文章数
19350
评论数
4
阅读量
7963631
文章搜索
热门文章
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛 NAS-3:安装飞牛 NAS 后的很有必要的操作 前言 如果你已经有了飞牛 NAS 系统,之前...
我把用了20年的360安全卫士卸载了

我把用了20年的360安全卫士卸载了

我把用了 20 年的 360 安全卫士卸载了 是的,正如标题你看到的。 原因 偷摸安装自家的软件 莫名其妙安装...
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛 NAS 中安装 Navidrome 音乐文件中文标签乱码问题解决、安装 FntermX 终端 问题背景 ...
阿里云CDN
阿里云CDN-提高用户访问的响应速度和成功率
随机文章
开发者福利:免费 .frii.site 子域名,一分钟申请即用

开发者福利:免费 .frii.site 子域名,一分钟申请即用

  开发者福利:免费 .frii.site 子域名,一分钟申请即用 前言 在学习 Web 开发、部署...
飞牛NAS玩转Frpc并且配置,随时随地直连你的私有云

飞牛NAS玩转Frpc并且配置,随时随地直连你的私有云

飞牛 NAS 玩转 Frpc 并且配置,随时随地直连你的私有云 大家好,我是星哥,最近在玩飞牛 NAS。 在数...
星哥带你玩飞牛NAS-7:手把手教你免费内网穿透-Cloudflare tunnel

星哥带你玩飞牛NAS-7:手把手教你免费内网穿透-Cloudflare tunnel

星哥带你玩飞牛 NAS-7:手把手教你免费内网穿透 -Cloudflare tunnel 前言 大家好,我是星...
4盘位、4K输出、J3455、遥控,NAS硬件入门性价比之王

4盘位、4K输出、J3455、遥控,NAS硬件入门性价比之王

  4 盘位、4K 输出、J3455、遥控,NAS 硬件入门性价比之王 开篇 在 NAS 市场中,威...
240 元左右!五盘位 NAS主机,7 代U硬解4K稳如狗,拓展性碾压同价位

240 元左右!五盘位 NAS主机,7 代U硬解4K稳如狗,拓展性碾压同价位

  240 元左右!五盘位 NAS 主机,7 代 U 硬解 4K 稳如狗,拓展性碾压同价位 在 NA...

免费图片视频管理工具让灵感库告别混乱

一言一句话
-「
手气不错
仅2MB大小!开源硬件监控工具:Win11 无缝适配,CPU、GPU、网速全维度掌控

仅2MB大小!开源硬件监控工具:Win11 无缝适配,CPU、GPU、网速全维度掌控

还在忍受动辄数百兆的“全家桶”监控软件?后台偷占资源、界面杂乱冗余,想查个 CPU 温度都要层层点选? 今天给...
4盘位、4K输出、J3455、遥控,NAS硬件入门性价比之王

4盘位、4K输出、J3455、遥控,NAS硬件入门性价比之王

  4 盘位、4K 输出、J3455、遥控,NAS 硬件入门性价比之王 开篇 在 NAS 市场中,威...
开发者福利:免费 .frii.site 子域名,一分钟申请即用

开发者福利:免费 .frii.site 子域名,一分钟申请即用

  开发者福利:免费 .frii.site 子域名,一分钟申请即用 前言 在学习 Web 开发、部署...
手把手教你,购买云服务器并且安装宝塔面板

手把手教你,购买云服务器并且安装宝塔面板

手把手教你,购买云服务器并且安装宝塔面板 前言 大家好,我是星哥。星哥发现很多新手刚接触服务器时,都会被“选购...
开源MoneyPrinterTurbo 利用AI大模型,一键生成高清短视频!

开源MoneyPrinterTurbo 利用AI大模型,一键生成高清短视频!

  开源 MoneyPrinterTurbo 利用 AI 大模型,一键生成高清短视频! 在短视频内容...