功能已经做完后, 发现有JS跨站访问的问题. 自己写了段JS请求转发器来解决这个问题.
情况是这样的:
请求Web服务器返回页面, 根据页面选择的条件绑定JS脚本在按钮上, 点击按钮之后, 需要发请求给Jenkins(跟Web服务器不在一起), 并解析Jenkins的返回结果, 显示在Web页面上. 典型的JS跨站, 安全性考虑: 客户端从Web服务器上拿到脚本, 该脚本只允许访问Web服务器上的资源, 如果JS中有访问另外一台服务器的代码, 则禁止访问, 返回400错误.
解决方法:
把JS脚本中请求Jenkins的地址(非Web服务器资源), 通通替换成Web服务器上的一个一般处理程序(JS跨站请求转发器), 在这个一般处理程序中, 将请求地址换成Jenkins的请求地址, 并将获得的返回结果回送给JS. 这样就完成了JS的跨站请求访问.
实例代码:
说明: 1. 只贴需要用的代码, 程序无法运行.
2. 年前赶时间, 要完成功能, 没有很好的考虑封装的问题, 代码臃肿, 将就看.
后台CS关键代码:
//其中的"/common/XSSTransferHandler.ashx?url="就是负责转发跨站请求的一般处理程序, url请求参数用来分隔原始请求的请求参数
this.btnStructure.Attributes.Add("onclick", string.Format("return preStructure('{0}');", "http://" + this.Request.Url.Authority + "/common/XSSTransferHandler.ashx?url="));
前台JS关键代码(JS不熟写的尤其乱): 这里有个东西解释下, 大部分都是异步的JS请求, 所以入口函数是: preStructure, 在该函数里构造了一个二维数组用来记录异步JS请求时各种状态信息, 如: 闭包函数的ID, 方便请求成功时终止闭包.
//###Start.自动编译###==>JS异步请求给定的URL,获得返回结果显示在页面上
var structureUrl;
var structureFlagTable; //任务名称|标志位, 其中:0初始状态(即编译中), 1表示编译失败, 2表示编译成功, 3表示打包中, 4表示打包失败, 5表示打包成功
function ajaxMethod(url, reqMethod, async, dealResultFunction, structureId) {
var xhr;
if (window.ActiveXObject) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
else if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
}
else {
throw new Error("Ajax is not supported by this browser");
}
//处理返回结果的方法
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
//将结果显示在弹出的新DIV上;
if(dealResultFunction != null)
dealResultFunction(xhr.responseText);
}
}
}
//发送构建请求
xhr.open(reqMethod, url, async);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
if (structureId != -1)
xhr.setRequestHeader("n", structureId.toString());
xhr.send(null);
}
function structure(url) {
var startReqUrl = url + "/build?delay=0sec";
var getRequestUrl = url + "/buildHistory/ajax";
window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "<font color='green'>启动编译任务< " + url.substring(url.lastIndexOf("/")+1) + " >...</font>";
//同步方式获取最近更新构建ID
var xhr;
if (window.ActiveXObject) {
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
else if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
}
else {
throw new Error("Ajax is not supported by this browser");
}
xhr.open("get", url, false);
xhr.send(null);
//处理结果
var lastId = getLastStuctureId(xhr.responseText);
window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "<font color='green'>编译任务开始< " + url.substring(url.lastIndexOf("/")+1) + " >...</font>";
//异步启动构建任务
ajaxMethod(startReqUrl, "get", true, null, -1);
window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "<font color='green'>编译任务< " + url.substring(url.lastIndexOf("/")+1) + " >正在执行...</font>";
//异步处理返回结果
function inner() {
ajaxMethod(getRequestUrl, "post", true, showStructureResult, parseInt(lastId) + 1);
}
//setStructureTableValue(url.substring(url.lastIndexOf("/")+1), 3, setInterval(inner, 5000));
setStructureTableValue(url.substring(url.lastIndexOf("=") + 1), 3, setInterval(inner, 5000));
}
//更具context的内容返回最近构建Id
function getLastStuctureId(resContext) {
window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "<font color='green'>编译任务开始...</font>";
var regPattern = /<\s*a\s*href=\"lastBuild\/\">Last\s+build\(\#([\d]+)\),[\W\w]+?<\s*\/\s*a\s*>/gi;
var result = regPattern.exec(resContext);
return (result == null || result[1] == null) ? "0" : result[1]; //返回最近Build的BuildID
}
//处理返回结果, 并显示在页面上
function showStructureResult(responseContext) {
var divShow = window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResult");
var packUrlPattern = /<a\s+class=\"tip"\s+href=\"\/job\/(.*?)\/.*?([\d]{4}-[\d]{1,2}-[\d]{1,2}\s{1}[\d]{1,2}\:[\d]{1,2}:[\d]{1,2}).*?<\/a>/;
var regPat1 = /src=\"/g;
var regPat2 = /href=\"/g;
var removeBar = /<tr\s+class=\"transitive\">[\s\S]*<\/tr><\/table>/;
var structureName = (packUrlPattern.exec(responseContext) != null && packUrlPattern.exec(responseContext)[1] != null) ? packUrlPattern.exec(responseContext)[1] : "";
var timePath = stringTime(packUrlPattern.exec(responseContext)[2]);
divShow.innerHTML = responseContext.replace(removeBar, "</table>").replace(regPat1, "src=\"http://192.168.199.61:8080").replace(regPat2, "href=\"http://192.168.199.61:8080");
if (divShow.innerHTML.indexOf("Success") != -1) {
window.clearInterval(getStructureTableValue(structureName,3));
setStructureTableValue(structureName, 2, structureName + "_" + timePath + ".tar.gz"); //暂存tar包的名称
setStructureTableValue(structureName, 1, 2);
//启动打包任务
packageTar(structureUrl + structureName);
window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "<font color='green'>< " + structureName + " >编译成功, 正在打包...</font>";
}
else if (divShow.innerHTML.indexOf("Failed") != -1) {
window.clearInterval(getStructureTableValue(structureName, 3));
window.clearInterval(flagTableID);
setStructureTableValue(structureName, 1, 1);
window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "<font color='red'>< " + structureName + " >编译失败, 任务终止...</font>";
}
else {
window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "<font color='green'>编译任务< " + structureName + " >正在执行...</font>";
}
}
//选择多个应用时, 在隐藏域中会生成构建任务列表, 根据构建任务列表执行多个构建任务
var flagTableID;
function preStructure(url) {
//判断是否勾选环境
var choseEviron = window.document.getElementById("ctl00_ContentPlaceHolder1_chkEnvironment").childNodes;
var isChose = false;
for (var v = 0; v < choseEviron.length; v += 2) {
if (choseEviron[v].tagName == "INPUT" && choseEviron[v].checked == true) {
isChose = true;
break;
}
}
if (isChose) {
window.document.getElementById("ctl00_ContentPlaceHolder1_chkEnvironment").style.backgroundColor = "#FFFFFF";
}
else {
window.document.getElementById("ctl00_ContentPlaceHolder1_chkEnvironment").style.backgroundColor = "#FEB8B8";
return false;
}
structureFlagTable = []; //清空JS的数组
structureUrl = url;
var hidValue = window.document.getElementById("ctl00$ContentPlaceHolder1$hiddenFldStructureList").value.split(",");
var errorCount;
var successCount;
//初始化构建任务标识表
structureFlagTable = new Array(hidValue.length);
for (var v = 0; v < hidValue.length; v++) {
structureFlagTable[v] = [hidValue[v], 0, "",0,0];
}
function inner() {
errorCount = 0;
successCount = 0;
for (var v = 0; v < structureFlagTable.length; v++) {
if (structureFlagTable[v][1] == 1 || structureFlagTable[v][1] == 4) {
errorCount++;
break;
}
if (structureFlagTable[v][1] == 5) {
successCount++;
}
}
showCompileAndPackageResult();
if (errorCount > 0 || successCount == structureFlagTable.length) {
window.clearInterval(flagTableID);
if (successCount == structureFlagTable.length) {
var resultShow = window.document.getElementById("ctl00$ContentPlaceHolder1$txtCode");
var tagUrl = "";
//通知执行多个MakeTar
var testEnvironment = {
"厂商测试环境SiteA": "Tar_ComTestA",
"厂商测试环境SiteB": "Tar_ComTestB",
"功能测试环境SiteA": "Tar_FunTestA",
"功能测试环境SiteB": "Tar_FunTestB",
"功能测试环境SiteC": "Tar_FunTestC"
};
var tempUrl = "";
var tempUrlBase = "";
choseEviron = window.document.getElementById("ctl00_ContentPlaceHolder1_chkEnvironment").childNodes;
for (var v = 0; v < structureFlagTable.length; v++) {
tagUrl += structureFlagTable[v][2] + "\r\n";
tempUrlBase = structureUrl + structureFlagTable[v][0] + "/batchTasks/task/Tar_CompilerMachine/execute";
for (var u = 0; u < choseEviron.length; u += 2) {
if (choseEviron[u].tagName == "INPUT" && choseEviron[u].checked == true) {
tempUrl = tempUrlBase.replace("Tar_CompilerMachine", testEnvironment[choseEviron[u + 1].innerText]);
ajaxMethod(tempUrl, "get", true, null, -1);
}
}
resultShow.value = tagUrl;
}
//允许点击提交
window.document.getElementById("ctl00_ContentPlaceHolder1_btnSubmit").disabled = false;
alert("所有构建任务均成功出包!");
}
}
}
//启动构建任务
for (var v = 0; v < structureFlagTable.length; v++) {
structure(structureUrl + structureFlagTable[v][0]);
}
flagTableID = setInterval(inner, 5000);
}
//显示编译结果
function showCompileAndPackageResult() {
var resultShow = window.document.getElementById("ctl00$ContentPlaceHolder1$txtCode");
var resultText = "";
for (var v = 0; v < structureFlagTable.length; v++) {
switch (structureFlagTable[v][1]) {
case 0:
resultText += structureFlagTable[v][0] + ": 正在编译...\r\n";
break;
case 1:
resultText += structureFlagTable[v][0] + ": 编译失败!\r\n";
break;
case 2:
resultText += structureFlagTable[v][0] + ": 编译成功!\r\n";
break;
case 3:
resultText += structureFlagTable[v][0] + ": 正在打包...!\r\n";
break;
case 4:
resultText += structureFlagTable[v][0] + ": 打包失败!\r\n";
break;
case 5:
resultText += structureFlagTable[v][0] + ": 打包成功!\r\n";
break;
default:
resultText += "未知异常\r\n";
}
}
resultShow.value = resultText;
}
//设置构建任务表示表的值, 注意字符串后便可能还有换行符
function setStructureTableValue(key, colIndex, value) {
for (var v = 0; v < structureFlagTable.length; v++) {
if (key.toLowerCase() == structureFlagTable[v][0].toLowerCase()) {
structureFlagTable[v][colIndex] = value;
}
}
}
//获取构建任务标示表的值
function getStructureTableValue(key, colIndex) {
for (var v = 0; v < structureFlagTable.length; v++) {
if (key.toLowerCase() == structureFlagTable[v][0].toLowerCase()) {
return structureFlagTable[v][colIndex];
}
}
}
//###End.###==>
//###Start.自动打包###
var packageId;
function packageTar(url) {
var startBuildUrl = url + "/batchTasks/task/Tar_CompilerMachine/execute";
var getBuildUrl = url + "/batchTasks/task/Tar_CompilerMachine/";
//var structureName = url.substring(url.lastIndexOf("/")+1);
var structureName = url.substring(url.lastIndexOf("=") + 1);
setStructureTableValue(structureName, 1, 3);
//同步启动构建任务
ajaxMethod(startBuildUrl, "get", true, null, -1);
//异步处理返回结果
function inner() {
ajaxMethod(getBuildUrl, "post", true, showBuildResult, -1);
}
setStructureTableValue(structureName, 4, setInterval(inner, 5000));
}
//处理返回结果, 并显示在页面上
function showBuildResult(responseContext) {
var divShow = window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResult");
var regPattern = /<tr\s+class=\"build-row\s+no-wrap\s+\">[\s\S]*?<\/tr>/;
var struNamePattern = /<a\s+href=\"\/job\/[\s\S]*?\/\">([\s\S]*?)<\/a>/;
var regPat1 = /src=\"/g;
var regPat2 = /href=\"/g;
var result = (regPattern.exec(responseContext) != null && regPattern.exec(responseContext)[0] != null) ? (regPattern.exec(responseContext)[0]).replace(regPat1, "src=\"http://192.168.199.61:8080").replace(regPat2, "href=\"http://192.168.199.61:8080") : "";
var structureName = (struNamePattern.exec(responseContext) != null && struNamePattern.exec(responseContext)[1] != null) ? struNamePattern.exec(responseContext)[1] : "";
divShow.innerHTML = "<table>" + result + "</table>";
if (result.indexOf("Success") != -1) {
window.clearInterval(getStructureTableValue(structureName, 4));
var tarBagUrl = "http://192.168.199.61:9082/"; //将出包后的源码默认URL路径
setStructureTableValue(structureName, 2, tarBagUrl + getStructureTableValue(structureName, 2));
setStructureTableValue(structureName, 1, 5);
if (getStructureTableValue(structureName, 2).indexOf(tarBagUrl) != -1) {
window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "<font color='green'> " + structureName + " 成功出包!</font>";
}
}
else if (divShow.innerHTML.indexOf("Failed") != -1) {
window.clearInterval(getStructureTableValue(structureName, 4));
window.clearInterval(flagTableID);
setStructureTableValue(structureName, 1, 4);
window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "<font color='red'>出包失败......</font>";
}
else {
window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "<font color='green'>出包中...</font>";
}
}
function stringTime(timeString) {
if (timeString != "") {
var dateTime = timeString.split(' ');
var tempDate = ("00" + dateTime[0]).replace(/\-/g, "-00").split("-");
var tempTime = ("00" + dateTime[1]).replace(/\:/g, ":00").split(":");
var date = "";
var time = "";
for (var v = 0; v < tempDate.length; v++) {
if (tempDate[v].length != 6) {
date += tempDate[v].substring(tempDate[v].length - 2, 4) + "-";
}
else {
date += tempDate[v].substring(tempDate[v].length - 4, 6) + "-";
}
}
date = date.substring(0, date.length - 1);
for (var v = 0; v < tempTime.length; v++) {
time += tempTime[v].substring(tempTime[v].length - 2, 4) + "-"
}
time = time.substring(0, time.length - 1);
return date + "_" + time;
}
}
//###End.###==>
JavaScript跨站请求转发器, 一般处理程序
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Configuration; using System.Net; using System.IO; using System.Text; namespace WebSite.Common { /// <summary> /// JS跨站转发器, 用来解决JavaScript无法跨站传输的问题 /// </summary> public class XSSTransferHandler : IHttpHandler, System.Web.SessionState.IRequiresSessionState { private static readonly string StructureUrl = ConfigurationManager.AppSettings["StructureUrl"]; public void ProcessRequest(HttpContext context) { context.Response.Cache.SetCacheability(HttpCacheability.NoCache); context.Response.ContentType = "text/html"; if ((context.Session["_userRoles"] as List<string>).Count != 0) { if ((context.Session["_userRoles"] as List<string>).Contains("开发负责人") || (context.Session["_userRoles"] as List<string>).Contains("管理员")) { if (!string.IsNullOrEmpty(context.Request.Url.PathAndQuery) && context.Request.Url.PathAndQuery.StartsWith("/common/XSSTransferHandler.ashx?url=")) { string newUrl = context.Request.Url.AbsoluteUri.Replace("http://" + context.Request.Url.Authority + "/common/XSSTransferHandler.ashx?url=", StructureUrl); HttpWebRequest hwrRequest = HttpWebRequest.Create(newUrl) as HttpWebRequest; //添加请求头, 其中Accept、Content-Length、Content-Type、User-Agent只能通过如下方式设置 hwrRequest.ContentType = "application/x-www-form-urlencoded"; //表头的格式必须要写,否则请求响应的页面得不到要传递的值 foreach (string str in context.Request.Headers.Keys) { switch (str) { case "Accept": hwrRequest.Accept = context.Request.Headers["Accept"]; break; case "Connection": break; case "Content-Length": hwrRequest.ContentLength = int.Parse(context.Request.Headers["Content-Length"]); break; case "Content-Type": hwrRequest.ContentType = context.Request.Headers["Content-Type"]; break; case "Expect": break; case "Date": break; case "Host": break; case "If-Modified-Since": break; case "Range": break; case "Referer": break; case "Transfer-Encoding": break; case "User-Agent": hwrRequest.UserAgent = context.Request.Headers["User-Agent"]; break; default: hwrRequest.Headers.Add(str, context.Request.Headers[str]); break; } } switch (context.Request.HttpMethod) { case "GET": hwrRequest.Method = "GET"; break; case "POST": hwrRequest.Method = "POST"; //hwrRequest.ContentType = "application/x-www-form-urlencoded"; //表头的格式必须要写,否则请求响应的页面得不到要传递的值 string postBodyStr = ""; //Post请求包体内容 //获取POST请求包体 using (Stream stream = context.Request.InputStream) using (StreamReader sReader = new StreamReader(stream, Encoding.UTF8)) { try { postBodyStr = sReader.ReadToEnd(); //读取请求参数的包体内容 if (!string.IsNullOrEmpty(postBodyStr)) { byte[] postBodyBytes = Encoding.Default.GetBytes(postBodyStr);//传递的值 hwrRequest.ContentLength = postBodyBytes.Length; //把传递的值写到流中 using (System.IO.Stream newStream = hwrRequest.GetRequestStream()) { try { newStream.Write(postBodyBytes, 0, postBodyBytes.Length); } catch (Exception ex) { throw ex; } } } } catch (Exception ex) { throw ex; } } break; default: throw new HttpException("未支持的请求方式!"); } //处理返回信息 using (HttpWebResponse hwrResponse = hwrRequest.GetResponse() as HttpWebResponse) using (Stream stream = hwrResponse.GetResponseStream()) using (StreamReader sReader = new StreamReader(stream, Encoding.UTF8)) { try { string responseHTML = sReader.ReadToEnd(); context.Response.Write(responseHTML); } catch (Exception ex) { throw ex; } } } } else { context.Response.Redirect("~/Default.aspx"); } } else { context.Response.Redirect("~/Default.aspx"); } } public bool IsReusable { get { return false; } } } }
需要着重说明的事项:
1. 尤其要看跨站安全性部分的写法, 平时注意一下吧. 我这个纯内网环境, 内部各种服务器, 业务复杂一下, 安全性要求不高.
跨域 XMLHttpRequest 请求
普通网页能够使用XMLHttpRequest对象发送或者接受服务器数据, 但是它们受限于同源策略. 扩展可以不受该限制. 任何扩展只要它先获取了跨域请求许可,就可以进行跨域请求。
注意:页面内容脚本不能直接发起跨域请求. 然而, 任何一个页面内容脚本都可以发送消息给父扩展,请求父扩展发起一次跨域请求。关于使用这一技术的例子,请参照contentscript_xhr example.
扩展所属域
每个正在运行的扩展都存在于自己独立的安全域里. 当没有获取其他权限时,扩展能够使用XMLHttpRequest获取来自安装该扩展的域的资源. 例如, 假设有一个扩展包含一个叫config.json的JSON配置文件,该文件位于config_resources?目录, 那么该扩展能够使用下面这段代码获取文件内容:
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = handleStateChange; // Implemented elsewhere.
xhr.open("GET", chrome.extension.getURL('/config_resources/config.json'), true);
xhr.send();
如果某个扩展希望访问自己所属域以外的资源,比如说来自http://www.google.com的资源(假设该扩展不是来自www.google.com), 浏览器不会允许这样的请求,除非该扩展获得了相应的跨域请求允许。
获取跨域请求允许
通过添加域名或者域名匹配到manifest文件的permissions段, 该扩展就拥有了访问除了自己所属域以外的其他域的访问权限.
{
"name": "My extension",
...
"permissions": [
"http://www.google.com/"
],
...
}跨域允许设置可以使用完整域名, 例如:
"http://www.google.com/"
"http://www.gmail.com/"
或者使用模式匹配, 例如:
"http://*.google.com/"
"http://*/"
模式匹配"http://*/" 表示可以发起到所有域的HTTP请求. 注意在这里, 模式匹配有点像内容脚本匹配, 但是这里的任何域名后的路径信息都被忽略
这里还需要注意访问权限是根据访问协议(匹配模式里的http或者https或者其他协议名)及域名来授予的. 例如某个扩展希望同时基于https和http协议访问某个域或者某些域, 那么它必须分别获取基于这两种协议的访问允许(类似下面这样的声明):
"permissions": [
"http://www.google.com/",
"https://www.google.com/"
]
安全性考虑
每当使用通过XMLHttpRequest获取的资源时, 你编写的背景页需要注意不要成为跨域脚本的牺牲品. 特别注意避免使用像下面这样的危险API:
background.html
===============
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
// 警告! 这里有可能执行了一段恶意脚本!
var resp = eval("(" + xhr.responseText + ")");
...
}
}
xhr.send();
background.html
===============
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
// 警告! 这样处理有可能被注入恶意脚本!
document.getElementById("resp").innerHTML = xhr.responseText;
...
}
}
xhr.send();实际上我们应该首选不会执行脚本的安全API:
background.html
===============
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
// JSON解析器不会执行攻击者设计的脚本.
var resp = JSON.parse(xhr.responseText);
}
}
xhr.send();
background.html
===============
var xhr = new XMLHttpRequest();
xhr.open("GET", "http://api.example.com/data.json", true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
// innerText不会给攻击者注入HTML元素的机会.
document.getElementById("resp").innerText = xhr.responseText;
}
}
xhr.send();另外在使用通过协议HTTP获取的资源时要特别小心. 如果你开发的扩展被应用在恶意网络环境中,网络攻击者(又叫 "中间人攻击") 可能篡改服务器响应内容从而可能攻击你编写的扩展. 事实上,你应该尽可能地首选使用HTTPS协议
第二个要注意的地方: 在JS跨站请求转发器的代码里, 需要构造HttpWebRequest请求, 构造请求时, 下述集中请求参数必须通过Request的属性来设置, 不能用Add和Set.
下面列出 Headers中不能add或者Set的名字 及解决办
Accept | 由 Accept 属性设置。 |
Connection | 由 Connection 属性和 KeepAlive 属性设置。 |
Content-Length | 由 ContentLength 属性设置。 |
Content-Type | 由 ContentType 属性设置。 |
Expect | 由 Expect 属性设置。 |
Date | 由系统设置为当前日期。 |
Host | 由系统设置为当前主机信息。 |
If-Modified-Since | 由 IfModifiedSince 属性设置。 |
Range | 由 AddRange 方法设置。 |
Referer | 由 Referer 属性设置。 |
Transfer-Encoding | 由 TransferEncoding 属性设置(SendChunked 属性必须为 true)。 |
User-Agent | 由 UserAgent 属性设置。 |