ASP. net mvc Form verification, asp. netmvc
I. Preface
There have been a lot of articles in the garden about form verification. I believe that Web developers have basically written about it and recently used it in individual projects. I would like to share it with you here. I originally wanted to write from user registration, but found many things, involving interfaces, front-end verification, front-end encryption, backend decryption, user password Hash, permission verification, and so on, the article may take a long time to write, so here we mainly introduce the login authentication and permission control sections. If you are interested, please join us.
Generally, the authentication methods include Windows verification and form verification. More web projects use form verification. The principle is very simple. Simply put, the authentication token is stored in the client browser using the cookie of the browser. Each time the cookie sends a request to the server, the server verifies the token. Generally, users in a system can be divided into multiple roles: anonymous users, common users, and administrators. The roles can be further subdivided. For example, users can be ordinary users or Vip users, administrators can be common administrators or super administrators. In the project, some pages may only allow the Administrator to view, and some may only Allow logon users to view, which means role differentiation. In some special cases, some pages can only be viewed by people named "Zhang San", which is user differentiation (Users ).
Let's take a look at the final effect:
1. This is Action-level control.
Public class Home1Controller: Controller {// anonymous access public ActionResult Index () {return View ();} // log on to the user to access [RequestAuthorize] public ActionResult Index2 () {return View ();} // Login User. Michael can access [RequestAuthorize (Users = "James")] public ActionResult Index3 () {return View ();} // The Administrator accesses [RequestAuthorize (Roles = "Admin")] public ActionResult Index4 () {return View ();}}
2. This is Controller-level control.Of course, if an Action requires anonymous access, it is also allowed, because at the control level, the Action priority is higher than the Controller.
// Controller-level permission control [RequestAuthorize (User = "Zhang San")] public class Home2Controller: Controller {// log on to the User to access public ActionResult Index () {return View ();} // allow anonymous access to [AllowAnonymous] public ActionResult Index2 () {return View ();}}
3. Area-level control.Sometimes we make some modules into partitions. Of course, we can mark them in the Area Controller and Action.
We can see from the above that we need to mark permissions in various places. It is not a good practice to hard write Roles and Users in the program. I hope it is simpler to describe it in the configuration file. For example, the following Configuration:
<? Xml version = "1.0" encoding = "UTF-8"?> <! -- 1. Here you can transfer the permission control to the configuration file, so that you do not need to write roles and users in the program. 2. If the program also writes, it will overwrite the configuration file. 3. Priority of action level> controller level> Area level --> <root> <! -- Area level --> <area name = "Admin"> <roles> Admin </roles> </area> <! -- Controller level --> <controller name = "Home2"> <user> Zhang San </user> </controller> <! -- Action level --> <controller name = "Home1"> <action name = "Inde3"> <users> Zhang San </users> </action> <action name = "Index4"> <roles> Admin </roles> </action> </controller> </root>
It is written in the configuration file to facilitate management. If it is also written in the program, it will overwrite the configuration file. OK. Next, go to the topic.
Ii. Main Interfaces
First look at the two main interfaces used.
IPrincipal defines the basic functions of user objects. The interface definition is as follows:
Public interface IPrincipal {// identifies the IIdentity Identity {get;} // determines whether the current role belongs to the specified role bool IsInRole (string role );}
It has two main members. IsInRole is used to determine whether the current object belongs to a specified role. IIdentity defines the information of the Identity object. The User attribute of HttpContext is of the IPrincipal type.
IIdentity defines the basic functions of an object. The interface is defined as follows:
Public interface IIdentity {// Identity Authentication Type string AuthenticationType {get;} // whether the authentication has passed bool IsAuthenticated {get;} // user Name string Name {get ;}}
IIdentity contains some user information, but sometimes we need to store more information, such as the user ID and user role, which will be encrypted and stored in cookies, the status can be saved after decoding and deserialization when the verification is passed. For example, define a 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 implements the IUserData interface, which defines two methods: IsInRole and IsInUser, which are used to determine whether the current user role and user name meet the requirements. This interface is defined as follows:
public interface IUserData { bool IsInRole(string role); bool IsInUser(string user); }
Next, define a Principal implementation IPrincipal interface, as shown below:
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 contains IUserData instead of the specific UserData, so it is easy to replace a UserData without affecting other code. The IsInRole and IsInUser of Principal indirectly call the method of the same name as IUserData.
3. write and read cookies
Next, you need to create UserData, serialize it, and use FormsAuthentication to encrypt it and write it into the cookie. When the request arrives, you need to try to decrypt the cookie and deserialize it. As follows:
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); // the information stored in the cookie string userJson = JsonConvert. serializeObject (userData); // create user ticket double t IckekDays = rememberDays = 0? 7: rememberDays; var ticket = new FormsAuthenticationTicket (2, userName, DateTime. now, DateTime. now. addDays (tickekDays), false, userJson); // FormsAuthentication provides web forms authentication service // encrypted string encryptValue = FormsAuthentication. encrypt (ticket); // create 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 <TUserData> (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;} // decrypt the cookie Value FormsAuthenticationTicket ticket = FormsAuthentication. decrypt (cookie. value); if (ticket = null | string. isNullOrEmpty (ticket. userData) {return null;} IUserData userData = JsonConvert. deserializeObject <TUserData> (ticket. userData); return new Principal (ticket, userData );}}
When logging on, we can handle it like this:
Public ActionResult Login (string userName, string password) {// verify the user name and password and other logic... userData userData = new UserData () {UserName = userName, UserID = userID, UserRole = "Admin"}; HttpFormsAuthentication. setAuthenticationCookie (userName, userData, 7); // verification passed ...}
After successful login, the information will be written into the cookie. You can observe the request through the browser, there will be a Cookie named "Form" (you also need to simply configure the configuration file ), its value is an encrypted string, and subsequent requests are verified based on this cookie request. The specific method is to call the above TryParsePrincipal in the AuthenticateRequest verification event of HttpApplication, such:
protected void Application_AuthenticateRequest(object sender, EventArgs e) { HttpContext.Current.User = HttpFormsAuthentication.TryParsePrincipal<UserData>(HttpContext.Current); }
If the verification fails, HttpContext. Current. User is null, indicating that the Current User is not identified. However, no permission can be processed here, because as mentioned above, some pages allow anonymous access.
Iii. AuthorizeAttribute
This is a Filter that is executed before the Action is executed. It implements the IActionFilter interface. For more information about Filter, see my previous article. We define a RequestAuthorizeAttribute that inherits AuthorizeAttribute and overwrites its OnAuthorization method. If a Controller or Action marks this feature, the method will be executed before the Action is executed, check whether you have logged on and have permissions. If not, handle the issue accordingly. The Code is as follows:
[AttributeUsage (AttributeTargets. class | AttributeTargets. method)] public class RequestAuthorizeAttribute: AuthorizeAttribute {// verify public override void OnAuthorization (AuthorizationContext context) {EnsureHelper. ensureNotNull (context, "httpContent"); // whether anonymous access to if (context. actionDescriptor. isDefined (typeof (AllowAnonymousAttribute), false) {return;} // login verification Principal principal = context. httpCont Ext. User as Principal; if (principal = null) {SetUnAuthorizedResult (context); HandleUnauthorizedRequest (context); return;} // permission verification if (! Principal. IsInRole (base. Roles) |! Principal. IsInUser (base. Users) {SetUnAuthorizedResult (context); HandleUnauthorizedRequest (context); return ;}// verify the configuration file if (! ValidateAuthorizeConfig (principal, context) {SetUnAuthorizedResult (context); encrypt (context); return ;}// private void SetUnAuthorizedResult (AuthorizationContext context) {HttpRequestBase request = context. httpContext. request; if (request. isAjaxRequest () {// process ajax request string result = JsonConvert. serializeObject (JsonModel. error (403); context. result = new ContentResult () {Content = result };} else {// jump to the logon page 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 );}}
Note: The code here is taken from my project and abbreviated as some code. Some are helper classes and the code is not posted, but it should not affect reading.
1. If IPrincipal obtained in the AuthenticateRequest event of HttpApplication is null, verification fails.
2. If the verification succeeds, the program will verify the Roles and User attributes of AuthorizeAttribute.
3. If the verification succeeds, the program verifies the corresponding Roles and Users attributes in the configuration file.
To verify the configuration file, follow these steps:
Private bool ValidateAuthorizeConfig (Principal principal, AuthorizationContext context) {// action may be overloaded. When reloading, you should mark ActionName to distinguish ActionNameAttribute actionNameAttr = context. actionDescriptor. getCustomAttributes (typeof (ActionNameAttribute), false ). ofType <ActionNameAttribute> (). 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 ;}}
As you can see, it will be verified by an AuthorizationConfig Class Based on the name of the current requested area, controller, and action. The definition of this class is as follows:
Public class AuthorizationConfig {public string Roles {get; set;} public string Users {get; set;} private static XDocument _ doc; // configuration file path: private static string _ path = "~ /Identity/Authorization. xml "; // load the configuration file static AuthorizationConfig () {string absPath = HttpContext for the first time. current. server. mapPath (_ path); if (File. exists (absPath) {_ doc = XDocument. load (absPath) ;}}// parse the configuration file to obtain information including Roles and Users: public static AuthorizationConfig ParseAuthorizationConfig (string areaName, string controllerName, string actionName) {EnsureHelper. ensureNotNullOrEmpty (controllerName, "cont RollerName "); 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 (); // if no area node or controller node exists, null if (areaElement = null & controllerElement = null) {return null;} is returned ;} // obtain the marked area if (controllerElement = null) {rootElement = areaElement. element ("roles"); usersElement = areaElement. element ("users");} else {XElement CtionElement = controllerElement. Elements ("action"). Where (e => CompareName (e, actionName). FirstOrDefault (); if (actionElement! = Null) {// obtain the rolesElement = actionElement that marks the action. element ("roles"); usersElement = actionElement. element ("users");} else {// obtain the rolesElement = controllerElement that marks the controller. 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 );}}
The code here is long, but the main logic is to parse the configuration information at the beginning of the article.
Briefly summarize the steps for implementing the Program:
1. After checking that the user name and password are correct, call SetAuthenticationCookie to write some status information into the cookie.
2. In the Authentication event of HttpApplication, call TryParsePrincipal to obtain the status information.
3. Mark the RequestAuthorizeAttribute attribute in the Action (or Controller) to be verified, and set Roles and Users; Roles and Users can also be configured in the configuration file.
4. perform authentication and Permission Logic processing in the OnAuthorization method of RequestAuthorizeAttribute.
Iv. Summary
The above is the core implementation process of the entire logon authentication. You only need to simply configure it. However, the entire process from user registration to user management in a project is complex and involves front-end verification and encryption/decryption. For security issues, FormsAuthentication is relatively secure because it is encrypted Based on the server's MachineKey and other information. Of course, it is still possible to say that the request is intercepted maliciously and then forged for login. This is a problem to be considered later, for example, using secure http protocol https.