WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

WCF と jQuery AJAX

by WebSurfer 2015年10月15日 21:01

Web サービスは "Legacy Technology" なので Windows Communication Foundation (WCF) を使うようにと言われています。なので、今さらながらですが、先の記事 jQuery AJAX と Web サービスで作ったサンプルと同様なものを WCF で実装してみました。

AJAX 対応 WCF サービス

作成に当たって参考にしたのは以下の 2 つの記事です。しかし、読んだだけではほとんど意味が分かりません。(汗)

自分の手を動かして作ってみれば少しは理解が深まるであろうということで、自分の環境(Vista SP2 32-bit の IIS7、ASP.NET 4, Visual Studio 2010 Professional、ASP.NET Web Forms アプリ)で実際にやってみました。やってみていろいろ分かったことがありましたので備忘録として書いておきます。

まず、開発マシンの IIS7 上で動く既存の ASP.NET Web Forms アプリに、上の画像のように Visual Studio の[新しい項目の追加(W)...]メニューからテンプレートを使って[AJAX 対応 WCF サービス]を CarService.svc という名前で追加します。([WCF サービス]の方は従来の SOAP を使う WCF サービス)

CarService.svc を追加すると以下のコードが自動生成されます。今回の記事では Web アプリケーションプロジェクトを使っています。Web サイトプロジェクトの場合は、コードビハンド CarService.svc.cs が CarService.cs という名前になって App_Code フォルダに置かれる、アセンブリ名 / 名前空間名が付与されないという違いがありますので注意してください。

CarService.svc(xxx は名前空間名)

<%@ ServiceHost
  Language="C#"
  Debug="true"
  Service="xxx.CarService"
  CodeBehind="CarService.svc.cs"
%>

CarService.svc.cs(xxx は名前空間名)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;

namespace xxx
{
  [ServiceContract(Namespace = "")]
  [AspNetCompatibilityRequirements(RequirementsMode = 
                AspNetCompatibilityRequirementsMode.Allowed)]
  public class CarService
  {
    // HTTP GET を使用するために [WebGet] 属性を追加します 
    // (既定の ResponseFormat は WebMessageFormat.Json)。
    // XML を返す操作を作成するには、
    // [WebGet(ResponseFormat=WebMessageFormat.Xml)] を追加し、
    // 操作本文に次の行を含めます。
    // WebOperationContext.Current.OutgoingResponse.ContentType = 
    //     "text/xml";
    [OperationContract]
    public void DoWork()
    {
      // 操作の実装をここに追加してください
      return;
    }

    // 追加の操作をここに追加して、[OperationContract] とマーク
    // してください
  }
}

web.config への追加(xxx は名前空間名)

<system.serviceModel>
  <behaviors>
    <endpointBehaviors>
      <behavior name="xxx.CarServiceAspNetAjaxBehavior">
        <enableWebScript />
      </behavior>
    </endpointBehaviors>
  </behaviors>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true"
    multipleSiteBindingsEnabled="true" />
  <services>
    <service name="xxx.CarService">
      <endpoint address=""
        behaviorConfiguration="xxx.CarServiceAspNetAjaxBehavior"
        binding="webHttpBinding"
        contract="xxx.CarService" />
    </service>
  </services>
</system.serviceModel>

上記は、web.config に <enableWebScript /> とあることから分かるように、ASP.NET AJAX の ScriptManager を使用する Web ページからサービスを使用するための WCF AJAX サービスのコードです。

これらのコードを、上の 2 つの記事を参考に、JavaScript / jQuery を使用する Web ページからアクセスできるように書き換えてみました。

上に紹介した ASP.NET を使用せずに WCF AJAX サービスを作成する方法 に書いてあるように、以下の 3 つの手順に分けて説明します。

(1) ブラウザーからアクセスできる AJAX エンドポイントの作成
(2) AJAX 互換サービス コントラクトの作成
(3) WCF AJAX サービスへのアクセス


(1) ブラウザーからアクセスできる AJAX エンドポイントの作成

AJAX エンドポイントを作成するためには、

  1. .svc ファイルの @ServiceHost の Factory 属性に WebServiceHostFactory を設定する、または
  2. web.config の <system.serviceModel> 要素で WebHttpBinding と WebHttpBehavior を使用してエンドポイントを構成する

のいずれかの方法を取るのだそうです。具体例は上の記事(前者)のサンプルコードを参照してください。

1 の方が簡単そうに思えますが、実は 2 の方は web.config に自動生成されて追加されたコード(上のコードを見てください)の中の <enableWebScript /> を <webHttp /> に書き換えるだけで済みますので、手間的には大差はないです。

今回は 2 の方法(.svc ファイルには手を加えないで、web.config に自動生成されたコードの中の <enableWebScript /> を <webHttp /> に書き換え)を取りました。

エンドポイントアドレス名を指定してサービスを呼び出したい場合は、自動生成された address="" を修正してください。例えば、address="ajaxEndpoint" とすると、carservice.svc/ajaxEndpoint/GetAllCars でアクセスできます。(ちなみに、そのように address を設定すると、carservice.svc/GetAllCars では HTTP 404 Not Found になります)

1 の方法を取る場合、web.config に自動生成されたコードは削除した方がよさそうです。<enableWebScript /> を <webHttp /> に書き換えれば WebScriptEnablingBehavior では BodyStyle の Wrapped がサポートされてない等の問題は回避できますが、既存のサービスなどと競合してエラーになることがありましたので。


(2) AJAX 互換サービス コントラクトの作成

先の記事 ASP.NET AJAX と Web サービスの Web サービス(.asmx ファイル)のコードを CarService.svc.cs に移植します。

GetAllCars は GET 要求で呼び出せるようにしました。(JSON サービスを GET で要求するのは潜在的なセキュリティ上のリスクがあるそうです。詳しくはこのセクションの下の方で紹介した記事を見てください)

GetCarsByDoors は先の例と同様に POST 要求で呼び出すようにしました。サンプルコードは以下のようになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Text;

namespace WebApplication1
{
    // web.config で aspNetCompatibilityEnabled="true" と設定され
    // ているので [AspNetCompatibilityRequirements(...)] が必要
    [ServiceContract(Namespace = "")]
    [AspNetCompatibilityRequirements(RequirementsMode = 
        AspNetCompatibilityRequirementsMode.Allowed)]
    public class CarService
    {
        List<Car> cars = new List<Car> {
            new Car {Make="Audi",Model="A4",Year=1995,
                Doors=5,Colour="Red",Price=2995f},
            new Car {Make="Ford",Model="Focus",Year=2002,
                Doors=5,Colour="Black",Price=3250f},
            new Car {Make="BMW",Model="5 Series",Year=2006,
                Doors=4,Colour="Grey",Price=24950f},
            new Car {Make="Renault",Model="Laguna",Year=2000,
                Doors=5,Colour="Red",Price=3995f},
            new Car {Make="Toyota",Model="Previa",Year=1998,
                Doors=5,Colour="Green",Price=2695f},
            new Car {Make="Mini",Model="Cooper",Year=2005,
                Doors=2,Colour="Grey",Price=9850f},
            new Car {Make="Mazda",Model="MX 5",Year=2003,
                Doors=2,Colour="Silver",Price=6995f},
            new Car {Make="Ford",Model="Fiesta",Year=2004,
                Doors=3,Colour="Red",Price=3759f},
            new Car {Make="Honda",Model="Accord",Year=1997,
                Doors=4,Colour="Silver",Price=1995f}
        };

        [OperationContract]
        [WebGet(ResponseFormat = WebMessageFormat.Json, 
            BodyStyle = WebMessageBodyStyle.Wrapped)]
        public List<Car> GetAllCars()
        {
            return cars;
        }

        [OperationContract]
        [WebInvoke(ResponseFormat = WebMessageFormat.Json, 
            BodyStyle = WebMessageBodyStyle.Wrapped)]
        public List<Car> GetCarsByDoors(int doors)
        {
            var query = from c in cars
                        where c.Doors == doors
                        select c;

            return query.ToList();
        }
    }

    [DataContract]
    public class Car
    {
        [DataMember]
        public string Make { get; set; }

        [DataMember]
        public string Model { get; set; }

        [DataMember]
        public int Year { get; set; }

        [DataMember]
        public int Doors { get; set; }

        [DataMember]
        public string Colour { get; set; }

        [DataMember]
        public float Price { get; set; }
    }
}

ポイントは WebGetAttribute 属性と WebInvokeAttribute 属性の設定で、具体的には以下の通りです。

  • ResponseFormat = WebMessageFormat.Json を設定して、応答に XML ではなく JSON を明示的に指定します。
  • 上に紹介した記事(後者)には "WebGetAttribute または WebInvokeAttribute 属性に Wrapped の本文スタイルが必要です" とありますが、その通りにすると応答もラップされて {"GetAllCarsResult":[{"Colour":"Red", ... ,"Year":1997}]} というような形になります。
  • BodyStyle = WebMessageBodyStyle.Wrapped を削除すると "GetAllCarsResult" というラップはなくなりますが、今度は POST 要求で HTTP 400 Bad Request となってしまいます。なので、少なくとも POST 要求する方は BodyStyle = WebMessageBodyStyle.WrappedRequest とする必要があります。

今回は記事に従って BodyStyle = WebMessageBodyStyle.Wrapped とし、ラップを外す処置をクライアントスクリプトにコーディングしました。具体的には、下の「(3) WCF AJAX サービスへのアクセス」のセクションを見てください。

ラップすると、Web サービスの JSON 応答を "d" でラップするのと同様に、セキュリティ上のリスクの軽減に効果があるはずです。

Web サービスの JSON 応答は .NET 3.5 から "d" でラップされるようになりましたが、それは JSON サービスの脆弱性に起因する XSS 対策だそうです。そのメカニズムは以下の記事に詳しく書いてあります。

WCF では "<メソッド名>Result" でラップされますが、Web サービスで "d" でラップするのと同様に JSON 配列を返さないという効果があります。(JSON 配列は JavaScript として有効というところが問題だそうです)

あと、上に紹介した記事に書いてありますが、GET 要求を使うことにもリスクがあるそうです。

いろいろ条件が重ならないとリスクにはならないですが、「君子危うきに近寄らず」ということで、(1) ラップする、(2) POST 要求を使う、という 2 つのことは実施した方がよさそうです。(ちなみに、ASP.NET AJAX を利用する場合はデフォルトでそうなります)


(3) WCF AJAX サービスへのアクセス

これは先の記事 jQuery AJAX と Web サービス とほぼ同様な jQuery.ajax のコードを利用して可能です。

上記 (2) の手順で、GetAllCars は GET 要求で、GetCarsByDoors は POST 要求で取得するようにしたので、それぞれ以下のようなコードで要求をかけ、JSON 文字列の応答を得てそれを処置することができます。

function getAllCars() {
    $.ajax({
        type: "GET",
        url: "carservice.svc/GetAllCars",
                
        success: function (cars) {
            if (cars.hasOwnProperty('GetAllCarsResult')) {
                cars = cars.GetAllCarsResult;

                // ・・・cars の処置(省略)・・・
            }
        },
        error: function (jqXHR, textStatus, errorThrown) {
            // ・・・省略・・・
        }
    });
}
        
function getCarsByDoors() {
    $.ajax({
        type: "POST",
        url: "carservice.svc/GetCarsByDoors",
        data: '{"doors":' + $("#ddlDoors").val() + '}',
        contentType: "application/json; charset=utf-8",

        success: function (cars) {
            if (cars.hasOwnProperty('GetCarsByDoorsResult')) {
                cars = cars.GetCarsByDoorsResult;

                // ・・・cars の処置(省略)・・・
            }
        },
        error: function (jqXHR, textStatus, errorThrown) {
            // ・・・省略・・・
        }
    });                
}

POST の方には contentType: "application/json; charset=utf-8" の設定は必須です。それがないと HTTP 400 Bad Request となって失敗します。

success オプションに設定したコールバックの引数 cars にはパース済みの JavaScript オブジェクトが渡されます。上の「(2) AJAX 互換サービス コントラクトの作成」のセクションで述べたように "<メソッド名>Result" でラップされていますので、ラップを外す処置が必要です。具体的には上のコードを見てください。(命名規則が不明なので、ラップを外す処置をハードコーディングするのはちょっと不安なのですが)

Tags: , ,

AJAX

jQuery.ajax の data の型

by WebSurfer 2015年10月5日 14:38

jQuery.ajax を利用してデータをサーバーに送信するには、以下のような形で data オプションに送信するデータを設定しますが、そのデータ(下のコードで言うと jsonString)の型は何にすべきかという話を書きます。

function apiHeroesPost() {
    $.ajax({
        type: "POST",
        url: "api/heroes",
        data: jsonString,
        contentType: "application/json; charset=utf-8",
        success: function (data) {
            // ・・・省略・・・
        },
        error: function (jqXHR, textStatus, errorThrown) {
            // ・・・省略・・・
        }
    });
}

答を先に書くと、

  1. GET 要求(クエリ文字列としてデータを送信)の場合は JavaScript オブジェクト
  2. POST 要求(コンテンツとしてデータを送信)の場合、(a) 受け取る側が application/x-www-form-urlencoded 形式を期待している場合は JavaScript オブジェクト、(b) ASP.NET Web API のように JSON 文字列を期待している場合は JSON 文字列

を data オプションに設定します。上のコードは POST 要求で「(b) ASP.NET Web API のように JSON 文字列を期待している」場合です。以下、POST 要求については (b) を前提に書きます

jQuery の API Dcoumentation の説明によると、設定できる型は PlainObject or String or Array となっていますが、どれでもいいという訳ではありません。

その説明にある "It is converted to a query string, if not already a string. It's appended to the url for GET-requests." というところがポイントです。

具体的には、例えば、processData は設定しない(デフォルトの true)という条件で、

var j = { Id: 5, Name: "日本語" };  // j はオブジェクト
var jsonString = JSON.stringify(j); // jsonString は文字列

とした場合、GET 要求なら data:j とし、POST 要求なら data:jsonString とします。

data:j とすると j は application/x-www-form-urlencoded 形式に変換されて Id=5&Name=%E6%97%A5%E6%9C%AC%E8%AA%9E となり、GET 要求の際 URL にクエリ文字列として追加されます。(%E6%97%A5 ... %AA%9E は "日本語" の UTF-8 パーセントエンコーディング)

しかし、data:jsonString とすると、文字列は無変換なので、GET 要求の際そのままクエリ文字列として URL に追加されます。下の画像は Fiddler2 による GET 要求のキャプチャです。反転表示した部分を見てください。クエリ文字列が {"Id":5,"Name":"日本語"} となってるのが分かるでしょうか? (ブラウザに依存すると思いますが、IE9 では "日本語" は Shift_JIS になり、下の画像では 22 93 FA 96 7B 8C EA 22 というバイト列になっています)

不正なクエリ文字列

POST 要求の場合も、data の変換の仕方は同じ(オブジェクトはクエリ文字列形式に変換、文字列は無変換)になります。しかし、POST 要求はコンテンツとしてデータを送信しますので、GET 要求と違って data:j(j はオブジェクト)ではダメです。

data:jsonString として文字列を設定すればそのまま UTF-8 コードのコンテンツとして送信されますので、jsonString が有効な JSON 文字列なら期待通りの結果が得られるはずです。(注:contentType: "application/json; charset=utf-8" が前提です)

たぶん普通は、GET と POST の使い分けは以下のようにする(参考:jQuery - AJAX get() and post() Methods)と思います。

  • GET - Requests data from a specified resource
  • POST - Submits data to be processed to a specified resource

なので、データを送信する際は POST 要求、jQuery.ajax の data には JSON 文字列を設定ということしか頭になかったです。それでちょっとハマってしまったので、備忘録として残しておくことにしました。

Tags: ,

AJAX

パススルー認証

by WebSurfer 2015年9月13日 16:29

IIS マネージャーで[サイトの編集]を行う際、下の画像(Vista の IIS7 のものです)のように「パススルー認証」という言葉が出てきますが、これは一体何かという話を書きます。

パススルー認証

まず、そもそもパススルー認証とは一般的にどういう意味かですが広義には以下のようなことらしいです。

例えば、サーバー A とサーバー B の 2 つのサーバーがあり、サーバー B のみにユーザーの資格情報が保持されているとします。

クライアントがサーバー A にアクセスした際、サーバー A ではユーザー認証ができないので、サーバー A はサーバー B にユーザー認証を要求します。そのようなメカニズムをパススルー認証 (Pass-Through Authentication) と呼んでいるそうです。

MSDN の記事 Pass-Through Authentication の Figure 1 を見て、Active Directory ドメインサービス環境で統合 Windows 認証を使用している場合を考えると理解しやすいかもしれません。

で、それと上の画像の「パススルー認証」とどういう関係があるのかですが、それについては「サイトの編集」ダイアログのヘルプ(右上の ? ボタンをクリックすると表示される)に以下の説明があります。

"必要に応じて、[接続] をクリックして、物理パスに接続するための資格情報を指定することもできます。 資格情報を指定しない場合、Web サーバーはパススルー認証を使用します。 これは、コンテンツにはアプリケーション ユーザーの ID を使用してアクセスし、構成ファイルにはアプリケーション プールの ID を使用してアクセスすることを意味します。"

注意 1:
上で「コンテンツにはアプリケーション ユーザーの ID を使用」とありますが、これは統合 Windows 認証の環境でパススルー認証によってユーザー認証が完了した場合で、匿名認証の場合は IUSR が使用されます。また、「コンテンツ」というのは静的コンテンツのみです。(動的コンテンツについては下の注意 3 参照)

注意 2:
上で言う「構成ファイル」とは web.config のことです。物理パス C:\inetpub\wwwroot には自動的にアプリケーションプール ID が適切なアクセス権を持つように設定されます(正確には、Microsoft TechNet の記事 に書いてあるように、wwwroot フォルダに対して必要なアクセス権を持つ IIS_IUSRS グループが設定されます。そして、実行時にアプリケーションプール ID のアクセストークンに対して IIS_IUSRS メンバーシップが自動的に追加されるので web.config には問題なくアクセスできます)。

ただし、物理パスが C:\inetpub\wwwroot 以外にある場合は要注意です。特に、物理パスが UNC にあって web.config も UNC にある場合がややこしいです(詳しくは Microsoft Support の記事 を見てください)。

注意 3:
.aspx, .ascx などの動的コンテンツへのアクセス、.aspx.cs, .ascx.cs などコードビハインドのコードでのファイルや SQL Server へのアクセスは「アプリケーション プールの ID を使用」します。

この場合、先の記事 で書きましたように Temporary ASP.NET Files フォルダーにコンパイル済みアセンブリが置かれますので、アプリケーション プールの ID はそのフォルダに対しても適切なアクセス権を持つ必要があります。(自動的に設定されているはず)

なお、.aspx ページの中に外部スクリプトファイルや外部 CSS ファイルなど静的ファイルを取り込むための定義(例: <script src="/scripts/jquery.js" ...)がされていて、.aspx ページがブラウザに読み込まれた後、ブラウザがそれらの静的ファイルをサーバーに要求した場合は、上で言う「コンテンツにはアプリケーション ユーザーの ID を使用」が当てはまりますので注意してください。ただし、HTTP ハンドラ経由で静的ファイルを取得する場合(例:HTTP ハンドラでキャッシュコントロール)は話は別で「アプリケーション プールの ID を使用」します。

さらに、「サイトの編集」ダイアログ上の [接続] をクリックすると表示される「接続」ダイアログのヘルプには [アプリケーション ユーザー (パススルー認証)] に対して以下の説明がされています。

"このオプションは、パススルー認証を使用する場合に選択します。 このオプションを選択すると、物理パスへのアクセスに要求元のユーザーの資格情報が使用されます。

匿名要求については、匿名認証用に構成されている ID が物理パスへのアクセスに使用されます。 既定では、この ID は組み込みの IUSR アカウントです。

認証された要求の場合、物理パスへのアクセスに要求元のユーザーの認証済み資格情報のセットが使用されます。 このアプリケーションで使用されるアプリケーション プール ID は物理パスに対して読み取りアクセス権を持ち、認証されたユーザーが、物理パス上のコンテンツにアクセスできるようにします。"

・・・という訳で、「接続」ダイアログのパス資格情報で [アプリケーション ユーザー (パススルー認証)] を選択するということは、

  1. 匿名アクセスの場合は IUSR、(デフォルト。IUSR は変更可能
  2. Winsows 認証の場合はログインしたユーザーの Windows アカウント、
  3. Forms 認証でユーザーがログイン済みの場合の場合アプリケーションプール ID(例:IIS7 では NETWORK SERVICE。ASP.NET の ID オブジェクト 参照)

の資格情報で「サイトの編集」ダイアログで [物理パス(P):] に設定したパスのコンテンツにアクセスすることになり、統合 Windows 認証を利用している場合は上に述べたパススルー認証のメカニズムによってユーザーの資格情報をドメインコントローラーから取得してコンテンツにアクセスすることになるはずです。

従って、通常は IIS のデフォルトの設定どおりパススルー認証としておけば、統合 Windows 認証に限らずほとんどのケースで問題なさそうです。

なお、「サイトの設定」ダイアログで [テスト設定(G):] をクリックすると、「テスト接続」ダイアログに [結果(R):] が 2 つ表示され、前者は OK ながら、後者の方に "パス (C:\xxx\yyy) へのアクセスを検証できません。" と表示されて問題ありそうな感じがしますが、それは気にしなくてよさそうです。

テスト接続

前者は ( ) 内に示す ID が有効かどうか、後者はパススルー認証で物理パスへのアクセス権があるかどうかの結果を表示しているようですが、IIS マネージャーが検証できないだけで、実際にアクセス権がないと言っているわけではなさそうですので。

------------

ところで、一番上の画像の「接続」ダイアログで [特定のユーザー(U):] を指定した場合ですが、それついてはまだよく分かってないです。(汗)

物理パスへのアクセスが指定したユーザーのアクセス権で行われるのは間違いなさそうです。

ただし、特定のユーザーを指定することによって、web.config にそのユーザーの偽装が設定されるとか、匿名アクセスの資格情報が IUSR から変更になるということはなかったです。

また、フォーム認証でのログイン操作の際に "System.Data.SqlClient.SqlException: ユーザーにはこの操作を実行する権限がありません。" というエラーが出てログインできなくなるという問題が出るなど、訳がわからないところもあります(App_Data の ASPNETDB.mdf へのユーザーインスタンスへの接続ですが、資格情報の問題ではなさそう。他の DB のユーザーインスタンスには接続できたので)。

今後の課題ということで、分かったらまたこの記事に追記することにします。

Tags: ,

Authentication

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar