ASP.NET MVC Form検証
19242 ワード
一、前言
フォーム検証については、園にはすでに多くの文章があり、Web開発者も基本的に書いたと信じています.最近、個人プロジェクトでちょうど使われているので、ここで皆さんと共有してみましょう.本来はユーザー登録から書こうと思っていたのですが、発見することが多く、インタフェース、フロントエンド検証、フロントエンド暗号化、バックグラウンド復号化、ユーザーパスワードHash、権限検証など、文章が長くなる可能性があるので、ここでは主にログイン検証と権限制御部分を紹介しています.興味のある方は、ご一緒に交流してください.
一般的な検証方式はWindows検証とフォーム検証があり、webプロジェクトではフォーム検証がより多く使われています.原理は簡単で、簡単に言えばブラウザのクッキーを利用して、検証トークンをクライアントブラウザに格納し、クッキーは毎回要求に従ってサーバーに送信され、サーバーはこのトークンを検証する.通常、1つのシステムのユーザーは、匿名ユーザー、一般ユーザー、管理者など、さまざまな役割に分けられます.ここでは、ユーザーは一般ユーザーやVipユーザー、管理者は一般管理者やスーパー管理者など、さらに細分化することができます.プロジェクトでは、管理者のみが表示できるページもあれば、ログインユーザーのみが表示できるページもあります.これがロール区分(Roles)です.特定の場合、一部のページでは「張三」という名前の人だけが表示できる場合があります.これがユーザー区分(Users)です.
まず、最後に実現する効果を見てみましょう.
1.これはActionレベルでの制御です.
2.これはControllerレベルでの制御です.もちろん、アクションが匿名アクセスを必要とする場合も、制御レベルではControllerよりもアクションの優先度が大きいため許可されます.
3.Areaレベルの制御.いくつかのモジュールをパーティション化することもありますが、ここではAreaのControllerとActionでマークすることもできます.
上から分かるように、RolesとUsersをプログラムにハードに書くと、いろいろな場所でマーキング権限が必要で、あまりよくありません.プロファイルでもっと簡単に説明してほしいです.たとえば、次のような構成があります.
プロファイルに書くのは、管理を容易にするためで、プログラムにも書いてあればプロファイルを上書きします.OK、次は本題に入ります.
二、主なインタフェース
まず、主に使用されている2つのインタフェースを見てみましょう.
IPrincipalはユーザーオブジェクトの基本機能を定義し、インタフェースの定義は以下の通りである.
現在のオブジェクトが指定されたロールに属しているかどうかを判断する2つの主要メンバーがあり、IIndentityはオブジェクトを識別する情報を定義します.HttpContextのUser属性はIPrincipalタイプです.
Iidentityはオブジェクトを識別する基本機能を定義し、インタフェースは以下のように定義されています.
Iidentityにはいくつかのユーザ情報が含まれていますが、ユーザID、ユーザロールなど、より多くの情報を格納する必要がある場合があります.これらの情報はクッキーに暗号化されて保存され、検証が通過すると復号化されて逆シーケンス化され、ステータスが保存されます.たとえば、UserDataを定義します.
UserDataは、現在のユーザーロールとユーザー名が要求に合致するかどうかを判断するための2つの方法を定義するIUserDataインタフェースを実現します.インタフェースの定義は次のとおりです.
次に、次のようにPrincipal実装IPrincipalインタフェースを定義します.
Principalには、特定のUserDataではなくIUserDataが含まれています.これにより、他のコードに影響を与えることなく、1つのUserDataを交換しやすくなります.PrincipalのInRoleとIsInUserは、IUserDataの同名メソッドを間接的に呼び出します.
三、クッキーの書き込みとクッキーの読み取り
次に、ユーザーがログインに成功した後、UserDataを作成し、シーケンス化し、FormsAuthentication暗号化を再利用してクッキーに書き込む必要があります.リクエストが来ると、クッキーを復号して逆シーケンス化する必要があります.次のようになります.
ログイン時には、次のように処理できます.
ログインに成功すると、クッキーに情報が書き込まれ、ブラウザでリクエストを見ることができます.「Form」という名前のクッキー(プロファイルを簡単に構成する必要もあります)があります.その値は暗号化された文字列で、後続のリクエストはこのクッキーリクエストに基づいて検証されます.具体的には、HttpApplicationのAuthenticateRequest検証イベントで、次のようなTryParsePrincipalを呼び出します.
ここで検証が通らなければ、HttpContext.Current.Userはnullであり、現在のユーザーが識別されていないことを示します.しかし、ここでは権限に関する処理はできません.上述したように、匿名アクセスが許可されているページがあるからです.
三、AuthorizeAttribute
これは、Actionの実行前に実行されるFilterであり、IActionFilterインタフェースを実現しています.Filterについては、私の前のこの文章を見ることができますが、ここではあまり紹介しません.RequestAuthorizeAttributeがAuthorizeAttributeを継承し、そのOnAuthorizationメソッドを再書き込みします.ControllerまたはActionがこのプロパティをマークしている場合、このメソッドはActionの実行前に実行され、ここでログインしたかどうかと権限があるかどうかを判断し、ない場合は対応する処理を行います.具体的なコードは以下の通りです.
注意:ここのコードは個人プロジェクトから抜粋され、一部のコードが簡潔に書かれています.一部は補助クラスで、コードは貼られていませんが、読書には影響しません.
1. HttpApplicationのAuthenticateRequestイベントで取得したIPrincipalがnullの場合、検証は失敗します.
2.検証に合格した場合、プログラムはAuthorizeAttributeのRolesおよびUserプロパティを検証します.
3.検証に合格した場合、プログラムはプロファイル内の対応するRolesプロパティとUsersプロパティを検証します.
プロファイルの検証方法は次のとおりです.
現在要求されているarea、controller、actionの名前に基づいて、次のように定義されたAuthorizationConfigクラスで検証されます.
ここでのコードは長いが,主な論理は文章の先頭の構成情報を解析することである.
プログラムの実装手順を簡単にまとめます.
1. ユーザー名とパスワードを照合した後、SetAuthenticationCookieを呼び出してステータス情報をクッキーに書き込みます.
2. HttpApplicationのAuthenticationイベントで、TryParsePrincipalを呼び出してステータス情報を取得します.
3. 検証が必要なアクション(またはController)にRequestAuthorizeAttributeプロパティをマークし、RolesとUsersを設定します.RolesとUsersは、プロファイルで構成することもできます.
4. 認証および権限論理処理は、RequestAuthorizeAttributeのOnAuthorizationメソッドで行います.
四、まとめ
上はログイン認証全体のコア実装プロセスであり、簡単に構成すれば実現できます.しかし、実際のプロジェクトでは、ユーザー登録からユーザー管理までのプロセスが複雑であり、前後の検証、復号化の問題に関連しています.セキュリティの問題に関しては、FormsAuthenticationは暗号化の際にサーバのMachineKeyなどの情報に基づいて暗号化されるので、比較的安全です.もちろん、要求が悪意的にブロックされ、偽造されてログインされる可能性があるとすれば、安全なhttpプロトコルhttpsの使用など、後で考慮すべき問題である.
フォーム検証については、園にはすでに多くの文章があり、Web開発者も基本的に書いたと信じています.最近、個人プロジェクトでちょうど使われているので、ここで皆さんと共有してみましょう.本来はユーザー登録から書こうと思っていたのですが、発見することが多く、インタフェース、フロントエンド検証、フロントエンド暗号化、バックグラウンド復号化、ユーザーパスワードHash、権限検証など、文章が長くなる可能性があるので、ここでは主にログイン検証と権限制御部分を紹介しています.興味のある方は、ご一緒に交流してください.
一般的な検証方式はWindows検証とフォーム検証があり、webプロジェクトではフォーム検証がより多く使われています.原理は簡単で、簡単に言えばブラウザのクッキーを利用して、検証トークンをクライアントブラウザに格納し、クッキーは毎回要求に従ってサーバーに送信され、サーバーはこのトークンを検証する.通常、1つのシステムのユーザーは、匿名ユーザー、一般ユーザー、管理者など、さまざまな役割に分けられます.ここでは、ユーザーは一般ユーザーやVipユーザー、管理者は一般管理者やスーパー管理者など、さらに細分化することができます.プロジェクトでは、管理者のみが表示できるページもあれば、ログインユーザーのみが表示できるページもあります.これがロール区分(Roles)です.特定の場合、一部のページでは「張三」という名前の人だけが表示できる場合があります.これがユーザー区分(Users)です.
まず、最後に実現する効果を見てみましょう.
1.これはActionレベルでの制御です.
public class Home1Controller : Controller
{
//
public ActionResult Index()
{
return View();
}
//
[RequestAuthorize]
public ActionResult Index2()
{
return View();
}
// ,
[RequestAuthorize(Users=" ")]
public ActionResult Index3()
{
return View();
}
//
[RequestAuthorize(Roles="Admin")]
public ActionResult Index4()
{
return View();
}
}
2.これはControllerレベルでの制御です.もちろん、アクションが匿名アクセスを必要とする場合も、制御レベルではControllerよりもアクションの優先度が大きいため許可されます.
//Controller
[RequestAuthorize(User=" ")]
public class Home2Controller : Controller
{
//
public ActionResult Index()
{
return View();
}
//
[AllowAnonymous]
public ActionResult Index2()
{
return View();
}
}
3.Areaレベルの制御.いくつかのモジュールをパーティション化することもありますが、ここではAreaのControllerとActionでマークすることもできます.
上から分かるように、RolesとUsersをプログラムにハードに書くと、いろいろな場所でマーキング権限が必要で、あまりよくありません.プロファイルでもっと簡単に説明してほしいです.たとえば、次のような構成があります.
Admin
Admin
プロファイルに書くのは、管理を容易にするためで、プログラムにも書いてあればプロファイルを上書きします.OK、次は本題に入ります.
二、主なインタフェース
まず、主に使用されている2つのインタフェースを見てみましょう.
IPrincipalはユーザーオブジェクトの基本機能を定義し、インタフェースの定義は以下の通りである.
public interface IPrincipal
{
//
IIdentity Identity { get; }
//
bool IsInRole(string role);
}
現在のオブジェクトが指定されたロールに属しているかどうかを判断する2つの主要メンバーがあり、IIndentityはオブジェクトを識別する情報を定義します.HttpContextのUser属性はIPrincipalタイプです.
Iidentityはオブジェクトを識別する基本機能を定義し、インタフェースは以下のように定義されています.
public interface IIdentity
{
//
string AuthenticationType { get; }
//
bool IsAuthenticated { get; }
//
string Name { get; }
}
Iidentityにはいくつかのユーザ情報が含まれていますが、ユーザID、ユーザロールなど、より多くの情報を格納する必要がある場合があります.これらの情報はクッキーに暗号化されて保存され、検証が通過すると復号化されて逆シーケンス化され、ステータスが保存されます.たとえば、UserDataを定義します.
public class UserData : IUserData
{
public long UserID { get; set; }
public string UserName { get; set; }
public string UserRole { get; set; }
public bool IsInRole(string role)
{
if (string.IsNullOrEmpty(role))
{
return true;
}
return role.Split(',').Any(item => item.Equals(this.UserRole, StringComparison.OrdinalIgnoreCase));
}
public bool IsInUser(string user)
{
if (string.IsNullOrEmpty(user))
{
return true;
}
return user.Split(',').Any(item => item.Equals(this.UserName, StringComparison.OrdinalIgnoreCase));
}
}
UserDataは、現在のユーザーロールとユーザー名が要求に合致するかどうかを判断するための2つの方法を定義するIUserDataインタフェースを実現します.インタフェースの定義は次のとおりです.
public interface IUserData
{
bool IsInRole(string role);
bool IsInUser(string user);
}
次に、次のようにPrincipal実装IPrincipalインタフェースを定義します.
public class Principal : IPrincipal
{
public IIdentity Identity{get;private set;}
public IUserData UserData{get;set;}
public Principal(FormsAuthenticationTicket ticket, IUserData userData)
{
EnsureHelper.EnsureNotNull(ticket, "ticket");
EnsureHelper.EnsureNotNull(userData, "userData");
this.Identity = new FormsIdentity(ticket);
this.UserData = userData;
}
public bool IsInRole(string role)
{
return this.UserData.IsInRole(role);
}
public bool IsInUser(string user)
{
return this.UserData.IsInUser(user);
}
}
Principalには、特定のUserDataではなくIUserDataが含まれています.これにより、他のコードに影響を与えることなく、1つのUserDataを交換しやすくなります.PrincipalのInRoleとIsInUserは、IUserDataの同名メソッドを間接的に呼び出します.
三、クッキーの書き込みとクッキーの読み取り
次に、ユーザーがログインに成功した後、UserDataを作成し、シーケンス化し、FormsAuthentication暗号化を再利用してクッキーに書き込む必要があります.リクエストが来ると、クッキーを復号して逆シーケンス化する必要があります.次のようになります.
public class HttpFormsAuthentication
{
public static void SetAuthenticationCookie(string userName, IUserData userData, double rememberDays = 0)
{
EnsureHelper.EnsureNotNullOrEmpty(userName, "userName");
EnsureHelper.EnsureNotNull(userData, "userData");
EnsureHelper.EnsureRange(rememberDays, "rememberDays", 0);
// cookie
string userJson = JsonConvert.SerializeObject(userData);
//
double tickekDays = rememberDays == 0 ? 7 : rememberDays;
var ticket = new FormsAuthenticationTicket(2, userName,
DateTime.Now, DateTime.Now.AddDays(tickekDays), false, userJson);
//FormsAuthentication web forms
//
string encryptValue = FormsAuthentication.Encrypt(ticket);
// cookie
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptValue);
cookie.HttpOnly = true;
cookie.Domain = FormsAuthentication.CookieDomain;
if (rememberDays > 0)
{
cookie.Expires = DateTime.Now.AddDays(rememberDays);
}
HttpContext.Current.Response.Cookies.Remove(cookie.Name);
HttpContext.Current.Response.Cookies.Add(cookie);
}
public static Principal TryParsePrincipal(HttpContext context)
where TUserData : IUserData
{
EnsureHelper.EnsureNotNull(context, "context");
HttpRequest request = context.Request;
HttpCookie cookie = request.Cookies[FormsAuthentication.FormsCookieName];
if(cookie == null || string.IsNullOrEmpty(cookie.Value))
{
return null;
}
// cookie
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
if(ticket == null || string.IsNullOrEmpty(ticket.UserData))
{
return null;
}
IUserData userData = JsonConvert.DeserializeObject(ticket.UserData);
return new Principal(ticket, userData);
}
}
ログイン時には、次のように処理できます.
public ActionResult Login(string userName,string password)
{
// ...
UserData userData = new UserData()
{
UserName = userName,
UserID = userID,
UserRole = "Admin"
};
HttpFormsAuthentication.SetAuthenticationCookie(userName, userData, 7);
// ...
}
ログインに成功すると、クッキーに情報が書き込まれ、ブラウザでリクエストを見ることができます.「Form」という名前のクッキー(プロファイルを簡単に構成する必要もあります)があります.その値は暗号化された文字列で、後続のリクエストはこのクッキーリクエストに基づいて検証されます.具体的には、HttpApplicationのAuthenticateRequest検証イベントで、次のようなTryParsePrincipalを呼び出します.
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
HttpContext.Current.User = HttpFormsAuthentication.TryParsePrincipal(HttpContext.Current);
}
ここで検証が通らなければ、HttpContext.Current.Userはnullであり、現在のユーザーが識別されていないことを示します.しかし、ここでは権限に関する処理はできません.上述したように、匿名アクセスが許可されているページがあるからです.
三、AuthorizeAttribute
これは、Actionの実行前に実行されるFilterであり、IActionFilterインタフェースを実現しています.Filterについては、私の前のこの文章を見ることができますが、ここではあまり紹介しません.RequestAuthorizeAttributeがAuthorizeAttributeを継承し、そのOnAuthorizationメソッドを再書き込みします.ControllerまたはActionがこのプロパティをマークしている場合、このメソッドはActionの実行前に実行され、ここでログインしたかどうかと権限があるかどうかを判断し、ない場合は対応する処理を行います.具体的なコードは以下の通りです.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequestAuthorizeAttribute : AuthorizeAttribute
{
//
public override void OnAuthorization(AuthorizationContext context)
{
EnsureHelper.EnsureNotNull(context, "httpContent");
//
if (context.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), false))
{
return;
}
//
Principal principal = context.HttpContext.User as Principal;
if (principal == null)
{
SetUnAuthorizedResult(context);
HandleUnauthorizedRequest(context);
return;
}
//
if (!principal.IsInRole(base.Roles) || !principal.IsInUser(base.Users))
{
SetUnAuthorizedResult(context);
HandleUnauthorizedRequest(context);
return;
}
//
if(!ValidateAuthorizeConfig(principal, context))
{
SetUnAuthorizedResult(context);
HandleUnauthorizedRequest(context);
return;
}
}
//
private void SetUnAuthorizedResult(AuthorizationContext context)
{
HttpRequestBase request = context.HttpContext.Request;
if (request.IsAjaxRequest())
{
// ajax
string result = JsonConvert.SerializeObject(JsonModel.Error(403));
context.Result = new ContentResult() { Content = result };
}
else
{
//
string loginUrl = FormsAuthentication.LoginUrl + "?ReturnUrl=" + preUrl;
context.Result = new RedirectResult(loginUrl);
}
}
//override
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if(filterContext.Result != null)
{
return;
}
base.HandleUnauthorizedRequest(filterContext);
}
}
注意:ここのコードは個人プロジェクトから抜粋され、一部のコードが簡潔に書かれています.一部は補助クラスで、コードは貼られていませんが、読書には影響しません.
1. HttpApplicationのAuthenticateRequestイベントで取得したIPrincipalがnullの場合、検証は失敗します.
2.検証に合格した場合、プログラムはAuthorizeAttributeのRolesおよびUserプロパティを検証します.
3.検証に合格した場合、プログラムはプロファイル内の対応するRolesプロパティとUsersプロパティを検証します.
プロファイルの検証方法は次のとおりです.
private bool ValidateAuthorizeConfig(Principal principal, AuthorizationContext context)
{
//action , ActionName
ActionNameAttribute actionNameAttr = context.ActionDescriptor
.GetCustomAttributes(typeof(ActionNameAttribute), false)
.OfType().FirstOrDefault();
string actionName = actionNameAttr == null ? null : actionNameAttr.Name;
AuthorizationConfig ac = ParseAuthorizeConfig(actionName, context.RouteData);
if (ac != null)
{
if (!principal.IsInRole(ac.Roles))
{
return false;
}
if (!principal.IsInUser(ac.Users))
{
return false;
}
}
return true;
}
private AuthorizationConfig ParseAuthorizeConfig(string actionName, RouteData routeData)
{
string areaName = routeData.DataTokens["area"] as string;
string controllerName = null;
object controller, action;
if(string.IsNullOrEmpty(actionName))
{
if(routeData.Values.TryGetValue("action", out action))
{
actionName = action.ToString();
}
}
if (routeData.Values.TryGetValue("controller", out controller))
{
controllerName = controller.ToString();
}
if(!string.IsNullOrEmpty(controllerName) && !string.IsNullOrEmpty(actionName))
{
return AuthorizationConfig.ParseAuthorizationConfig(
areaName, controllerName, actionName);
}
return null;
}
}
現在要求されているarea、controller、actionの名前に基づいて、次のように定義されたAuthorizationConfigクラスで検証されます.
public class AuthorizationConfig
{
public string Roles { get; set; }
public string Users { get; set; }
private static XDocument _doc;
//
private static string _path = "~/Identity/Authorization.xml";
//
static AuthorizationConfig()
{
string absPath = HttpContext.Current.Server.MapPath(_path);
if (File.Exists(absPath))
{
_doc = XDocument.Load(absPath);
}
}
// , Roles Users
public static AuthorizationConfig ParseAuthorizationConfig(string areaName, string controllerName, string actionName)
{
EnsureHelper.EnsureNotNullOrEmpty(controllerName, "controllerName");
EnsureHelper.EnsureNotNullOrEmpty(actionName, "actionName");
if (_doc == null)
{
return null;
}
XElement rootElement = _doc.Element("root");
if (rootElement == null)
{
return null;
}
AuthorizationConfig info = new AuthorizationConfig();
XElement rolesElement = null;
XElement usersElement = null;
XElement areaElement = rootElement.Elements("area")
.Where(e => CompareName(e, areaName)).FirstOrDefault();
XElement targetElement = areaElement ?? rootElement;
XElement controllerElement = targetElement.Elements("controller")
.Where(e => CompareName(e, controllerName)).FirstOrDefault();
// area controller null
if (areaElement == null && controllerElement == null)
{
return null;
}
// area
if (controllerElement == null)
{
rootElement = areaElement.Element("roles");
usersElement = areaElement.Element("users");
}
else
{
XElement actionElement = controllerElement.Elements("action")
.Where(e => CompareName(e, actionName)).FirstOrDefault();
if (actionElement != null)
{
// action
rolesElement = actionElement.Element("roles");
usersElement = actionElement.Element("users");
}
else
{
// controller
rolesElement = controllerElement.Element("roles");
usersElement = controllerElement.Element("users");
}
}
info.Roles = rolesElement == null ? null : rolesElement.Value;
info.Users = usersElement == null ? null : usersElement.Value;
return info;
}
private static bool CompareName(XElement e, string value)
{
XAttribute attribute = e.Attribute("name");
if (attribute == null || string.IsNullOrEmpty(attribute.Value))
{
return false;
}
return attribute.Value.Equals(value, StringComparison.OrdinalIgnoreCase);
}
}
ここでのコードは長いが,主な論理は文章の先頭の構成情報を解析することである.
プログラムの実装手順を簡単にまとめます.
1. ユーザー名とパスワードを照合した後、SetAuthenticationCookieを呼び出してステータス情報をクッキーに書き込みます.
2. HttpApplicationのAuthenticationイベントで、TryParsePrincipalを呼び出してステータス情報を取得します.
3. 検証が必要なアクション(またはController)にRequestAuthorizeAttributeプロパティをマークし、RolesとUsersを設定します.RolesとUsersは、プロファイルで構成することもできます.
4. 認証および権限論理処理は、RequestAuthorizeAttributeのOnAuthorizationメソッドで行います.
四、まとめ
上はログイン認証全体のコア実装プロセスであり、簡単に構成すれば実現できます.しかし、実際のプロジェクトでは、ユーザー登録からユーザー管理までのプロセスが複雑であり、前後の検証、復号化の問題に関連しています.セキュリティの問題に関しては、FormsAuthenticationは暗号化の際にサーバのMachineKeyなどの情報に基づいて暗号化されるので、比較的安全です.もちろん、要求が悪意的にブロックされ、偽造されてログインされる可能性があるとすれば、安全なhttpプロトコルhttpsの使用など、後で考慮すべき問題である.