WebSurfer's Home

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

loginUrl に絶対 URL を指定した際の問題

by WebSurfer 2014年6月18日 17:56

Fiddler2 で見たログインプロセス

web.config 内の forms 要素の loginUrl 属性に絶対 URL を指定した場合、ログイン後のリダイレクトがうまく行かないという問題と、その対応方法の紹介です。

左の画像はログインに成功して HTTP 302 応答が返ってきたところを Fiddler2 でキャプチャして見たものです。

クリックすると拡大画像が表示されますので、応答ヘッダ(右下のウィンドウ内)の Location に注目してください。本来 /Default.aspx であるべきところが /Account/Login.aspx になっています。

具体的にどういう問題かを以下に説明します。

ASP.NET Web Forms アプリケーションのフォーム認証で、Login コントロール を使った Login.aspx ページでログイン操作を行っているとします。

ログインに成功すると、Login.aspx の ReturnUrl クエリ文字列に指定されたページ(上の画像では /Default.aspx)か、クエリ文字列がない場合は forms 要素の defaultUrl 属性に指定されたページにリダイレクトされるのが普通の動作です。

ところが、以下のように loginUrl 属性に絶対 URL を指定すると、ログイン後に loginUrl 属性に指定した URL(即ち、/Account/Login.aspx ページ)にリダイレクトされてしまいます。← これが今回紹介する問題です。

<authentication mode="Forms">
  <forms loginUrl="http://<hostname>/Account/Login.aspx" />
</authentication>

何故そうなるかと言うと、Login.aspx でログイン成功後、認証クッキーを設定する HTTP 302 応答が帰ってきますが、応答ヘッダの Location が何故か ReturnUrl クエリ文字列(それがない場合は defaultUrl 属性の指定)を無視して /Account/Login.aspx となるからです。

上の画像で、左側のウィンドウ内で上から 4 番目の行がそのときの応答です。右下のウィンドウ内が応答ヘッダの内容で、その一番下の行 Location が /Account/Login.aspx となっています。ブラウザはこれを見て /Account/Login.aspx を GET 要求するという訳です。

なお、loginUrl 属性に相対 URL やルート演算子 (~) を使用した場合(例: loginUrl="~/Account/Login.aspx")は問題なく、期待通り ReturnUrl クエリ文字列に指定されたページにリダイレクトされます。

何故、絶対 URL を指定したいかと言えば forms 要素の requireSSL 属性 を True に設定して、認証 Cookie を送信するために SSL 接続を必須としたいからです。

その場合、loginUrl="https://<hostname>/Account/Login.aspx のように https で始まる絶対 URL を指定しないとサーバーエラーとなってログインできません。

ちなみに、loginUrl="~/Account/Login.aspx" requireSSL="true" とすると、Login.aspx ページは SSL 経由せずに表示されますが、ID とパスワードを入力して[ログイン]ボタンをクリックするとサーバーエラーとなり、以下のエラーメッセージが返ってきます。

"System.Web.HttpException: アプリケーションは、セキュリティで保護されたクッキーを発行するように構成されています。これらのクッキーでは、サーバーが SSL (https プロトコル) 経由で要求を発行することが必要ですが、現在の要求は SSL 経由ではありません。"

defaultUrl 属性の指定が無視されるという事例は聞いたことがありませんし、ググって調べても同様な事例は見つかりませんでした。

MSDN フォーラムASP.NET Forum で聞いてみましたが回答は得られませんでした。

というわけで、何故 loginUrl に絶対 URL を指定するとログイン後のリダイレクト先がその URL になってしまうかの原因は、現在のところ分かっていません。(汗)

この問題の解決方法ですが、原因が分かってないので対症療法的ではありますが、一応、以下の手段で回避できることは分かりました。

  1. Login コントロールを使わない。
  2. Login コントロールを使うなら、
    (1) Login.DestinationPageUrl プロパティでリダイレクト先を指定、または
    (2) Login.LoggedIn イベントで強制的にリダイレクトする。

上記 1 の具体的な例は FormsAuthentication.GetRedirectUrl メソッド のサンプルコードを見てください。

上記 2 の (2) の具体例は以下のコードを参考にしてください。

// LoggedIn イベントは、認証プロバイダによってユーザーの資格
// 情報がチェックされ、次の応答でブラウザに送信するために認証 
// Cookie がキューに置かれた後に発生。
protected void LoginUser_LoggedIn(object sender, EventArgs e)
{        
  string userName = LoginUser.UserName;
  string redirectUrl = 
      FormsAuthentication.GetRedirectUrl(userName, false);
  Response.Redirect(redirectUrl);

  // ちなみに以下ではいずれも問題は回避できないので注意。
  // Response.Redirect(redirectUrl, false);
  // FormsAuthentication.RedirectFromLoginPage(userName, false);
}

自分が検証に使った環境は以下の通りです。ASP.NET 4.5 で解決されているかどうかは分かりません。

  • ASP.NET 3.5 および 4
  • Vista 32-bit SP2 の IIS7
  • Visual Studio 2010 Professional
  • SQL Server 2008 Express で aspnetdb.mdf 使用
  • Login コントロールを使用
  • Web Forms アプリ(MVC は問題なし。たぶん Login を使わないため)

Tags:

Authentication

ClientTarget の利用

by WebSurfer 2014年6月14日 16:20

ClientTarget を利用すると ASP.NET によるブラウザの判定を自由に設定できます。

下の画像は、IE9 でアクセスして(一番上の行の HttpRequest.UserAgent プロパティで取得した User Agent 参照)、HttpBrowserCapabilities が Firefox30 と判定されている例です。

ClientTarget の利用

例えば、開発環境で IE11 を持ってなくても、IE11 でアクセスした場合に ASP.NET がどのような html や JavaScript をレンダリングするかを調べるなどに役立つと思います。

ただ、User Agent を偽装するなら Fiddler2 を使えばもっと簡単にできます。なので、あまり使い道はないかもしれませんが、せっかく調べたので書いておきます。

web.config の clientTarget 要素の add 子要素 に任意のブラウザのエイリアスと User Agent を設定します。

ASP.NET 4 の場合、ルート(C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config)の web.config を調べてみると、uplevel と downlevel が定義済みでした。

しかし、appcmd コマンドで調べてみると、以下の画像の通り、uplevel と downlevel に加えて、何故か ie4 と ie5 も定義されています。自分で ie4 と ie5 の定義を追加した覚えはないのですが・・・

デフォルトの clientTarget 要素

ところが、@ Page ディレクティブの ClientTarget 属性で ie4 を指定すると "ClientTarget が無効なエイリアス 'ie4' に設定されています。<clientTarget> 構成セクションは ClientTarget エイリアスを定義するために使用されます。" というエラーになります。(ie5 も同様にエラー。uplevel と downlevel は問題なし) 理由はどうしても分かりません。(汗)

やむをえず、以下のように一旦全て clear して、ie4, ie5, uplevel, downlevel を定義しなおし、IE11 と Firefox30 の定義を追加して検証しました。

<clientTarget>
  <clear/>
  <add alias="ie4" 
    userAgent="Mozilla/4.0 (compatible; MSIE 4.0; Windows NT 4.0)"/>
  <add alias="ie5" 
    userAgent="Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 4.0)"/>
  <add alias="uplevel" 
    userAgent="Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.1)" />
  <add alias="downlevel" 
    userAgent="Generic Downlevel" />
  <add alias="ie11" 
    userAgent="Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"/>
  <add alias="ff30" 
    userAgent="Mozilla/5.0 (Windows NT 6.0; rv:30.0) Gecko/20100101 Firefox/30.0"/>
</clientTarget>

Page.ClientTarget プロパティ のページに書いてありますように、@ Page ディレクティブの ClientTarget 属性に、上の alias 属性に指定された文字列を設定すると、ASP.NET によるブラウザ検出機能が無効になり、userAgent 属性に指定された User Agent 文字列を使ってブラウザが判定されます。

一番上の画像のページの例では @ Page ディレクティブ で ClientTarget="ff30" としているので、実際にアクセスしているのは IE9 ですが、Firefox30 の User Agent でブラウザ判定が行われ、その判定結果が HttpBrowserCapabilities オブジェクトに反映されています。

一番上の画像を出力したコードを参考にアップしておきます。

protected string GetBrowswerCapabilities()
{
    HttpBrowserCapabilities browser = Request.Browser;
    StringBuilder sb = new StringBuilder();
    sb.Append("User Agent: " + Request.UserAgent + 
        "<br /><br />");
    sb.Append("HttpBrowserCapabilities are:<br />");
    sb.AppendFormat("Type = {0}<br />", 
        browser.Type);
    sb.AppendFormat("Name = {0}<br />", 
        browser.Browser);
    sb.AppendFormat("Version = {0}<br />", 
        browser.Version);
    sb.AppendFormat("Major Version = {0}<br />", 
        browser.MajorVersion);
    sb.AppendFormat("Minor Version = {0}<br />", 
        browser.MinorVersion);
    sb.AppendFormat("Platform = {0}<br />", 
        browser.Platform);
    sb.AppendFormat("Is Beta = {0}<br />", 
        browser.Beta);
    sb.AppendFormat("Is Crawler= {0}<br />", 
        browser.Crawler);
    sb.AppendFormat("Is AOL = {0}<br />", 
        browser.AOL);
    sb.AppendFormat("Is Win16 = {0}<br />", 
        browser.Win16);
    sb.AppendFormat("Is Win32 = {0}<br />", 
        browser.Win32);
    sb.AppendFormat("Supports Frames = {0}<br />", 
        browser.Frames);
    sb.AppendFormat("Supports Tables = {0}<br />", 
        browser.Tables);
    sb.AppendFormat("Supports Cookies = {0}<br />", 
        browser.Cookies);
    sb.AppendFormat("Supports VBScript = {0}<br />", 
        browser.VBScript);
    sb.AppendFormat("Supports JavaScript = {0}<br />", 
        browser.EcmaScriptVersion.ToString());
    sb.AppendFormat("Supports Java Applets = {0}<br />", 
        browser.JavaApplets);
    sb.AppendFormat("Supports ActiveX = {0}<br />", 
        browser.ActiveXControls);
    return sb.ToString();
}

Tags:

ASP.NET

接続文字列でのデータベース名の指定

by WebSurfer 2014年5月30日 16:37

SQL Server の接続文字列で Initial Catalog(データベース名)の指定は必要かという話を書きます。

MSDN ライブラリなどでは、SQL Server の既定のインスタンスまたは名前つきインスタンスに接続する場合の接続文字列(正確には SqlClient 接続文字列)の例として、Initail Catalog(もしくは Database)キーワードでデータベース名を指定するように書かれています。

具体例は下記のページを見てください。

Initial Catalog(もしくは Database)のない接続文字列の例は、自分が知る限りですが、見たことがないです。

なので、接続文字列にデータベース名の指定は必須かと思い込んでいましたが、ある機会に試してみたところ、そんなことはなかったです。

以下の条件が満足されれば、任意のデータベースからデータを取得できます。

  1. SQL Server の既定のインスタンスまたは名前つきインスタンスが対象。(ユーザーインスタンスとか LacalDB など、.mdf ファイルを動的にアタッチするようなケースはダメです)
  2. 同一インスタンス内にある、静的にアタッチ済みのデータベースを対象とする。
  3. 接続文字列で指定したログイン権限で SQL Server による認証が通ってログインできること。
  4. 接続文字列にデータベース名の指定がない場合、もしくは指定されていてもそれとは異なるデータベースを対象とする場合は、データベース名を特定できるクエリを記述する。例えば、 [データベース名].[スキーマ名].[テーブル名] というように。
  5. ログインしたユーザーが、目的のデータベースに対して必要な権限を持っている(すなわち、SQL Server によるユーザー承認が通る)。具体的には、先の記事 ユーザー権限の設定 に書いたような設定がしてあることが条件です。

ADO.NET のコードを使って説明した方が分かりやすいと思いますので、以下にサンプルをアップしておきます。説明はコメントに書きましたので見てください。

using System;
using System.Data.SqlClient;

class Program
{
  static void Main(string[] args)
  {
    // Initial Catalog=PUBS でも NORTHWIND から取得できる。
    // Initial Catalog の指定なしでも OK。下はその例。
    // ただしクエリに DB 名の指定が必要。下の例を参照。
    // Integrated Security=True まで省くとログインできない。
    string connString1 =
      "Data Source=tcp:papiko-pc;Integrated Security=True";
    string query1 =
      "SELECT CategoryID, CategoryName " + 
          "FROM [NORTHWIND].[dbo].[Categories];" +
      "SELECT EmployeeID, LastName " + 
          "FROM [NORTHWIND].[dbo].[Employees]";
    RetrieveMultipleResults(new SqlConnection(connString1), query1);

    // Initial Catalog を指定すればクエリで DB 名の指定は不要(下
    // の例では NORTHWIND に含まれる Categories テーブル)。
    // ただし、titleauthor テーブルは PUBS に含まれるものなので、
    // 下の例のようにクエリに DB 名の指定が必要。
    string connString2 =
      "Data Source=tcp:papiko-pc;Initial Catalog=NORTHWIND;" + 
      "Integrated Security=True";
    string query2 =
      "SELECT CategoryID, CategoryName FROM Categories;" +
      "SELECT royaltyper, au_id FROM [PUBS].[dbo].[titleauthor]";
    RetrieveMultipleResults(new SqlConnection(connString2), query2);
  }

  // MSDN ライブラリ「DataReader によるデータの取得」
  // http://msdn.microsoft.com/ja-jp/library/haa3afyz.aspx
  // NextResult による複数の結果セットの取得
  static void RetrieveMultipleResults(
    SqlConnection connection, string query)
  {
    using (connection)
    {
      SqlCommand command = new SqlCommand(query, connection);
      connection.Open();

      SqlDataReader reader = command.ExecuteReader();

      while (reader.HasRows)
      {
        Console.WriteLine("\t{0}\t{1}", reader.GetName(0),
            reader.GetName(1));

        while (reader.Read())
        {
          Console.WriteLine("\t{0}\t{1}", 
              reader.GetInt32(0), reader.GetString(1));
        }
        // バッチ Transact-SQL ステートメントの結果を読み込む
        // ときに、データリーダーを次の結果に進めます。
        reader.NextResult();
      }
    }
  }
}

よく分からないのは、上記のようなことができるのに、何のためにデータベース名を接続文字列に指定するかということです。

クエリを書くときにデータベース名を省略できるぐらいのメリットしか思い浮かびません。他に、コードファーストを利用する際 Initial Catalog に指定された名前でデータベースを作ってくれるということもあるそうですが。でも、そんな理由ではなさそうです。

接続プールが複数作られることになるのも無駄なような気がします。(Initial Catalog が違えばもちろん、キーワードを異なる順序で指定しただけでも接続プールは異なるとのこと。詳しくは MSDN ライブラリ SQL Server の接続プール (ADO.NET) を参照)

上のサンプルの結果からわかるように、接続プールは接続文字列で指定されるインスタンスに接続するためにあるだけで、インスタンス中にあるどのデータベースに接続するかは関知しないです。

それなのに、わざわざ接続文字列にデータベースを指定して、複数の接続プールを作る理由が分かりません。

そもそも、同じ接続プールで複数の異なるデータベースに接続するなどということはやってはいけないことで、好ましからざる副作用があるのでしょうか?

Tags: ,

ADO.NET

About this blog

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

Calendar

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

View posts in large calendar