ASP.NET MVC Form検証

19242 ワード

一、前言
フォーム検証については、園にはすでに多くの文章があり、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の使用など、後で考慮すべき問題である.