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

Windows Forms アプリで Membership を利用

by WebSurfer 2014年2月11日 17:47

ASP.NET の Membership クラスRoles クラス を Windows Forms アプリケーションでも利用できるという話です。

Windows Forms アプリで Membership と Roles を利用

Membership オブジェクトおよび Roles オブジェクトは、ASP.NET が web.config の情報を基に Membership プロバイダおよび Role プロバイダ(ユーザー情報のストアが SQL Server の場合は SqlMembershipProvider クラス および SqlRoleProvider クラス)を利用して自動的に生成します。

プログラマがコードを書いて Membership オブジェクト、Roles オブジェクトを作る手段は提供されていません。なので、ASP.NET Web アプリケーションでしか使用できないと思っていましたが、実はそうではなかったです。(汗)

そのことは、MSDN フォーラムで Using the ASP.Net membership provider in a Windows forms application part 1. という記事を紹介されて、初めて知りました。

その記事によると、Windows コンソールアプリでも(ASP.NET でなくても)、app.config から(web.config からでなくても)設定値を取得して、Membership オブジェクトや Roles オブジェクトを自動的に生成してくれるということでした。

自分でも、以下のコードの Windows Forms アプリを作って試してみましたが、確かにその通りであることが確認できました。上の画像が以下のコードを実行したときのものです。

Form1.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Web.Security;

namespace ASPNETMembershipWindowsForms
{
  public partial class Form1 : Form
  {
    public Form1()
    {
      InitializeComponent();
            
      this.listBox2.DataSource = Roles.GetAllRoles();

      List<string> userNames = new List<string>();
      foreach (MembershipUser user in Membership.GetAllUsers())
      {
        userNames.Add(user.UserName);
      }
      this.listBox1.DataSource = userNames;
    }

    private void button1_Click(object sender, EventArgs e)
    {
      if (Membership.ValidateUser(this.textBox1.Text, 
            this.textBox2.Text))
      {
        this.label3.Text = "Result: User validated.";
      }
      else
      {
        this.label3.Text = "Result: User invalid!";
      }            
    }

    private void button2_Click(object sender, EventArgs e)
    {
      if (this.listBox1.SelectedItem == null)
      {
        this.label6.Text = "Please select a user.";
        return;
      }

      if (this.listBox2.SelectedItem == null)
      {
        this.label6.Text = "Please select a role.";
        return;
      }

      string user = this.listBox1.SelectedItem.ToString();
      string role = this.listBox2.SelectedItem.ToString();

      try
      {
        if (Roles.IsUserInRole(user, role))
        {
          this.label6.Text = "Yes";
        }
        else
        {
          this.label6.Text = "No";
        }
      }
      catch (Exception ex)
      {
        this.label6.Text = ex.Message;
      }
    }
  }
}

app.config

<?xml version="1.0"?>
<configuration>
  <configSections>
  </configSections>
  <connectionStrings>
    <add name="MemberShip" 
      connectionString="..." 
      providerName="System.Data.SqlClient"/>
  </connectionStrings>

  <system.web>
    <machineKey validationKey="..." 
      decryptionKey="..." 
      validation="SHA1" 
      decryption="AES"/>

    <membership defaultProvider="SqlProvider">
      <providers>
        <remove name="SqlProvider"/>
        <add name="SqlProvider" 
          type="System.Web.Security.SqlMembershipProvider" 
          connectionStringName="MemberShip" 
          applicationName="MyHomePage" 
          enablePasswordRetrieval="false" 
          enablePasswordReset="true" 
          requiresQuestionAndAnswer="true" 
          requiresUniqueEmail="false" 
          passwordFormat="Hashed" 
          maxInvalidPasswordAttempts="5" 
          minRequiredPasswordLength="7" 
          minRequiredNonalphanumericCharacters="0" 
          passwordAttemptWindow="10"/>
      </providers>
    </membership>
        
    <roleManager enabled="true" defaultProvider="RoleProvider">
      <providers>
        <clear/>
        <add name="RoleProvider" 
          type="System.Web.Security.SqlRoleProvider" 
          connectionStringName="MemberShip" 
          applicationName="MyHomePage"/>
      </providers>
    </roleManager>
  </system.web>
  <startup>
    <supportedRuntime version="v2.0.50727"/>
  </startup>
</configuration>

今回の検証では、既存の ASP.NET Web アプリケーションの Forms 認証用の ASPNETDB.mdf を利用ました。

なので、上記 app.config で既存の ASPNETDB.mdf に接続できるように接続文字列を設定し、machineKey 要素、membership 要素、roleManager 要素の設定は既存の Web アプリの web.config と同じにしています。

machineKey 要素の設定まで同じにしたのは、MSDN ライブラリの How To: ASP.NET 2.0 で MachineKey を構成する方法 の「メンバシップ」のセクションに "暗号化パスワード フォーマットを選択すると、データの暗号化と暗号化解除のときに <machineKey> 設定が使用されます" という記述があったからです。

ただし、これは passwordFormat が "Encrypted" に設定されている場合のようで、デフォルトの "Hashed" の場合は関係なさそうです。

それから、ランタイムバージョンが .NET 2.0 になっているのは、.NET 4 では何故か MembershipUser が使えないからです。

オブジェクトブラウザを見ても MembershipUser 以外にも .NET 4 に含まれないクラスが多々あります。理由不明ですが、今日はもうこれ以上追求する気力がないです。(笑)

----- 2014/2/13 追記 -----

.NET 3.5 以前では System.Web.dll のみを参照設定に追加すれば使えた MembershipUser その他のクラスが .NET 4 では使えない理由が分かりました。

Microsoft の ASP.NET のサイトの ASP.NET 4 Breaking Changes という記事の MembershipUser and Other Types in the System.Web.Security Namespace Have Been Moved という記事に以下のように書いてありました。

"Some types that are used in ASP.NET membership have been moved from System.Web.dll to the new System.Web.ApplicationServices.dll assembly. The types were moved in order to resolve architectural layering dependencies between types in the client and in extended .NET Framework SKUs."

従って、.NET 4 以降では System.Web.ApplicationServices.dll も参照設定に追加しないと MembershipUser クラスは使えません。

ただし、"System.Web.ApplicationServices.dll was added to the list of referenced assemblies that is used by default by the ASP.NET compilation system." とのことなので、ASP.NET Web アプリケーションではプログラマが参照設定を追加する必要はないそうです。

詳しくは上に紹介した Microsoft のサイトの記事を見てください。

Tags: ,

Authentication

セッションタイムアウトとログアウト

by WebSurfer 2013年6月29日 20:13

クラッシック ASP の場合は分かりませんが、ASP.NET の場合は、セッションの「タイムアウト」状態とフォーム認証の「ログアウト」状態は違います。

ASP.NET の場合「セッションタイムアウト」というと、MSDN ライブラリの セッション タイムアウトを構成する (IIS 7) によれば、sessionState 構成要素の timeout 属性に設定した時間が過ぎたことを言います。

セッションタイムアウトすると、サーバーに保存されていた SessionState 情報が破棄されます。しかし、それ以外の影響はありません。少なくともフォーム認証には無関係です。

また、SessionState 情報、SessionID、セッション Cookie のいずれからもフォーム認証のチケットに関する情報は取得できません。

なお、SessionState 情報を格納しない場合(Session["Data"] = xxxx; というようなコードが存在しない場合)、サーバーはセッション Cookie を発行しません。(2014/7/26 追記:Global.asax に Session_Start ハンドラを追加すると話が違ってきます。詳しくは、Session_Start ハンドラの影響 を参照ください)

SessionState 情報が格納される場合に限り、SeesionID を含むセッション Cookie が発行されます。なお、セッションタイムアウトしても SessionID は書き換えられず、同じ ID が使用され続けます。即ち、Session["Data"] に保存したデータは破棄されても、ブラウザは要求のたび同じセッション Cookie を送り続けます。

一方、フォーム認証の「ログアウト」状態とは認証チケットを持っていない、もしくは認証チケットは持っていても期限切れという状態です。セッションがタイムアウトしていても認証チケットは有効と言うことは当然あります。その逆もあります。

従って、フォーム認証チケットの有無もしくは期限切れと、セッションタイムアウトとは関係なく、セッションの「タイムアウト」状態 = フォーム認証の「ログアウト」状態にはならないです。

なお、Windows 認証、統合 Windows 認証は、フォーム認証とは全くメカニズムが異なっていて、セッションもクッキーも関係ないです。

Tags:

Authentication

About this blog

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

Calendar

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

View posts in large calendar