詳しくはASP.NET MVC Form検証

17434 ワード

一、前言
フォーム検証については、Web開発者も基本的に書いたと信じている文章が少なくありませんが、最近は個人プロジェクトでちょうど使われているので、ここで皆さんと共有してみましょう.本来はユーザー登録から書こうと思っていたのですが、発見することが多く、インタフェース、フロントエンド検証、フロントエンド暗号化、バックグラウンド復号化、ユーザーパスワードHash、権限検証など、文章が長くなる可能性があるので、ここでは主にログイン検証と権限制御部分を紹介しています.興味のある方は、ご一緒に交流してください.
一般的な検証方式はWindows検証とフォーム検証があり、webプロジェクトではフォーム検証がより多く使われています.原理は簡単で、簡単に言えばブラウザのクッキーを利用して、検証トークンをクライアントブラウザに格納し、クッキーは毎回要求に従ってサーバーに送信され、サーバーはこのトークンを検証する.通常、1つのシステムのユーザーは、匿名ユーザー、一般ユーザー、管理者など、さまざまな役割に分けられます.ここでは、ユーザーは一般ユーザーやVipユーザー、管理者は一般管理者やスーパー管理者など、さらに細分化することができます.プロジェクトでは、管理者のみが表示できるページもあれば、ログインユーザーのみが表示できるページもあります.これがロール区分(Roles)です.特定の場合、一部のページでは「張三」という名前の人だけが表示できる場合があります.これがユーザー区分(Users)です.
まず、最後に実現する効果を見てみましょう.
1.アクションレベルでの制御です.

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の使用など、後で考慮すべき問題である.
以上が本文のすべての内容で、みんなの学習に役立つことを望みます.