WebSurfer's Home

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

Web API で AuthorizationFilter 実装

by WebSurfer 2016年4月10日 14:39

MSDN Forum の「ASP.net MVC5 WEB APIのキャンセル(特定のデータの返却)する方法はありますか?」という表題のスレッドでの話です。

[クライアント]⇒[閲覧用ページ]⇒[Web API]⇒[DB サーバー]

という構成で、(1) [Web API]が要求を受けたらそこで時間外か否かを判断し、(2) 時間外であれば[Web API]のアクションメソッドは実行せず、(3) [閲覧用ページ]に時間外メッセージと遷移先の URL を応答として返し、(4) [閲覧用ページ]は応答の内容をチェックして時間外であれば遷移先に指定された URL に JavScript で遷移する・・・という方法を考えてみます。

もちろん時間外でなければ、上記で、(2)[Web API]でアクションメソッドを実行し、(3) 通常の応答を[閲覧用ページ]に返し、(4)[閲覧用ページ]は応答を処理して表示するということになります。

それを実現するためには、[Web API]において以下のような AuthorizationFilterAttribute を継承したフィルターを作って、OnAuthorization メソッドを override し、そこでアクションメソッドが実行される前に時間外か否かをチェックし、時間外であればその旨メッセージを返すようにします。(以下のコードは既存のコードを流用したため日付の範囲で制限してます)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Net.Http;
using System.Web.Http.Filters;
using System.Web.Http.Controllers;

namespace WebApi1.Filters
{
  [AttributeUsage(
       AttributeTargets.Method | AttributeTargets.Class,
       Inherited = true, AllowMultiple = false)]
  public class TimeLimitAttribute : AuthorizationFilterAttribute
  {
    public TimeLimitAttribute(string dateBegin, string dateEnd)
    {
      this.Begin = dateBegin;
      this.End = dateEnd;
    }
        
    private DateTime _begin = DateTime.MinValue;
    private DateTime _end = DateTime.MaxValue;

    public string Begin
    {
      set
      {
        DateTime dateSet = DateTime.Parse(value);                
        if (dateSet >= this._end)
        {
          throw new ArgumentException("開始日設定エラー");
        }
        this._begin = dateSet;
      }            
    }

    public string End
    {
      set
      {
        DateTime dateSet = DateTime.Parse(value);
        if (dateSet <= this._begin)
        {
          throw new ArgumentException("終了日設定エラー");
        }
          this._end = dateSet;
      }
    }

    public override void OnAuthorization(
                      HttpActionContext actionContext)
    {
      if (actionContext == null)
      {
        throw new ArgumentNullException("context が null");
      }

      DateTime current = DateTime.Now;
      if (current < this._begin || current > this._end)
      {

        HttpResponseMessage response = 
            new HttpResponseMessage();
        response.Content = 
            new StringContent("時間外,<リダイレクト先 URL>");
        actionContext.Response = response;
      }

      base.OnAuthorization(actionContext);
    }        
  }
}

ASP.NET MVC 用と ASP.NET Web API 用とでは Filter は違うそうなので注意してください。詳しくは MSDN Blog の記事「Web API における操作ごとの制御 (Validation, 認証/権限, Exception 処理 など)」を見てください。

上のフィルター属性を ApiController のクラスに付与することで制限がかけられます。たとえば、以下のようにフィルター属性を付与すれば、2013/12/1 ~ 2013/12/31 以外の日時にアクセスした場合、"時間外,<リダイレクト先 URL>" という文字列が[Web API]から応答として返ってきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using WebApi1.Models;

namespace WebApi1.Controllers
{
    [WebApi1.Filters.TimeLimit("2013/12/1", "2013/12/31")]
    public class HeroesController : ApiController
    {
        private List<Hero> heroes = new List<Hero> {
              new Hero {Id = 1, Name = "スーパーマン"},
              new Hero {Id = 2, Name = "バットマン"},
              new Hero {Id = 3, Name = "ウェブマトリクスマン"},
              new Hero {Id = 4, Name = "チャッカマン"},
              new Hero {Id = 5, Name = "スライムマン"}
          };
                
        public IEnumerable<Hero> Get()
        {
            return heroes;
        }

        public Hero Get(int id)
        {
            return heroes[id - 1];
        }

        //・・・中略・・・
    }
} 

それを[閲覧用ページ]で jQuery.Ajax + JavaScript を使って、例えば上のアクションメソッドの public IEnumerable<Hero> Get() を呼んだ場合は以下のようの処置すれば、時間外であれば[Web API]からの応答に含まれる <リダイレクト先 URL> にリダイレクトされます。

function apiHeroesGet() {
    $.ajax({
        type: "GET",
        url: "api/heroes",
        contentType: "application/json; charset=utf-8",
        success: function (data, textStatus, jqXHR) {
            if (typeof(data) == "string" && 
                data.indexOf("時間外") == 0) {
                // 時間外の場合はリダイレクト
                var results = data.split(",");
                window.location.href = results[1];
            }
            else {
                // 時間内の場合の処置(省略)
            }
        },
        error: function (jqXHR, textStatus, errorThrown) {
            // エラーの場合の処置(省略)
        }
    });
}

Tags: ,

MVC

IP アドレスで SQL Server に接続

by WebSurfer 2016年3月18日 15:42

サーバー名の代わりに IP アドレスを使って SQL Server に接続に行くと、TCP/IP プロトコルを使って接続に行くようです。

SQL Server への接続

自分の開発マシンには SQL Server 2005 Developer Edition と SQL Server 2008 Express Edition がインストールしてあって、プロトコルは前者が共有メモリのみ有効、後者が共有メモリ、名前つきパイプ、TCP/IP(ポートは 1433)が有効になっています。

そこに、コマンドラインから sqlcmd を使って接続に行くと、(local) の場合は SQL Server 2005 に、IP アドレスの場合は SQL Server 2008 に接続されます。(SELECT @@version; クエリで確認しました)

そして、上の画像で示したとおり、(local) の場合はプロトコルは共有メモリで、IP アドレス(192.168.1.4)の場合は TCP/IP で接続に行きます。

TCP/IP プロトコルを有効にしてあるのは SQL Server 2008 Express のみなので、TCP/IP プロトコルで接続に行く場合、自動的に SQL Server 2008 Express に接続されるということのようです。

知ってました? 実は自分は知らなくて、(local) とかサーバー名を使った場合と同様に SQL Server 2005 Developer Edition に接続に行くと思っていて、半日ぐらいハマってしまいました。(汗)

その他いろいろ新発見があったので、覚えておいたほうがよさそうなことを備忘録として以下に書いておきます。

サーバー名でなく IP アドレスを使った場合、プロトコルを強制的に指定しようとしても、指定できるプロトコルは tcp のみで、np, lpc ではエラーとなって接続できません。実際に試してみたところ、以下の結果となりました。

  • sqlcmd -S tcp:192.168.1.4 -E ⇒ OK
  • sqlcmd -S np:192.168.1.4 -E ⇒ NG
    エラーメッセージ:Named Pipes Provider: Could not open a connection to SQL Server [64].
  • sqlcmd -S lpc:192.168.1.4 -E ⇒ NG
    エラーメッセージ:SQL Server Network Interfaces: Cannot open a Shared Memory connection to a remote SQL Server instance [87].

sqlcmd -S (local) -E または sqlcmd -S <サーバー名> -E は SQL Server 2005 に共有メモリを使って接続されます。

sqlcmd -S (local)\sqlexpress -E または sqlcmd -S <サーバー名>\sqlexpress -E は SQL Server 2008 Express に共有メモリを使って接続されます。

インスタンス名を追加して sqlcmd -S 192.168.1.4\SQLEXPERESS -E とすると "SQL Server Network Interfaces: Error Locating Server/Instance Specified [xFFFFFFFF]." というエラーとなって接続に失敗します。sqlcmd -S 192.168.1.4\SQLEXPERESS,1433 -E というようにポートを指定すれば接続できます。

その理由は、先の記事 SQLEXPRESS は「名前つきインスタンス」名で書きましたように、インスタンス名を指定した場合、UDP 1434 (SQL Browser サービス) に接続して、指定したインスタンス名のポート番号を取得し、その後対象ポートに接続をするという動作になるからのようです。

MSDN Blog の記事 Troubleshooting Connectivity #1 – SQL Server への接続によると、SQL Server に接続する場合、以下の 3 つのステップにで処理が行われるそうです。この記事の話は「OS レベルのセッション確立」のステップに該当します。

  1. OS レベルのセッション確立
  2. ログイン認証
  3. データベースアクセス

上に紹介した MSDN Blog の記事にはいろいろ参考になることが書いてありますので、一読されることをお勧めします。あと、Troubleshooting Connectivity #5 – セッション確立までの動作という記事も参考になると思います。

Tags: ,

SQL Server

DataType 属性による検証

by WebSurfer 2016年3月8日 17:21

データアノテーション検証でのデフォルトのエラーメッセージと DataType 属性による検証についておぼえておいた方がよさそうなことを備忘録として書きます。

エラーメッセージ

上の画像は、下のサンプルコードの Model のパブリックプロパティに付与したデータアノテーション属性によるエラーメッセージです。

(注1:上に表示されているエラーメッセージは全てクライアント側での検証結果によるものです。)

(注2:BrithDate については 0001 で何故かクライアント側での検証はパスします。ただし、サーバー側で検証がかかって、コントローラーで ModelState.IsValid は false になります。エラーメッセージは上記と違って「値 '0001' は BirthDate に対して無効です」となります)

なお、自分が検証に使った環境は Vista SP2 32-bit, .NET 4, Visual Studio 2010 プロフェッショナル, MVC4 のインターネットアプリケーションのテンプレート使用、ASP.NET 開発サーバー利用、IE9 です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;

namespace Mvc4App2.Models
{
  public class DefaultErrorMessage
  {
    [Required]
    public string Name { get; set; }

    [Required]
    [DataType(DataType.Password)]
    [RegularExpression(@"^(?=.*\d)(?=.*[A-Z])[A-Z0-9]{4,8}$")]
    public string PassWord { get; set; }

    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    [DataType(DataType.PhoneNumber)]
    public string Phone { get; set; }

    [DataType(DataType.Date)]
    public DateTime BirthDate { get; set; }

    [DataType(DataType.Url)]
    public string Url { get; set; }

    [StringLength(10)]
    [DataType(DataType.MultilineText)]
    public string Message { get; set; }
  }
}

各属性の ErrorMessage プロパティに何も設定しなくても上の画像のようにデフォルトでエラーメッセージが出ます。

しかし、正規表現がそのまま出たり(「フィールド PassWord は正規表現 '^(?=.*\d)(?=.*[A-Z])[A-Z0-9]{4,8}$' と一致する必要があります。」とか言われても一般ユーザーには訳がわからない)、一部はローカル化されておらず英文のままだったりとユーザーフレンドリーとは言えないようです。

さらに、MSDN フォーラムのスレッドASP.NET MVC5 でVaridateメッセージが英語で初めて知りましたが、自動的にはローカル化されず全部英語になってしまうこともあるようです。

また、DataType 属性については、付与しても検証は行われずエラーメッセージは出ないものがありますので注意が必要です。上の例では Password, PhoneNumber, MultilineText では検証が行われていません。

Password と MultilineText にはエラーメッセージが出ていますが、それは DataType 属性によるものではなく、追加で付与した RegularExpression 属性と StringLength 属性によるものです。

一方、EmailAddress, Date, Url は検証がかかります。クライアント側での検証はブラウザ依存になると思いますので自分が試した IE9 以外で同じ結果になるかは分かりませんが(未検証・未確認です)。なので、DataType 属性による検証に頼らず、自分で StringLength 属性や RegularExpression 属性等を使ってきちんと検証したほうがよさそうです。

ただし、DataType 属性を付与することによるメリットは検証以外にもあります。それは、View のコードに全て Html.EditorFor を利用しても、DataType 属性によって生成される html ソースの input 要素の type 属性が変わってくる(MultilineText の場合は input 要素でなく、textarea 要素になる)ことです。ちなみに、上の Model の DataType 属性の設定では以下のようになります。

<input ... name="Name" type="text" value="" />
            
<input ... name="PassWord" type="password" value="" />
            
<input ... name="Email" type="email" value="" />
            
<input ... name="Phone" type="tel" value="" />
            
<input ... name="BirthDate" type="date" value="0001/01/01" />
            
<input ... name="Url" type="url" value="" />

<textarea ... id="Message" name="Message"></textarea>

それゆえ、Password と MultilineText にいては DataType 属性を付与するのがよさそうです。他のデータアノテーション属性による検証とバッティングすることもなさそうですし。

以上のような訳で、DataType 属性による検証やデフォルトのエラーメッセージでは期待した結果を得るのは難しいので、

  1. DataType 属性による検証は利用せず Range, RegularExpression, Required, StringLength 属性などを組み合わせて使う。
  2. DataType 属性を使用するのは、Password や MultilineText のように、レンダリングされる html ソースの input 要素の type 属性を "password" にしたいとか、input 要素でなく textarea 要素にしたい場合に限る。
  3. エラーメッセージは自分で各データアノテーション属性の ErrorMessage プロパティに設定する。(先の記事「コレクションのデータアノテーション検証」参照)
  4. Model のコード内にエラーメッセージをハードコーディングしたくない場合は自分でリソースファイル (.resx) を作って使う。(先の記事「データアノテーション検証の多言語対応」参照)
  5. エラーメッセージを多言語化する場合は自分でリソースファイルを追加してサテライトアセンブリを作って使う。(同じく、先の記事「データアノテーション検証の多言語対応」参照)

というようにするのが正解だと思いました。

-------- 2016/6/17 追記 --------

ErrorMessage プロパティの設定は行わず、デフォルトの日本語のエラーメッセージを利用したいのであれば、Azure など特にサーバーが外国にある場合は Culture, UICulture を "auto" に設定するのを忘れないようにしてください。

Culture, UICulture を "auto" に設定すると、ASP.NET は、ブラウザから送信されてくる要求ヘッダに含まれる Accept-Language の設定を調べて、その要求を処理するスレッドのカルチャを Accept-Language に設定されているカルチャに書き換えます。

そして、リソースマネージャが実行時に、Thread.CurrentUICulture などで得られる CultureInfo(現在の要求を処理しているスレッドのカルチャ情報)を参照してローカライズされたリソースを検索し、UI に表示されるテキストを取得するという仕組みになっています。

Culture, UICulture を "auto" に設定するのを忘れるとブラウザの言語設定は無視されます。デフォルトではシステムのロケールに該当するカルチャがスレッドに設定されますので、例えば英語 OS では英語のリソースから UI に表示されるテキストを取得します。(日本語のリソースがあっても使われません)

Tags: ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2017年1月  >>
25262728293031
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar