ASP.NET Ajaxとの相性が良くないよね、という思いからprototype.jsへ返り咲き。 jQueryにはすごいのがあったしね。あ、でもそこまで(Controllerを派生させてExecuteとRenderViewをオーバーライドして、Site.Masterか Ajax.Masterか切り替え。さらにクライアントサイドのjQueryで対象エレメント部だけを抽出しなおして描画に反映。すごくて感動した)のはちょっと面倒なので、軽くね。
- まず、普通にASP.NET MVC(Preview2)でプロジェクト作成。
- そしたらControllers/Models/Views/Contentフォルダができるので、このContentにprototype.jsを入れる。
- Controllersに名前を"SecureController"を作成(なんでもいいんだけど)。
- Controllersに名前を"ApiController"を作成(なんでもいいんだけど)。
- Views/Homeフォルダに"Mvc View Content Page"をテンプレートに"Login.aspx"を作成。
- 同じくViews/Apiフォルダ(作る)に"Mvc View Page"(ここ大事!)をテンプレートに"Login.aspx"を作成。
- Views/Secureフォルダ(作る)に"Mvc View Content Page"として"Index.aspx"を作成。
ここまでで準備完了。イメージ的には/Secure/Indexはログインしてないとアクセスできないよ、って言う感じです。 ルートのweb.configでForm認証使うことと、セキュアなパスを指定。
<authentication mode="Forms"> <forms loginUrl="/Home/Login"></forms> </authentication>
↑これと↓これ。
<location path="Secure"> <system.web> <authorization> <deny users="?"/> </authorization> </system.web> </location>
Views/Shared/Site.Masterのhead要素内にprototype.jsへのscriptタグを作成。
<script src="/Content/prototype.js" type="text/javascript"></script>
※常にルートを指すようにしてるけど、相対で参照するようにしたい場合は、ヘルパーを作りましょう。標準で用意してくれることを希望しつつ。 Views/Home/Indexの中身は単純にSecure/Indexへのリンクだけ。これをActionLink<T>で作成(なんかこれがお勧めなリンクの作り方っぽい)。
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcApplication1.Views.Home.Index" %> <%@import Namespace="MvcApplication1.Controllers" %> <asp:Content ID="indexContent" ContentPlaceHolderID="MainContentPlaceHolder" runat="server"> <h2>Ajaxな認証機能</h2> <p> <%= Html.ActionLink<SecureController>(c=>c.Index(),"Secure Page") %> </p> </asp:Content>
※@importでコントローラのnamespaceを指定しておく。
"Secure Page"のリンク先は"/Secure/Index"(Indexは省略されてるけど)。 中身は単純に↓こんな感じにしときました。
<h2>要認証!!</h2> ログインしてないとみれないよ。<br /> Welcome <b><%=User.Identity.Name %></b>!!
この段階でリンクをクリックすると認証してないので、単純に”Home/Login”にリダイレクトされます。なので、今度はそっちを用意。
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="MvcApplication1.Views.Home.Login" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContentPlaceHolder" runat="server"> <form> <table> <tr> <th>Longin ID</th> <td><%=Html.TextBox("id") %></td> </tr> <tr> <th>Password</th> <td><%=Html.Password("pass") %></td> </tr> <tr> <td colspan="2"><input type="button" value="Login" onclick="login()" /></td> </tr> </table> </form> <script type="text/javascript"> function login() { var params = $H({"id":$F("id"), "pass":$F("pass")}).toQueryString(); new Ajax.Request("/Api/Login",{ method:"post", postBody:params, onComplete:function(req){ var status = req.status; //req.responseText if (status==200) alert("Success!! " + req.responseText); else alert("Fail... " + req.responseText); } }); } </script> </asp:Content>
認証するためのリクエストパスが"Api/Login"になってるのと、認証結果はRESTfulっぽくステータスコードで判定。今回は成功すれば200(OK)、失敗すれば406(Not Acceptable)を利用(使い方があってるのか自信ないけど...)。 HTTPステータスコード – Wikipedia
続いて、Api/Loginの中身。今回は単純なIDとPasswordを利用してみます。実際にはMembershipを使うと思うけど。
public void Login()
{
string id = this.ReadFromRequest("id");
string pass = this.ReadFromRequest("pass");
ViewData["login"] = false;
if (id == "yama" && pass == "kawa")
{
ViewData["login"] = true;
FormsAuthentication.SetAuthCookie(id, false);
}
RenderView("Login");
}
続いて、Views/Api/Login。 ここは戻り値を返すだけの機能にしたいし、レスポンス内容をJsonにしてみるために、Masterを参照しないページ(Mvc View Page)にしました。
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="MvcApplication1.Views.Api.Login" %> (<%= ResponseJson() %>)
ページの中身は↑これだけ。
public void Page_Load() { Response.ContentType = "application/json"; if ((bool)ViewData["login"] == true) Response.StatusCode = 200; else Response.StatusCode = 406; } public string ResponseJson() { return "{\"result\":" + (ViewData["login"]+"").ToLower() + "}"; }
↑コードもこれだけ。Jsonをベタ書きしてるけど、実際には上手いことシリアライズを使うでしょうね。 で、動かす!
あえて、違うID/Passwordを入れてみる。
正しく入れる。
この成功か失敗の判定はレスポンス内容のJsonをevalして判定せず、XHRのstatsプロパティが200かどうかで判定。RESTfulっぽい~。 画像だと分かりにくいけど、画面のリフレッシュは発生してないからね! 最後に、Home/Indexに戻って"Secure Page"のリンクをクリックすると~。
イエ~イ。 実際にはControllerでSetAuthCookieやるより、Viewでやるの?ViewでJsonとか返したかったらControllerFactory使うの?えらい人教えて! これやってて思ったけど、承認はアクション毎にかけるのがMVCっぽいんだね(今回は違うけど)。リソースに対するアクションに権限があるかどうかの方が、コントローラに権限があるかどうかよりもRESTfulだし。フォルダに制限をかけるっていう発想じゃなくて、誰がどのリソースにどんな権限があるかってことだもんね。 結局ActionFilterAttributeで制限かけるのが王道になるのかな~。