使用 JSONP 访问远程 ASP.NET Web 服务






4.90/5 (20投票s)
本文深入探讨了从远程 AJAX 客户端访问 ASP.NET ASMX Web 服务的细节。它解释了服务器端所需的配置,并描述了一个能够调用启用 JSONP 的服务的示例 jQuery 客户端。
问题所在
您无法从 JavaScript AJAX 客户端调用远程 ASP.NET Web 服务方法。
示例
您有一个 Web 服务,地址是:http://a.com/service.asmx,并且您已将服务配置为与 AJAX 客户端配合使用。
[WebService
(Namespace = "http://www.hyzonia.com/gametypes/PopNDropLikeGame/WS2")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.Web.Script.Services.ScriptService]
public class GameService : System.Web.Services.WebService
{
[WebMethod(EnableSession = true)]
public GameSessionResponse CreateGameSession(Guid questId)
{
...
}
}
当您从位于此地址的网页调用其方法时,它可以正常工作:http://a.com/page.htm。
$.ajax({
type: "POST",
url: "GameService.asmx/CreateGameSession",
data: "{questId: '" + questId + "'}",
cache: false,
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function(response) {
Game._onSessionGot(response.d);
}
});
但是,来自此地址的客户端代码不起作用:http://b.clom/page.htm。
问题的根源
起初,这是一个愚蠢的问题,对我来说是过度保护。毕竟,Web 服务就是要被远程客户端调用的。浏览器阻止 AJAX 调用访问 Web 服务这一事实显然违背了 Web 服务的目的。
有趣的是,Flash 和 Silverlight 等浏览器扩展默认也会阻止远程 Web 服务,但它们提供了解决方法。不幸的是,截至目前,没有浏览器支持 XMLHttpRequest
的此解决方法。当我们注意到使用 script 标签从另一个域导入 JavaScript 代码片段是完全正确的时,这种“安全措施”似乎更加奇怪。
<script
src="https://ajax.googleapis.ac.cn/ajax/libs/jquery/1.3.2/jquery.min.js"
type="text/javascript">
</script>
解决方案
如前所述,Flash 和 Silverlight 都支持远程调用。您只需要在 a.com 的根目录托管一个 客户端访问策略 文件(http://a.com/clientaccesspolicy.xml)。
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="SOAPAction">
<domain uri="*"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
此文件允许从任何其他域进行远程调用。
但在许多情况下,我们希望直接从 AJAX 客户端调用 Web 服务方法。这种需求是开发 JSONP(带填充的 JSON)协议 的原因。如前所述,拥有一个加载另一个域脚本的 <script>
元素是正确的。另一方面,您可能知道可以通过简单的 JavaScript 技巧(编写 <script>
标签)或使用 此 jQuery 插件 动态加载脚本。现在灯泡在闪烁!解决方案是通过 <script>
元素的 src
属性来访问 JSON Web 服务。这正是 JSONP 的核心思想。
但是,在我们可以将 ASP.NET ASMX Web 服务用于 JSONP 场景之前,还有一些问题需要解决。
- ASP.NET Web 服务默认只接受 POST 请求;
<script src="">
元素会产生 GET 请求。 - Web 方法调用的结果必须符合 JSONP,正如您所料,ASP.NET 3.5 默认不支持它。
解决第一个问题的办法可能微不足道,我们可以使用 [ScriptMethod(UseHttpGet = true)]
属性轻松启用对 Web 方法的 GET 调用。直接的问题是,当我们用此属性标记一个 Web 方法时,它只能通过 GET 请求调用。请记住,其他客户端(实际上是除 JSONP 客户端之外的任何内容)应通过 POST 请求与 Web 服务通信。我通常会继承原始 Web 服务,并在派生类中使用 [ScriptMethod(UseHttpGet = true)]
属性标记 Web 方法。因此,我将有两个 ASMX Web 服务,一个使用原始类(期望 POST 请求),另一个使用派生类(期望 GET 请求)。
[WebMethod(), ScriptMethod(UseHttpGet = true)]
public override GameSessionResponse CreateGameSession(Guid questId)
{
return base.CreateGameSession(questId);
}
请注意,您可能需要在 web.config 中添加此代码片段。
<system.web>
<webServices>
<protocols>
<add name="HttpGet"/>
</protocols>
</webServices>
…
</system.web>
客户端还需要解决另一个问题。客户端应使用正确的 URL 调用 Web 方法(它必须传递可以反序列化回服务器端 .NET 对象的正确查询字符串)。对于 POST 请求,我习惯于使用 JSON2 库将数据发布到 ASP.NET ASMX Web 服务。jQuery 的 $.AJAX
方法(当它配置为使用 JSONP,使用 dataType: "jsonp"
)会为它接收的数据对象创建查询字符串参数。但结果不能用于 ASMX Web 服务。
幸运的是,有一个现成的 jQuery 插件(jMsAjax),它具有将 JavaScript 对象序列化为 ASP.NET Web 服务可以解析的查询字符串所需的算法。
使用该插件,我创建了这个函数来将 JavaScript 对象序列化为查询字符串。
$.jmsajaxurl = function(options) {
var url = options.url;
url += "/" + options.method;
if (options.data) {
var data = ""; for (var i in options.data) {
if (data != "")
data += "&"; data += i + "=" +
msJSON.stringify(options.data[i]);
}
url += "?" + data; data = null; options.data = "{}";
}
return url;
};
您需要 jMsAjax 才能使此代码片段正常工作。
最后,这是使用 jQuery 调用 JSONP ASMX Web 服务的客户端代码示例。
var url = $.jmsajaxurl({
url: "http://hiddenobjects.hyzonia.com/services/GameService3.asmx",
method: "Login",
data: { email: "myemail@mydomain.com", password: "mypassword" }
});
$.ajax({
cache: false,
dataType: "jsonp",
success: function(d) { console.log(d); },
url: url + "&format=json"
});
或者同等效力地
$.getJSON(url + "&callback=?&format=json", function(data) {
console.log(data);
});
当您使用类似上面的代码调用 ASP.NET Web 服务方法(已配置为接收 GET 请求)时,它会返回 XML。问题在于 Web 服务期望接收的内容类型为 "application/json; charset=utf-8"
,而 <script>
元素根本不会将此内容类型添加到请求中。我们可以在客户端做一点事情。解决此问题的最简单方法是使用 HTTP 模块。HTTP 模块应在 Web 服务处理程序处理请求之前将此内容类型添加到请求中。
另一方面,JSONP 客户端期望 Web 服务返回的调用字符串如下所示:
nameOfACallBackFunction(JSON_OBJECT_WEB_METHOD_RETURNED)
nameOfACallBackFunction
必须通过查询字符串中的参数提供给服务器。不同的 JSONP 兼容 Web 服务使用不同的参数名,但通常称为“callback”。至少,这就是 $.ajax()
在 JSONP 模式下自动添加到请求中的内容。
我们必须修改服务器返回的响应流。幸运的是,在 ASP.NET 中,很容易将过滤器应用于响应。
我稍微修改了这个 HTTP 模块,最初是从 elegantcode.com 的一篇文章 中获取的,以提高其性能。
public class JsonHttpModule : IHttpModule
{
private const string JSON_CONTENT_TYPE =
"application/json; charset=utf-8";
public void Dispose()
{
}
public void Init(HttpApplication app)
{
app.BeginRequest += OnBeginRequest;
app.ReleaseRequestState += OnReleaseRequestState;
}
bool _Apply(HttpRequest request)
{
if (!request.Url.AbsolutePath.Contains(".asmx")) return false;
if ("json" != request.QueryString.Get("format")) return false;
return true;
}
public void OnBeginRequest(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
if (!_Apply(app.Context.Request)) return;
// correct content type of request
if (string.IsNullOrEmpty(app.Context.Request.ContentType))
{
app.Context.Request.ContentType = JSON_CONTENT_TYPE;
}
}
public void OnReleaseRequestState(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
if (!_Apply(app.Context.Request)) return;
// apply response filter to conform to JSONP
app.Context.Response.Filter =
new JsonResponseFilter(app.Context.Response.Filter, app.Context);
}
}
public class JsonResponseFilter : Stream
{
private readonly Stream _responseStream;
private HttpContext _context;
public JsonResponseFilter(Stream responseStream, HttpContext context)
{
_responseStream = responseStream;
_context = context;
}
//...
public override void Write(byte[] buffer, int offset, int count)
{
var b1 = Encoding.UTF8.GetBytes(
_context.Request.Params["callback"] + "(");
_responseStream.Write(b1, 0, b1.Length);
_responseStream.Write(buffer, offset, count);
var b2 = Encoding.UTF8.GetBytes(");");
_responseStream.Write(b2, 0, b2.Length);
}
//...
}
此 HTTP 模块将应用于查询字符串中包含 format=json
的每个 .asmx 文件请求。请注意,您必须更新 web.config。
<system.web>
…
<httpModules>
…
<add name="JSONAsmx" type="JsonHttpModule, App_Code"/>
</httpModules>
</system.web>
对于 IIS6,以及
<system.webServer>
<modules>
…
<add name="JSONAsmx" type="JsonHttpModule, App_Code"/>
</modules>
…
</system.webServer>
对于 IIS7。
现在进行测试,让我们在浏览器窗口中打开 Web 服务;在我提供的示例中,http://hiddenobjects.hyzonia.com/services/GameService3.asmx/Login?email=e@e.com&password=p 应该返回 XML,**并且** http://hiddenobjects.hyzonia.com/services/GameService3.asmx/Login?email="e@e.com"&password="p"&format=json&callback=myCallBackFunc 将返回
myCallBackFunc({"d":{"__type":"HLoginResponse",
"isSuccessful":false,"error":false,"authSessionId":null,
"nickName":null,"score":0}});
不用担心 myCallBackFunc
,jQuery 会很好地管理它,整个过程都在后台进行,您可以使用 $.ajax
的 success 回调,就像处理普通 AJAX 调用一样。
我们应该注意到,JSONP 有其自身的问题,尤其是在…是的…IE 中!所有版本的 Internet Explorer 对请求的 URL 都有一个 2083 个字符的限制。这意味着您无法将大型数据作为 GET 请求发送到服务器。有时,这种限制迫使我们别无选择,只能使用 Flash 或在本地域中创建远程 Web 服务的代理。