功能已经做完后, 发现有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 = "启动编译任务< " + url.substring(url.lastIndexOf("/")+1) + " >..."; //同步方式获取最近更新构建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 = "编译任务开始< " + url.substring(url.lastIndexOf("/")+1) + " >..."; //异步启动构建任务 ajaxMethod(startReqUrl, "get", true, null, -1); window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = "编译任务< " + url.substring(url.lastIndexOf("/")+1) + " >正在执行..."; //异步处理返回结果 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 = "编译任务开始..."; 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 = / /; var regPat1 = /src=\"/g; var regPat2 = /href=\"/g; var removeBar = /[\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, "").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 = " < " + structureName + " >编译成功, 正在打包..."; } 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 = " < " + structureName + " >编译失败, 任务终止..."; } else { window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = " 编译任务< " + structureName + " >正在执行..."; } } //选择多个应用时, 在隐藏域中会生成构建任务列表, 根据构建任务列表执行多个构建任务 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 = / [\s\S]*?<\/tr>/; var struNamePattern = / ([\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 = " " + result + "
"; 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 = " " + structureName + " 成功出包!"; } } 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 = " 出包失败......"; } else { window.document.getElementById("ctl00_ContentPlaceHolder1_divBuildResultStatus").innerHTML = " 出包中..."; } } 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.###==>
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{ ////// JS跨站转发器, 用来解决JavaScript无法跨站传输的问题 /// 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).Count != 0) { if ((context.Session["_userRoles"] as List ).Contains("开发负责人") || (context.Session["_userRoles"] as List ).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 属性设置。 |