有時寫一個Ajax Service,在寫的時候9成9都是自己網站用,那時多半不會考慮跨網站安全性問題,突然有其他同事說他也要用,就要回頭變動程式可以吃Jsonp,幾次下來就覺得要從根本解起,就寫了JsonPlusResult + ControllerPlus,由底層來處理這些事情,在開發的時候可以使用相同的習慣,做到多樣事情。
而內建的JsonConverter,在處理Json的Property轉換,遇到循環參考會出錯,但用ORM一定會遇到這問題,如Order.OrderDetails與OrderDetail.Order,這樣簡單的結構內建的JsonConverter就無法轉換了,後來改用Json.Net來處理轉換,也把這功能放入JsonPlusResult中。
有時寫一個Ajax Service,在寫的時候9成9都是自己網站用,那時多半不會考慮跨網站安全性問題,突然有其他同事說他也要用,就要回頭變動程式可以吃Jsonp,幾次下來就覺得要從根本解起,就寫了JsonPlusResult + ControllerPlus,由底層來處理這些事情,在開發的時候可以使用相同的習慣,做到多樣事情。
而內建的JsonConverter,在處理Json的Property轉換,遇到循環參考會出錯,但用ORM一定會遇到這問題,如Order.OrderDetails與OrderDetail.Order,這樣簡單的結構內建的JsonConverter就無法轉換了,後來改用Json.Net來處理轉換,也把這功能放入JsonPlusResult中。
Ajax跨網域安全性
Ajax跨網域安全性問題不是Server端抯擋跨網域的連線,而是用戶的Browser為了安全性去抯擋跨網域連線,為了決解這問題,之前查有發現三個解法:
- 因為只有Broswer擋,所以可以Server對跨網域Server下載資料,前端在和同網域Server下載資料,但方法最囉嗦最麻煩,不建議使用。
-
CORS
Cross-Origin Resource Sharing(CORS)是W3C的提案,可以在HttpHeader中設定那些跨網域的網站可以存取,但只限有實做XMLHttpRequest Level 2的Browser,如下IE8(Windows 7 version), Safari 4+, Chrome and Firefox 3.5+,很可惜的萬惡的IE6-8(XP version)不支援,所以就放棄這個選項。
實作方法如下:
public class AllowCrossSiteJsonAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { filterContext.RequestContext.HttpContext.Response.AddHeader("Access-Control-Allow-Origin", "*"); //或某網域 //filterContext.RequestContext.HttpContext.Response.AddHeader("Access-Control-Allow-Origin", "http://mvc.tw"); base.OnActionExecuting(filterContext); } } [AllowCrossSiteJson] public ActionResult API() { return Json(model); } //也可以在global.asax中增加,這樣可以不改變Controller,但前提是所有的Action都可以對外 protected void Application_BeginRequest(object sender, EventArgs e) { HttpContext.Current.Response.AddHeader("Access-Control-Allow-Origin", "*"); }
-
JSONP
JSONP其實是鑽Broswer的漏洞,因為 HTML<script>
標籤在瀏覽器裡不遵守同源策略,可以想像送一個跨網站Request,下載js檔,js檔中呼叫,事先寫好的function,而參數是物件字面量表示法(跟JSON是相同的格式),達到呼叫遠端並下載的作用,但ASP.NET MVC中沒有內建處理的ActionResult(可能是漏洞的關係吧),而且還要處理Callback,會比CORS麻煩,但本篇就是要教你如何簡化這些麻煩。
範例:
//js端 //先寫好callback function myFunction(jsonData){ //do something } //產生<script>標籤 document.write("<script src='http://CrossDomainSite/api?jsonCallback=myFunction'></script>"); //Server端 public ActionResult Api(string jsonCallback) { //回傳的是Javascript return Javascript("{0}({1});", jsonCallback, model.ToJson()); }
//回傳的資料 <script> //會呼叫事先寫好的function myFunction( { Name:"Wade", Site:"Mvc.Tw" } ); </script> //如果用jQuery不用那麼麻煩 $.ajax({ type:"post", url:"http://localhost:30881/JsonTest/Data", dataType : "jsonp", //只要設定jsonp就會幫你處理function與url與<script> success:function (data) { //do something }, });
JsonPlusReslut原始碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
//可以將ObjectExtensions與ControllerPlus可以移到你自己相關的Code中
namespace System
{
public static class ObjectExtensions
{
/// <summary>
/// 轉成Json格式
/// </summary>
/// <returns></returns>
public static string ToJson(this object target)
{
return JsonConvert.SerializeObject(target, Formatting.None);
}
}
}
namespace System.Web.Mvc
{
public class ControllerPlus : Controller
{
//覆寫Controller.Json方法,回傳JsonPlusResult
protected override JsonResult Json(object data, string contentType, Encoding contentEncoding, JsonRequestBehavior behavior)
{
return new JsonPlusResult(data) { ContentType = contentType, ContentEncoding = contentEncoding, JsonRequestBehavior = behavior };
}
}
/// <summary>
/// 處理Json與Jsonp的Request與Json的轉換並解決循環參考
/// </summary>
public class JsonPlusResult : JsonResult
{
public JsonPlusResult(object model)
{
this.Data = model;
}
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
{
throw new ArgumentNullException("context");
}
if ((this.JsonRequestBehavior == JsonRequestBehavior.DenyGet) && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("DenyGet");
}
//Json Padding的callback
string jsoncallback = GetCallbackName(context);
//ContentType
HttpResponseBase response = context.HttpContext.Response;
if (this.ContentEncoding != null)
{
response.ContentEncoding = this.ContentEncoding;
}
//
if (!string.IsNullOrEmpty(this.ContentType))
{
response.ContentType = this.ContentType;
}
else
{
if (string.IsNullOrEmpty(jsoncallback))
{
response.ContentType = "application/json";
}
else
{
response.ContentType = "application/x-javascript"; //json Padding
}
}
string dataToJson = this.Data.ToJson();
if (response.ContentType == "application/x-javascript")
{
response.Write(string.Format("{0}({1})", jsoncallback, dataToJson)); //json Padding
}
else
{
response.Write(dataToJson);
}
}
private static string GetCallbackName(ControllerContext context)
{
string result = context.HttpContext.Request["jsoncallback"] as string;
if (!string.IsNullOrWhiteSpace(result))
{
return result;
}
result = context.HttpContext.Request["callback"] as string;
if (!string.IsNullOrWhiteSpace(result))
{
return result;
}
return null;
}
}
}
使用方法
//可以使用繼承ControllerPlus
public class JsonTestController : ControllerPlus //繼承
{
public ActionResult Index()
{
//跟以前一樣
return Json(model);
}
}
//也可以直接使用JsonPlusResult
public class JsonTestController : Controller //不繼承
{
public ActionResult Index()
{
return new JsonPlusResult(model);
}
}
qunit測試結果
<script type="text/javascript">
//json
$(function () {
test("json", function () {
$.ajax({
async:false,
type:"post",
url:"@Url.Action("Data", "JsonTest")",
dataType : "json",
success:function (data) {
equal(data.Name, "Wade", "Json資料比對");
},
error:function (xhr,status,errorThrown) {
ok(false, errorThrown);
}
});
});
test("jsonp auto callback", function () {
$.ajax({
async:false,
type:"post",
url:"@Url.Action("Data", "JsonTest")" ,
dataType : "jsonp",
success:function (data) {
equal(data.Name, "Wade", "Json資料比對");
},
error:function (xhr,status,errorThrown) {
ok(false, errorThrown);
}
});
});
test("jsonp custom callback", function () {
$.ajax({
async:false,
type:"post",
url:"@Url.Action("Data", "JsonTest")",
jsonpCallback:"customJsonpCallback",
dataType : "jsonp",
error:function (xhr,status,errorThrown) {
ok(false, errorThrown);
}
});
});
});
function customJsonpCallback(data, status) {
equal(data.Name, "Wade", "Json資料比對");
}
</script>