Asp.Net フォーム認証

フォーム認証
http://www.atmarkit.co.jp/fdotnet/aspnet/aspnet19/aspnet19_01.html

フォーム認証を使う場合に選択することが二つある。

1・ID、パスワードをどこに保存するか?
2・どこまでweb.configの設定を使うか?

1・ID、パスワードをどこに保存するか?
ID、パスワードをweb.configに保存するか、XMLなりRDBなり他の場所に保存するかの選択になる。
web.configのauthentication/forms/credentialsにID、パスワードを保存すればFormsAuthentication.Authenticateを使ってID、パスワードの検証を行ってくれる。
が、web.configにID、パスワードを書きたい人はほとんどいないんじゃないかと思うし、web.configのauthentication/forms/credentialsにはロールの設定まではかけない。
まぁ大半はFormsAuthentication.Authenticateはあきらめて認証は自前でするんだと思う。

2・チケットやcookieにどこまでweb.configの設定を使うか?
FormsAuthentication.RedirectFromLoginPageを使えばweb.configの設定を使って認証coookieを発行してくれるが、汎用性にはかける。
UserDataに任意の値を渡したい、発行する認証チケットやcookieの有効期限の設定を自身のロジックで設定したい、などのニーズがあるなら、認証チケットの発行とcookieへの細んは自分で行う必要がある。

以下はサンプル

「web.configにID、パスワードを保存+RedirectFromLoginPage使用」
web.configにユーザ情報まで書いてしまう方法。
ほとんどAsp.Netが面倒を見てくれるけど、さすがに相当小規模なサイトでないとこの方法は使えないと思う。

サンプルを作ってたとき、認証cookie発行前なのに、ログインページへリダイレクトされずに悩んだ。
結局、authorization/allow/@usersを?にしていたせいで非認証ユーザのアクセスが許可されてただけでした。orz

web.config
ユーザIDやパスワードのアカウント情報もここに書いてしまう。
この辺が実際には使わない理由。configSourceでここだけ外に出す手はあるかもしれないけどそれでも・・・。
configuration/system.web
にこんな記述をする。

        <authentication mode="Forms">
            <forms name="auth" loginUrl="login.aspx" protection="All" path="/" timeout="30">
                <credentials passwordFormat="Clear">
                    <user name="aaa" password="aaa"/>
                    <user name="bbb" password="bbb"/>
                </credentials>
            </forms>
        </authentication>

authentication/@modeは文字通り認証のモードをForm認証にしている。
authentication/formsはForm認証の設定。
authentication/forms/@name:認証cookieの名前

authentication/forms/@loginurl:認証cookieが見つからない場合に、リダイレクトするurlを指定。
認証せずにコンテンツを要求すると、Asp.Netがログインページに飛ばしてくれる。便利やな。

authentication/forms/@protection:cookie暗号化の種類。allにしとけ。
authentication/forms/@path:cookieのパス
authentication/forms/@timeout:認証cookieのタイムアウト。単位は分。
ここでいうタイムアウトは認証cookieのタイムアウトなので、Asp.Netのセッションオブジェクトのタイムアウトとは別物みたいなので注意。

authentication/forms/credentials以下は認証に使うアカウントとパスワード。
authentication/forms/credentials/@passwordFormatを変えればハッシュ値をパスワードにできたりもする。一応。

その他はこちら。
http://msdn2.microsoft.com/ja-jp/library/1d3t3c61(VS.80).aspx

login.aspx

<%@ Page Language="C#" EnableViewStateMac="true" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login"  %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>ログイン</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:TextBox ID="txtId" runat="server"></asp:TextBox>
        <br>
        <asp:TextBox ID="txtPas" runat="server"></asp:TextBox>
        <br>
        <asp:Label ID="lblPersist" runat="server" Text="次回から自動ログインする"></asp:Label><asp:CheckBox ID="cbPersist" runat="server" />
        <br />
        <br />
        <asp:Button ID="btnLogin" runat="server" Text="Login" 
            onclick="btnLogin_Click" />  
        <br />
        <asp:Label ID="lblMsg" runat="server" Text=""></asp:Label>
    </div>
    </form>
</body>
</html>

login.aspx.cs

using System;
using System.Web.Security;

public partial class Login : System.Web.UI.Page
{
    protected void btnLogin_Click(object sender, EventArgs e)
    {
        if (!FormsAuthentication.Authenticate(this.txtId.Text, this.txtPas.Text))
        {
            this.txtPas.Text = string.Empty;
            this.lblMsg.Text = "ログイン失敗";
            return;
        }

        FormsAuthentication.RedirectFromLoginPage(this.txtId.Text, this.cbPersist.Checked);
    }
}

aspxはシンプルなログイン画面。
認証のロジックはbtnLogin_Clickなんだけど、とても簡単。
System.Web.Security.FormsAuthentication#Authenticateメソッドがweb.configのauthentication/forms/credentialsの定義をみてユーザ名、パスワードの検証をやってくれる。
System.Web.Security.FormsAuthentication#RedirectFromLoginPageは認証cookie発行し、ログイン画面に遷移する前のページにリダイレクトしてくれる。

Default.aspx

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" Inherits="_Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>認証テスト</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:Button ID="btnLogout" runat="server" Text="Logout" 
            onclick="btnLogout_Click" />
        <a href="page.html">page.html</a>
        <a href="page.aspx">page.aspx</a>
    </div>
    </form>
</body>
</html>

Default.aspx.cs

using System;
using System.Linq;
using System.Web.Security;
using System.Security.Principal;

public partial class _Default : System.Web.UI.Page 
{
    protected void btnLogout_Click(object sender, EventArgs e)
    {
        FormsAuthentication.SignOut();
        Response.Redirect("login.aspx");
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        Response.Write("ページ要求を行っている認証されたユーザの情報");
        Response.Write("<hr>");
        IIdentity iid = User.Identity;
        iid.GetType().GetProperties().Where(
            pi => pi.Name == "AuthenticationType" || 
                pi.Name == "ImpersonationLevel" || 
                pi.Name == "IsAuthenticated" || 
                pi.Name == "IsGuest" || 
                pi.Name == "IsSystem" || 
                pi.Name == "IsAnonymous" || 
                pi.Name == "Name" || 
                pi.Name == "Groups").ToList().ForEach(
            pi => this.Response.Write(pi.Name + ":" + pi.GetValue(iid, null) + "<br>"));

        //グループも表示
        //iid.GetType().GetProperties().Where(
        //    pi => pi.Name == "Groups").ToList().ForEach(
        //    pic =>
        //    {
        //        IdentityReferenceCollection ic = pic.GetValue(iid, null) as IdentityReferenceCollection;
        //        if (ic == null)
        //        {
        //            return;
        //        }

        //        ic.OfType<System.Security.Principal.IdentityReference>()
        //            .ToList().ForEach(
        //                t => Response.Write(t.Value + "<br>"+ t.Translate(typeof( NTAccount)).Value + "<br>"));
        //    });

        Response.Write("<br>");
        Response.Write("<br>");
        Response.Write("プロセスを実行しているユーザの情報");
        Response.Write("<hr>");
        WindowsIdentity wi = WindowsIdentity.GetCurrent();
        wi.GetType().GetProperties().Where(
            pi => pi.Name == "AuthenticationType" || 
                pi.Name == "ImpersonationLevel" || 
                pi.Name == "IsAuthenticated" || 
                pi.Name == "IsGuest" || 
                pi.Name == "IsSystem" || 
                pi.Name == "IsAnonymous" || 
                pi.Name == "Name" || 
                pi.Name == "Groups").ToList().ForEach(
            pi => this.Response.Write(pi.Name + ":" + pi.GetValue(wi,null) + "<br>"));
    }
}

btnLogout_Clickでサインアウト処理しているの以外は認証の情報を表示するだけ。

「ID、パスワードを自前で確認+チケット、cookieを自前で発行(あとロールも)」
多少書かないといけないけど、それでも大した量ではないと思う。

web.config

        <authentication mode="Forms">
            <forms name="auth" loginUrl="login.aspx" path="/">
            </forms>
        </authentication>

web.configの情報はあまり使わないので記述が減った。

Login.aspx

<%@ Page Language="C#" EnableViewStateMac="true" AutoEventWireup="true" CodeFile="Login.aspx.cs" Inherits="Login"  %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>ログイン</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:TextBox ID="txtId" runat="server"></asp:TextBox>
        <br>
        <asp:TextBox ID="txtPas" runat="server"></asp:TextBox>
        <br>
        <asp:Label ID="lblPersist" runat="server" Text="次回から自動ログインする"></asp:Label><asp:CheckBox ID="cbPersist" runat="server" />
        <br />
        <br />
        <asp:Button ID="btnLogin" runat="server" Text="Login" 
            onclick="btnLogin_Click" />  
        <br />
        <asp:Label ID="lblMsg" runat="server" Text=""></asp:Label>
    </div>
    </form>
</body>
</html>

Login.aspx.cs

using System;
using System.Web;
using System.Web.Security;

public partial class Login : System.Web.UI.Page
{
    protected void btnLogin_Click(object sender, EventArgs e)
    {
        if (!Validate(this.txtId.Text, this.txtPas.Text))
        {
            this.txtPas.Text = string.Empty;
            this.lblMsg.Text = "ログイン失敗";
            return;
        }

        string role = "admin";  //ロール

        var ticket = new FormsAuthenticationTicket(
            1,       //チケットのバージョン
            this.txtId.Text,    //ユーザID
            DateTime.Now,    //発行日時
            DateTime.Now.AddMinutes(30),    //有効期限
            this.cbPersist.Checked,        //永続的なcookieに保存するかどうか
            role);                //UserData領域。好きなものを入れればいい。

        var ck = new HttpCookie(
            FormsAuthentication.FormsCookieName,
            FormsAuthentication.Encrypt(ticket));

        if (this.cbPersist.Checked)
        {
            ck.Expires = DateTime.Now.AddYears(50);
        }
        ck.Path = FormsAuthentication.FormsCookiePath;

        Response.Cookies.Add(ck);
        Response.Redirect(FormsAuthentication.GetRedirectUrl(this.txtId.Text, this.cbPersist.Checked));
    }

    //ユーザのチェック
    private bool Validate(string id, string pass)
    {
        return (id == "aaa" && pass == "aaa") || (id == "bbb" && pass == "bbb"); 
    }
}

チケットの発行、cookieへの格納を自前でやった上で元ページへリダイレクトしている。

Default.aspxとDefault.aspx.csは前と同じ。
これに加えてロールでのauthorizationをしたいのであればもう少しやらなければならないことがある。
Windows認証ではWindowsアカウントのグループがロールにマップされたけど、フォーム認証では自分でPrincipalにロールを設定しないといけない。
別にロールを設定しなくてもいい場合もあるだろうけど、ロールでのauthorizationをしたり、Page.User.IsRoleでプログラムの挙動を変えたいのであればロールの設定が必要。

認証後でauthorizationの前にロールを設定しなければならないのでロールの設定場所はHttpApplicationのAuthenticateRequestイベント、ということになる。
AuthenticateRequestイベントをハンドルするにはIHttpModule実装を用意する方法とglobal.asaxがあるみたい。
IHttpModule実相は受信フィルタパターンで使う予定だから(つうかもともと受信フィルタパターンをやってたのに。。。)今回はglobal.asaxに書いた。

global.asax

<%@ Application Language="C#" %>

<script runat="server">

    protected void Application_AuthenticateRequest(object sender, EventArgs e)
    {
        var ck = Context.Request.Cookies[FormsAuthentication.FormsCookieName];
        if (ck == null)
        {
            return;
        }

        var ticket = FormsAuthentication.Decrypt(ck.Value);
        if (ticket == null)
        {
            return;
        }

        string role = (string)ticket.UserData;
        var fid = new FormsIdentity(ticket);
        var p = new System.Security.Principal.GenericPrincipal(
            fid, new[] { role });
        Context.User = p;
    }
</script>