PrismBox > Tips >

 
ASP.NET Tips
 
No.0001 Windowsユーザーアカウントを使わずにBASIC認証を行う
BASIC認証はほとんどのWebサーバー、Webブラウザが対応しており、簡単に使えることが特徴ですが、パスワードはクリアテキスト(平文)で送られます。 実際はE-Mailの添付ファイルでよく使われるBASE64という形式でエンコードされていますが、暗号化されているわけではないのでデコードして改ざんすることも容易です。 ですから、多くの場合はSSLとあわせて暗号化して用いられます。

IISにも、このBASIC認証(基本認証)を行う機能が付いています。ASP.NETでは「Web.config」ファイルで「<authentication mode="Windows" />」を指定し、 IISの「ディレクトリセキュリティ」-「認証方法」で「基本認証」にチェックを付けることで利用できます。 ただし、IISではBASIC認証でログインするユーザーは “Windowsにユーザーアカウントを持っている” 必要があります。つまり、 「BASIC認証のパスワード=Windowsユーザーアカウントのパスワード」となっています。 Webからのアクセス許可をしたいだけの場合もWindowsのユーザーアカウントを作らねばなりません。 できればDBに入っているユーザー名、パスワードを使って認証をする、とかしたいところです。

そういうわけでSSLによる暗号化までは行っていませんが、サンプルを作成しました。まずは「Web.config」の設定です。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.web>
        <compilation defaultLanguage="c#" debug="true" /> ←テストなのでとりあえずDebug
        <customErrors mode="RemoteOnly" />
        <authentication mode="None" /> ←独自に認証を実装するので「None」にする
        <authorization>
            <deny users="?" /> ←許可ユーザーは特に指定せずに匿名のみ拒否する
        </authorization>
        <globalization
            requestEncoding="Shift_JIS"
            responseEncoding="Shift_JIS"
        /> ↑文字エンコーディングはシフトJIS
    </system.web>
    <appSettings>
        <add key="AuthRealm" value="Authentication Test" />
    </appSettings> ↑認証ダイアログに表示する文字列の定義
</configuration>
「authentication mode」は他に「Forms」「Passport」「Windows」がありますが、今回は独自に実装するので「None」に設定しておきます。
「authorization」では「deny users="?"」を設定しています。これは未認証のユーザーのみを拒否する設定です。認証を通っていればどのユーザーでもアクセスを許可するということです。 ユーザーロールをみて許可したい場合、例えば「Users」のみを許可したい場合は「allow roles="Users"」と指定します。「deny」や「allow」は組み合わせて設定できます。
「appSettings」の「AuthRealm」は認証のダイアログに表示する文字列を設定しています。これはソースコードに含めることもできますが、今回は「Web.config」に書き出しました。

次は「Global.asax」です。認証の処理はすべてここに書きます。
<%@ Application language="C#" %>
<%@ Import Namespace="System.Configuration" %>
<%@ Import Namespace="System.Security.Principal" %>

<script runat="server">
    public void Application_OnAuthenticateRequest(Object sender, EventArgs e) {
        HttpApplication app = (HttpApplication)sender;
        string strAuth = app.Request.Headers["Authorization"];

        if (strAuth == null || strAuth.Length == 0) {
            // 認証用のヘッダ(Authorization)がない場合
            return;
        }

        // 念のためトリミング
        strAuth = strAuth.Trim();

        if (strAuth.IndexOf("Basic", 0) != 0) {
            // 認証用のヘッダの構文がおかしい場合
            return;
        }

        // 「Basic 」以降の文字列を抽出
        string encodedString = strAuth.Substring(6);

        // BASE64をデコードする
        byte[] decodedBytes = Convert.FromBase64String(encodedString);
        string decodedString = new ASCIIEncoding().GetString(decodedBytes);

        // ユーザー名とパスワードを分離
        string[] arrSplited = decodedString.Split(new char[] {':'});
        string username = arrSplited[0];
        string password = arrSplited[1];

        // 認証処理
        string[] roles;
        if (AuthenticateUser(username, password, out roles)) {
            // セキュリティ情報を設定
            app.Context.User = new GenericPrincipal(new GenericIdentity(username), roles);

        } else {
            // 認証に失敗した場合はアクセス拒否の情報をセットする
            app.Response.StatusCode = 401;
            app.Response.StatusDescription = "Unauthorized";

            // 認証に失敗したことを通知する
            app.Response.Write("<html>\r\n<head>\r\n<title>");
            app.Response.Write("401 Unauthorized</title>\r\n</head>\r\n<body>\r\n");
            app.Response.Write("<h1>401 Access Denied</h1>\r\n");
            app.Response.Write("</body>\r\n</html>\r\n");

            app.CompleteRequest();

            return;

        }
    }

    public void Application_OnEndRequest(Object sender, EventArgs e) {
        HttpApplication app = (HttpApplication)sender;

        // 認証が成功していない場合は「WWW-Authenticate」ヘッダを付加する
        if (app.Response.StatusCode == 401) {
            string strRealm = ConfigurationSettings.AppSettings["AuthRealm"];
                   strRealm = "Basic Realm=" + strRealm;
            app.Response.AppendHeader("WWW-Authenticate", strRealm);
        }
    }

    private bool AuthenticateUser(string username, string password, out string[] roles) {
        bool ret = false;
        
        // 認証処理
        if (username == "test" && password == "test") {
            ret = true;
        }

        // ユーザーに割り当てるロールを作成
        roles = new string[1] {"Users"};

        return ret;
    }
</script>
「AuthenticateRequest」は「HttpApplication」クラスのパブリックイベントで、セキュリティモジュールがユーザーのIDを確立すると発生します。 「Global.asax」ではこれを「Application_OnAuthenticateRequest」メソッドで処理します。
「Application_OnAuthenticateRequest」のsenderオブジェクトは「HttpApplication」です。まずはこれを取得します。 そしてリクエストヘッダから「Authorization」ヘッダを探して取得します。「Authorization」ヘッダが取得できなかった場合はここで処理を終え、 後述の「Application_OnEndRequest」で処理を行います。さて、ここで「Authorization」ヘッダが得られた場合、これの処理を行います。「Authorization」ヘッダの中身は次のようになっています。
Basic [BASE64でエンコードされたユーザー名とパスワード]
ここからユーザー名とパスワードを取り出すために「Substring」メソッドで文字列の切り出し、デコードを行います。 デコード後の文字列は
[ユーザー名]:[パスワード]
となっているので、これを「Split」メソッドで分割してユーザー名とパスワードを取得します。

ユーザー名とパスワードが得られたので、次に「AuthenticateUser」で実際の認証を行います。「AuthenticateUser」は成功か否かでbool型を返すようにし、 ユーザーロールの取得も行いたいので「out」パラメータをつけて1次元の文字列配列を渡します。(「out」パラメータは「ref」パラメータと同様、 参照渡しをするためのキーワードですが、「ref」とは異なり、あらかじめ初期化する必要がありません。)ここで実際の認証処理を行います。 サンプルでは単純にソースコードに「test」「test」を埋め込んでユーザーロールとして「Users」を返すようにしていますが、実際にはここでDBへのアクセスを行い、 ユーザー名、パスワードをチェック、そのユーザーが属するロールの配列を取得、などの処理を行います。DBでは大げさな場合はXMLファイルでもよいかもしれません。 ここではユーザーロールとして「Users」のみを割り当てていますが、
roles = new string[3] {"Users", "Administrators", "Developers"};
などとして、複数割り当てることもできます。

さて、「AuthenticateUser」がtrueを返した場合、「HttpContext」クラス(app.Context)Userプロパティのセットを行います。 プリンシパルオブジェクトを代入しなければならないので、「GenericPrincipal」クラスのインスタンスを入れます。 ユーザー名をそのまま使うことはできないので「GenericIdentity」クラスで標準ユーザーを作成して渡します。第2引数には先ほど作成したロールの文字列配列を渡します。 これでBASIC認証の処理は完了です。

一方、「AuthenticateUser」がtrueを返した場合は認証失敗です。ステータスコードを「401」(未認証,認証失敗)にし、それを表すHTMLを出力します。 最後の「CompleteRequest」メソッドは「EndRequest」を即時実行させるメソッドです。

「EndRequest」は「HttpApplication」クラスのパブリックイベントで、ASP.NETが要求に応答するときに、実行のHTTPパイプラインチェインの最後のイベントとして発生します。 つまり「Application_OnEndRequest」メソッドは最後に実行されるメソッドです。ステータスコードが「401」の場合(「Authorization」ヘッダがない場合と、未認証(または失敗している)場合)に 「WWW-Authenticate」ヘッダを追加します。内容は
Basic realm="[ダイアログに表示する文字列]"
です。ここではダイアログに表示する文字列は「Web.config」の「AuthRealm」から読み取っています。これで認証を要求するダイアログが表示されます。

これで認証の仕組みは完成です。「Web.config」と「Global.asax」をASP.NETアプリケーションのフォルダに入れます(ここでは「c:\Inetpub\wwwroot\authtest」とします)。 このフォルダには「Default.aspx」等の認証をかけたいASP.NETのページファイルも入れておいてください。次にIISの設定を行います。IISの設定ウィンドウから「authtest」フォルダを右クリックし、 「プロパティ」のウィンドウを開きます。


「ディレクトリ」タブの「アプリケーションの設定」で「作成」ボタンをクリックします。すると上図のようになっていると思います。次に「ディレクトリ セキュリティ」タブの 「匿名アクセスおよび認証コントロール」で「編集」ボタンをクリックします。


上図のウィンドウが開くので、図のように「匿名アクセス」のみチェックし、他のチェックをすべてはずします。これですべての作業が完了です。

Webブラウザから「http://localhost/authtest/Default.aspx」等の「*.aspx」ファイルにアクセスしてください。認証のダイアログが開き、正しいユーザー名とパスワードを入力すると、ページを見ることができ、 不正な値を入力するとアクセスははじかれます。


以上のようにしてWindowsユーザーアカウントを使わないBASIC認証は実装できます。
注意点としては(どの認証方式にも言えることですが)ASP.NETのファイル以外には認証がかからないことが挙げられます。 「*.html」や「*.gif; *.jpg; *.png」等は「http://localhost/authtest/test.html」と直接アクセスすれば認証不要で表示できます。すべてをASP.NETのファイルで構成するか、 その他のファイルをASP.NETのファイルとして認識するようにしなければいけないと思います。(確かそんな感じ)