Preface The so-called authentication is ultimately a security issue. There are many ways to implement security in Web API, such as the [accepted] method to handle IIS-based security (via WindowsIdentity mentioned in the previous section that relies on HttpContext and IIS authentication) or by using the message processing mechanism in Web API. However, if we want the application to run outside of IIS, the Windows Idenitity method seems unlikely. At the same time, Web API itself does not provide a direct way to handle authentication. We have to customize it to implement the authentication function. This is also the method we recommend, do it yourself and have enough food and clothing. Tips: The following implementation methods are all based on basic authentication. If you are not familiar with Basic authentication in the HTTP protocol, please refer to this article [Garden Friend Seabird - Introduction to Basic Authentication and Diges Summary Authentication] first. No matter which method is used, we need to use credential-based user authentication at the business layer for our applications. Because it is a requirement of the client, the client needs to clarify the basic authentication. Basic authentication is very simple and supports any Web client, but the disadvantage of basic authentication is that it is not safe. By using SSL, encryption can ensure security to a certain extent. If it is for general applications, it can be said to be safe if it is encoded without encryption through basic authentication. Let's take a look at the picture given in the previous section
From the rough information in the above picture, we can see that the request to the Action method must go through the Web API message processing pipeline, and before the request to the target element, it must go through the HttpMessageHandler and the authentication filter, so we can customize the authentication through these two. Let's take a look at them one by one. Implementing authentication based on Web API's authentication filter (AuthorizationFilterAttribute) ***step We customize a class for authentication identity (user name and password), so this class must also inherit from GenericIdentity. Since it is based on basic verification, the type is of course Basic. - public class BasicAuthenticationIdentity : GenericIdentity
- {
- public string Password { get; set; }
- public BasicAuthenticationIdentity(string name, string password)
- : base(name, "Basic" )
- {
- this .Password = password;
- }
- }
Step 2
We need to customize an authentication filter feature and inherit AuthorizationFilterAttribute, which will become as follows: - public class BasicAuthenticationFilter : AuthorizationFilterAttribute
- {
- public override void OnAuthorization(HttpActionContext actionContext)
- {}
- }
So what should we write in this rewritten method? Let's analyze it slowly! Please read on. Parsing request header First of all, for the request sent by the client, we must obtain the request header, and then parse the Authorization in the request header. If its parameter is empty at this time, we will return to the client and initiate a query. - string authParameter = null ;
-
- var authValue = actionContext.Request.Headers.Authorization;
- if (authValue != null && authValue.Scheme == "Basic" )
- authParameter = authValue.Parameter;
-
- if (string.IsNullOrEmpty(authParameter))
-
- return null ;
Secondly, if the authentication parameter is not empty at this time, it starts to encode it and returns a BasicAuthenticationIdentity object. If the object is empty at this time, it is also returned to the client and a challenge is initiated.
- uthParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter));
-
- var authToken = authParameter.Split( ':' );
- if (authToken.Length < 2 )
- return null ;
-
- return new BasicAuthenticationIdentity(authToken[ 0 ], authToken[ 1 ]);
***, we encapsulate the above two into a ParseHeader method for calling
- public virtual BasicAuthenticationIdentity ParseHeader(HttpActionContext actionContext)
- {
- string authParameter = null ;
-
- var authValue = actionContext.Request.Headers.Authorization;
- if (authValue != null && authValue.Scheme == "Basic" )
- authParameter = authValue.Parameter;
-
- if (string.IsNullOrEmpty(authParameter))
-
- return null ;
-
- authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter));
-
- var authToken = authParameter.Split( ':' );
- if (authToken.Length < 2 )
- return null ;
-
- return new BasicAuthenticationIdentity(authToken[ 0 ], authToken[ 1 ]);
- }
Next, we will initiate an authentication challenge if the authentication fails. We will encapsulate it into a method Challenge - void Challenge(HttpActionContext actionContext)
- {
- var host = actionContext.Request.RequestUri.DnsSafeHost;
- actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
- actionContext.Response.Headers.Add( "WWW-Authenticate" , string.Format( "Basic realm=\"{0}\"" , host));
-
- }
Define a method to verify the username and password, and modify it as a virtual method to avoid adding other user data later. - public virtual bool OnAuthorize(string userName, string userPassword, HttpActionContext actionContext)
- {
- if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(userPassword))
-
- return false ;
- else
- return true ;
-
- }
After successful authentication, set the authentication identity to the Principal attribute in the current thread - ar principal = new GenericPrincipal(identity, null );
-
- Thread.CurrentPrincipal = principal;
-
-
-
-
Step 3
Everything is ready, now you can make the corresponding call in the rewritten method, as follows: - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false )]
- public class BasicAuthenticationFilter : AuthorizationFilterAttribute
- {
- public override void OnAuthorization(HttpActionContext actionContext)
- {
- var userIdentity = ParseHeader(actionContext);
- if (userIdentity == null )
- {
- Challenge(actionContext);
- return ;
- }
-
- if (!OnAuthorize(userIdentity.Name, userIdentity.Password, actionContext))
- {
- Challenge(actionContext);
- return ;
- }
-
- var principal = new GenericPrincipal(userIdentity, null );
-
- Thread.CurrentPrincipal = principal;
-
- base.OnAuthorization(actionContext);
- }
#p# Step 4
Customize CustomBasicAuthenticationFilter and inherit from BasicAuthenticationFilter, overriding its virtual methods. - public class CustomBasicAuthenticationFilter : BasicAuthenticationFilter
- {
- public override bool OnAuthorize(string userName, string userPassword, HttpActionContext actionContext)
- {
- if (userName == "xpy0928" && userPassword == "cnblogs" )
-
- return true ;
- else
- return false ;
-
- }
- }
***step
Registering custom authentication attributes and calling them - config.Filters.Add( new CustomBasicAuthenticationFilter());
-
- [CustomBasicAuthenticationFilter]
- public class ProductController : ApiController
- {....}
At this point, the authentication method has been fully implemented. Next, we will use [Sogou Browser] to verify our results. Seeing the following picture of verifying its username and password, we know we are halfway there We click Cancel and observe whether 401 is returned and the query header WWW-Authenticate is added, as we expected. We enter the correct username and password and try again. The authentication is successful, as follows: Implementing authentication based on Web API message processing pipeline (HttpMessageHandler) We know that HttpMessageHandler is an important role in the message processing pipeline in the request-response of Web API, but the one that really implements the pipeline connection is DelegatingHandler. If you don't understand the Web API message pipeline, please refer to the previous series of articles, so we can customize the pipeline to intercept by inheriting DelegatingHandler. Let's implement authentication based on this pipeline step by step. ***step It is the same as the first method and will not be described again. Step 2 This step is of course to customize the pipeline for processing and inherit DelegatingHandler, overload the SendAsync method in this class, and respond by obtaining its request and processing it. If you don’t understand the specific implementation of this class, please refer to the previous series of articles. We also need to parse the request header according to the request. We still need to parse the header method, but we need to modify it slightly. - public virtual BasicAuthenticationIdentity ParseHeader(HttpRequestMessage requestMessage)
- {
- string authParameter = null ;
-
- var authValue = requestMessage.Headers.Authorization;
- if (authValue != null && authValue.Scheme == "Basic" )
- authParameter = authValue.Parameter;
-
- if (string.IsNullOrEmpty(authParameter))
-
- return null ;
-
- authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter));
-
- var authToken = authParameter.Split( ':' );
- if (authToken.Length < 2 )
- return null ;
-
- return new BasicAuthenticationIdentity(authToken[ 0 ], authToken[ 1 ]);
- }
The query must also be modified accordingly, because it no longer relies on the Action request context, but on the request (HttpRequestMessage) and response (HttpResponseMessage) - void Challenge(HttpRequestMessage request,HttpResponseMessage response)
- {
- var host = request.RequestUri.DnsSafeHost;
-
- response.Headers.Add(authenticationHeader, string.Format( "Basic realm=\"{0}\"" , host));
-
- }
The code that finally inherits from DelegatingHandler is as follows - public class BasicAuthenticationHandler : DelegatingHandler
- {
- private const string authenticationHeader = "WWW-Authenticate" ;
- protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- var crendentials = ParseHeader(request);
-
- if (crendentials != null )
- {
- var identity = new BasicAuthenticationIdentity(crendentials.Name, crendentials.Password);
-
- var principal = new GenericPrincipal(identity, null );
-
- Thread.CurrentPrincipal = principal;
-
-
-
-
- }
-
- return base.SendAsync(request, cancellationToken).ContinueWith(task => {
- var response = task.Result;
- if (crendentials == null && response.StatusCode == HttpStatusCode.Unauthorized)
- {
- Challenge(request, response);
- }
-
- return response;
- });
-
- }
-
- void Challenge(HttpRequestMessage request,HttpResponseMessage response)
- {
- var host = request.RequestUri.DnsSafeHost;
-
- response.Headers.Add(authenticationHeader, string.Format( "Basic realm=\"{0}\"" , host));
-
- }
-
- public virtual BasicAuthenticationIdentity ParseHeader(HttpRequestMessage requestMessage)
- {
- string authParameter = null ;
-
- var authValue = requestMessage.Headers.Authorization;
- if (authValue != null && authValue.Scheme == "Basic" )
- authParameter = authValue.Parameter;
-
- if (string.IsNullOrEmpty(authParameter))
-
- return null ;
-
- authParameter = Encoding.Default.GetString(Convert.FromBase64String(authParameter));
-
- var authToken = authParameter.Split( ':' );
- if (authToken.Length < 2 )
- return null ;
-
- return new BasicAuthenticationIdentity(authToken[ 0 ], authToken[ 1 ]);
- }
- }
#p# Step 3 The above-mentioned custom BasicAuthenticationFilter must inherit AuthorizeAttribute at this time. This feature is also inherited from the above-mentioned AuthorizationFilterAttribute. We need to use the IsAuthorized method in AuthorizeAttribute to verify whether the Principal in the current thread has been authorized.
- public class BasicAuthenticationFilter : AuthorizeAttribute
- {
- protected override bool IsAuthorized(HttpActionContext actionContext)
- {
-
- var identity = Thread.CurrentPrincipal.Identity;
- if (identity != null && HttpContext.Current != null )
- identity = HttpContext.Current.User.Identity;
-
- if (identity != null && identity.IsAuthenticated)
- {
-
- var basicAuthIdentity = identity as BasicAuthenticationIdentity;
-
-
- if (basicAuthIdentity.Name == "xpy0928" && basicAuthIdentity.Password == "cnblogs" )
- {
- return true ;
- }
- }
-
- return false ;
-
- }
- }
From the return value of the IsAuthorized method, if it is false, a 401 status code is returned, which triggers a challenge in BasicAuthenticationHandler, and this method mainly contains the business logic code for authenticating users. At the same time, we also said that the filter feature of our first method is AuthorizationFilterAttribute (if we have more logic to use this feature, it is a good choice), and here it is AuthorizeAttribute (for authenticating users and returning bool values, using this filter feature is a good choice). Step 4 Registering custom pipe and authentication filter traits - config.MessageHandlers.Add( new BasicAuthenticationHandler());
- config.Filters.Add( new BasicAuthenticationFilter());
***step [BasicAuthenticationFilter] public class ProductController : ApiController {.....} Next, we will use [360 Speed Browser] to check the results. Click the button to directly request the controller Next, cancel and return 401 This is the end. Summary <br /> Is it a question of whether to use the AuthorizationFilterAttribute or HttpMessageHandler to implement authentication? By comparing the implementation of the two operations, it is obvious that there are great differences in the implementation methods. I personally think that using AuthorizationFilterAttribute to implement authentication is simpler and more compact, because every implementation is in every place. In most scenarios of implementing custom login, it is more efficient to use this for such compact business logic with filters. The advantage of using HttpMessageHandler is that it is globally applied and is part of the Web API message processing pipeline. If different authentications are required for different parts, then using HttpMessageHandler is better, but at this time you need to customize a filter, especially when MessageHandler needs a filter for one authentication. So in summary, according to different application scenarios, we should choose the corresponding method to implement authentication. |