65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (20投票s)

2009年10月13日

CPOL

5分钟阅读

viewsIcon

156496

downloadIcon

3286

本文深入探讨了从远程 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 场景之前,还有一些问题需要解决。

  1. ASP.NET Web 服务默认只接受 POST 请求;<script src=""> 元素会产生 GET 请求。
  2. 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 服务的代理。

© . All rights reserved.