Detailed explanation of ASP. NET MVC Form form verification

  • 2021-07-13 05:00:20
  • OfStack

1. Preface

On the form validation, there have been a lot of articles, I believe Web developers have basically written, recently in a personal project just used, here to share with you 1. Originally, I wanted to start writing from user registration, but I found that there are many things, involving interface, front-end authentication, front-end encryption, background decryption, user password Hash, authority authentication, etc. The article may be very long to write, so this article mainly introduces login authentication and authority control. Interested friends welcome an exchange.

1 There are Windows verification and form verification, and web project uses form verification more. The principle is very simple. Simply put, the authentication token is stored on the client browser by using the browser's cookie. cookie will be sent to the server with the request every time, and the server will verify this token. Usually, users of a system will be divided into various roles: anonymous users, ordinary users and administrators; It can be subdivided again, for example, users can be ordinary users or Vip users, and administrators can be ordinary administrators or super administrators. In the project, some of our pages may only allow administrators to view, and some only allow logged-in users to view, which is role distinction (Roles); In some special cases, some pages may only be allowed to be viewed by people with the name "Zhang 3", which is called user discrimination (Users).

Let's first look at the final effect to be achieved:

1. This is control at the Action level.


public class Home1Controller : Controller
{
  // Anonymous access 
  public ActionResult Index()
  {
    return View();
  }
  // Login user access 
  [RequestAuthorize]
  public ActionResult Index2()
  {
    return View();
  }
  // Login user, Zhang 3 To access 
  [RequestAuthorize(Users=" Zhang 3")]
  public ActionResult Index3()
  {
    return View();
  }
  // Administrator access 
  [RequestAuthorize(Roles="Admin")]
  public ActionResult Index4()
  {
    return View();
  }
}

2. This is control at the Controller level. Of course, if an Action requires anonymous access, it is also allowed, because Action takes precedence over Controller at the control level.


//Controller Level of permission control 
[RequestAuthorize(User=" Zhang 3")]
public class Home2Controller : Controller
{
  // Login user access 
  public ActionResult Index()
  {
    return View();
  }
  // Allow anonymous access 
  [AllowAnonymous]
  public ActionResult Index2()
  {
    return View();
  }
}

3. Area level control. Sometimes we will partition some modules. Of course, we can also mark them in Controller and Action of Area.

As can be seen from the above, we need to mark permissions in various places. If Roles and Users are hard written in the program, it is not a good practice. I hope it can be simpler by one point, which is explained in the configuration file. For example, the following configuration:


<?xml version="1.0" encoding="utf-8" ?>
<!--
  1. Here, you can transfer permission control to the configuration file so that you don't have to write in the program roles And users It's over 
  2. If the program is also written, the configuration file will be overwritten. 
  3.action Priority of level  > controller Level  > Area Level   
-->
<root>
 <!--area Level -->
 <area name="Admin">
  <roles>Admin</roles>
 </area>
  
 <!--controller Level -->
 <controller name="Home2">
  <user> Zhang 3</user>
 </controller>
  
 <!--action Level -->
 <controller name="Home1">
  <action name="Inde3">
   <users> Zhang 3</users>
  </action>
  <action name="Index4">
   <roles>Admin</roles>
  </action>
 </controller>
</root>

Write in the configuration file, is to facilitate management, if the program is also written, will overwrite the configuration file. ok, let's get down to business.

2. Main interfaces

First, look at the two main interfaces used.

IPrincipal defines the basic functions of user objects, and the interfaces are defined as follows:


public interface IPrincipal
{
  // Identification object 
  IIdentity Identity { get; }
  // Determine 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 the specified role, and IIdentity defines the identification object information. The User attribute of HttpContext is of type IPrincipal.

IIdentity defines the basic functionality of identifying objects, and the interface is defined as follows:


public interface IIdentity
{  
  // Authentication type 
  string AuthenticationType { get; }
  // Whether the verification passes 
  bool IsAuthenticated { get; } 
  // User name 
  string Name { get; }
}

IIdentity contains some user information, but sometimes we need to store more information, such as user ID, user role, etc. These information will be encrypted and saved in cookie, which can be decoded and deserialized when verified, and the state can be saved. For example, define an 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 judge whether the current user role and user name meet the requirements, respectively. The interface is defined as follows:


public interface IUserData
{
  bool IsInRole(string role);
  bool IsInUser(string user);
}
 Next define 1 A Principal Realization IPrincipal Interface, as follows: 
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);
  }
}

The Principal contains the IUserData rather than the specific UserData, making it easy to replace one UserData without affecting the rest of the code. IsInRole and IsInUser of Principal indirectly call the method of the same name of IUserData.

3. Write cookie and read cookie

Next, what needs to be done is to create UserData after the user logs in successfully, serialize it, encrypt it with FormsAuthentication, and write it to cookie; When the request arrives, you need to try to decrypt and deserialize cookie. 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);
 
    // Save in cookie Information in 
    string userJson = JsonConvert.SerializeObject(userData);
 
    // Create a user ticket 
    double tickekDays = rememberDays == 0 ? 7 : rememberDays;
    var ticket = new FormsAuthenticationTicket(2, userName,
      DateTime.Now, DateTime.Now.AddDays(tickekDays), false, userJson);
 
    //FormsAuthentication Provide web forms Authentication service 
    // Encryption 
    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;
    }
    // Decryption 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);
  }
}

At login time, we can do something like this:


public ActionResult Login(string userName,string password)
{
  // Verify user name and password, and so on 1 Some logic ... 
 
  UserData userData = new UserData()
  {
    UserName = userName,
    UserID = userID,
    UserRole = "Admin"
  };
  HttpFormsAuthentication.SetAuthenticationCookie(userName, userData, 7);
   
  // Verify through ...
}

After successful login, the information will be written into cookie. You can observe the request through the browser, and there will be an Cookie named "Form" (the configuration file under 1 needs to be simply configured). Its value is an encrypted string, and subsequent requests will be verified according to this cookie request. This is done by calling the above TryParsePrincipal in the AuthenticateRequest validation event of HttpApplication, such as:


protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
  HttpContext.Current.User = HttpFormsAuthentication.TryParsePrincipal<UserData>(HttpContext.Current);
}

If the authentication fails here, HttpContext. Current. User is null, indicating that the current user is not identified. But you can't do anything about permissions here, because as mentioned above, some pages are allowed to be accessed anonymously.

3. AuthorizeAttribute

This is an Filter that executes before Action executes and implements the IActionFilter interface. As for Filter, you can see my previous article, which will not be introduced here. We define an RequestAuthorizeAttribute to inherit AuthorizeAttribute and override its OnAuthorization method. If an Controller or Action marks this feature, the method will be executed before Action executes. Here, we judge whether we have logged in and have permission, and if not, we will make corresponding treatment. The specific code is as follows:


//Controller Level of permission control 
[RequestAuthorize(User=" Zhang 3")]
public class Home2Controller : Controller
{
  // Login user access 
  public ActionResult Index()
  {
    return View();
  }
  // Allow anonymous access 
  [AllowAnonymous]
  public ActionResult Index2()
  {
    return View();
  }
}
0

Note: The code here is taken from the personal project, abbreviated part of the code, some are auxiliary classes, the code is not posted, but should not affect the reading.

1. If the IPrincipal we get in the AuthenticateRequest event of HttpApplication is null, then the validation fails.

2. If validation passes, the program validates the Roles and User properties of AuthorizeAttribute.

3. If validation passes, the program validates the corresponding Roles and Users properties in the configuration file.

The method of validating the configuration file is as follows:


//Controller Level of permission control 
[RequestAuthorize(User=" Zhang 3")]
public class Home2Controller : Controller
{
  // Login user access 
  public ActionResult Index()
  {
    return View();
  }
  // Allow anonymous access 
  [AllowAnonymous]
  public ActionResult Index2()
  {
    return View();
  }
}
1

As you can see, it is validated by an AuthorizationConfig class based on the current request's area, controller, and action names, defined as follows:


//Controller Level of permission control 
[RequestAuthorize(User=" Zhang 3")]
public class Home2Controller : Controller
{
  // Login user access 
  public ActionResult Index()
  {
    return View();
  }
  // Allow anonymous access 
  [AllowAnonymous]
  public ActionResult Index2()
  {
    return View();
  }
}
2

The code here is relatively long, but the main logic is to parse the configuration information at the beginning of the article.

Briefly summarize the steps of program implementation under 1:

1. After checking the user name and password correctly, call SetAuthenticationCookie to write some status information into cookie.

2. In the Authentication event of HttpApplication, call TryParsePrincipal for status information.

3. Mark the RequestAuthorizeAttribute feature on the Action (or Controller) to be verified, and set Roles and Users; Roles and Users can also be configured in configuration files.

4. Authentication and permission logic in the OnAuthorization method of RequestAuthorizeAttribute.

4. Summary

The above is the core implementation process of the whole login authentication, which can be realized only by simple configuration 1. However, the whole process from user registration to user management in the actual project is quite complex, and involves the front and back end verification, encryption and decryption issues. On security issues, FormsAuthentication in encryption, according to the server MachineKey and other information for encryption, so relatively safe. Of course, if the request is maliciously intercepted and then forged login is still possible, this is a problem to be considered later, such as using the secure http protocol https.

The above is the whole content of this paper, hoping to help everyone's study.


Related articles: