1
头图

The authentication and authorization of ASP.NET Core is nothing new. Microsoft's official documentation has a very detailed and in-depth introduction to how to implement authentication and authorization in ASP.NET Core. But sometimes in the development process, we often feel that we have no way to start, or because the design and planning of the authentication and authorization mechanism is not carried out at the beginning, some confusion occurs in the later stage. Here I will try to combine a practical example to introduce how to implement its own authentication and authorization mechanism in ASP.NET Core from 0 to 1.

When we use the ASP.NET Core Web API project template that comes with Visual Studio to create a new project, Visual Studio will ask us if we need to enable the authentication mechanism. If you choose to enable it, then Visual Studio will add it when the project is created. Some auxiliary dependencies and some auxiliary classes, such as adding dependencies on Entity Framework and ASP.NET Identity, to help you implement authentication based on Entity Framework and ASP.NET Identity. If you haven't understood some basics of ASP.NET Core's authentication and authorization, then when you open this project automatically created by Visual Studio, you will definitely be at a loss. Are all classes or methods really required in the created project?

Therefore, in order to make this article easier to understand, we still choose not to enable authentication and directly create the simplest ASP.NET Core Web API application for subsequent introduction.
Create a new ASP.NET Core Web API application. Here I use JetBrains Rider to create a new project under Linux. You can also use standard Visual Studio or VSCode to create a project. Once created, run the program, then use your browser to access the /WeatherForecast endpoint to get an array of randomly generated weather and temperature data. You can also access this API using the following curl command:

1curl -X GET "http://localhost:5000/WeatherForecast" -H "accept: text/plain"

Now let's set a breakpoint on the Get method of WeatherForecastController, restart the program, and still send the above request to hit the breakpoint. At this time, we are more concerned about the state of the User object, open the monitor to view the properties of the User object, and find its IsAuthenticated property is false:

In many cases, we may not need to obtain the information of the authenticated user in the Controller method, so we never pay attention to whether the User object is really in the authenticated state. But when the API needs to perform some special logic based on some information of the user, we need to make the user's authentication information in a reasonable state here: it has been authenticated and contains the information required by the API. This is the authentication and authorization of ASP.NET Core discussed in this article.

Certification

An application's identification of a user consists of two parts: authentication and authorization. Authentication refers to whether the current user is a legal user of the system, while authorization refers to specifying what access rights the legal user has to which system resources. Let's first look at how to implement authentication.

Here, we only talk about the authentication implemented by the ASP.NET Core application itself, and do not discuss the situation where a unified Identity Provider completes the authentication (such as single sign-on), so that we can understand the ASP.NET Core itself more clearly. Authentication mechanism. Next, we try to implement Basic authentication on the ASP.NET Core application.

Basic authentication requires the user's authentication information to be attached to the Authorization header of the HTTP request. The authentication information is a string generated by BASE64 encoding the username and password. For example, when you use Basic authentication and use When using daxnet and password as the username and password to access the WeatherForecast API, you may need to use the following command line to invoke WeatherForecast:

1curl -X GET "http://localhost:5000/WeatherForecast" -H "accept: text/plain" -H "Authorization: Basic ZGF4bmV0OnBhc3N3b3Jk"

In ASP.NET Core Web API, when the application receives the above request, it will read the Authorization information from the Request header, then BASE64 decode to get the username and password, and then access the database to confirm the provided username and password Is it legal to judge whether the authentication is successful or not. This part of the work can usually be implemented using the ASP.NET Core Identity framework, but here, in order to have a clearer understanding of the entire process of authentication, we choose to implement it ourselves.

First, we define a User object and design several users in advance to simulate the database storing user information. The code of this User object is as follows:

public class User

{

public string UserName { get; set; }

public string Password { get; set; }

public IEnumerable<string> Roles { get; set; }

public int Age { get; set; }



public override string ToString() => UserName;



public static readonly User[] AllUsers = {

    new User

    {

        UserName = "daxnet", Password = "password", Age = 16, Roles = new[] { "admin", "super_admin" }

    },

    new User

    {

        UserName = "admin", Password = "admin", Age = 29, Roles = new[] { "admin" }

    }

};

}

The User object includes the username, password, and its role name, but we don't need to care about role information for now. The User object also contains a static field, which we use as a database of user information.

Next, add an AuthenticationHandler to the application to obtain the user information in the Request Header and verify the user information. The code is as follows:

public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationSchemeOptions>

{

public BasicAuthenticationHandler(

    IOptionsMonitor<BasicAuthenticationSchemeOptions> options,

    ILoggerFactory logger,

    UrlEncoder encoder,

    ISystemClock clock) : base(options, logger, encoder, clock)

{

}

protected override Task<AuthenticateResult> HandleAuthenticateAsync()

{

    if (!Request.Headers.ContainsKey("Authorization"))

    {

        return Task.FromResult(AuthenticateResult.Fail("Authorization header is not specified."));

    }

    var authHeader = Request.Headers["Authorization"].ToString();

    if (!authHeader.StartsWith("Basic "))

    {

        return Task.FromResult(

            AuthenticateResult.Fail("Authorization header value is not in a correct format"));

    }


    var base64EncodedValue = authHeader["Basic ".Length..];

    var userNamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(base64EncodedValue));

    var userName = userNamePassword.Split(':')[0];

    var password = userNamePassword.Split(':')[1];

    var user = User.AllUsers.FirstOrDefault(u => u.UserName == userName && u.Password == password);

    if (user == null)

    {

        return Task.FromResult(AuthenticateResult.Fail("Invalid username or password."));

    }


    var claims = new[]

    {

        new Claim(ClaimTypes.NameIdentifier, user.UserName),

        new Claim(ClaimTypes.Role, string.Join(',', user.Roles)),

        new Claim(ClaimTypes.UserData, user.Age.ToString())

    };

    var claimsPrincipal =

        new ClaimsPrincipal(new ClaimsIdentity(

            claims,

            "Basic",

            ClaimTypes.NameIdentifier, ClaimTypes.Role));

    var ticket = new AuthenticationTicket(claimsPrincipal, new AuthenticationProperties

    {

        IsPersistent = false

    }, "Basic");



    return Task.FromResult(AuthenticateResult.Success(ticket));

}

}

In the HandleAuthenticateAsync code above, the validity of the Request Header is first checked, such as whether it contains the Authorization Header and whether the value of the Authorization Header is legal. Then, the value of the Authorization Header is parsed, and the username and password are obtained after Base64 decoding. , and match the records in the user information database to find the matching user. Next, create a ClaimsPrincipal based on the user object found, and create an AuthenticationTicket based on the ClaimsPrincipal and return it.

There are a few things worth noting in this code:

  1. BasicAuthenticationSchemeOptions itself is just a POCO class that inherits from AuthenticationSchemeOptions. The AuthenticationSchemeOptions class is typically used to provide some input parameters to the AuthenticationHandler. For example, in a custom user authentication logic, it may be necessary to read the key information for string decryption through environment variables. At this time, you can add a Passphrase attribute to this custom AuthenticationSchemeOptions, and then in Startup.cs , pass in the value of Passphrase read from the environment variable via the service.AddScheme call.
  2. In addition to adding the username as an Identity Claim to the ClaimsPrincipal, we also concatenate the user's role (Role) with a comma and add it to the ClaimsPrincipal as a Role Claim. We don't need to involve role-related content for the time being, but first This part of the code is put here for later use. In addition, we put the user's age (Age) in the UserData claim. In practice, there should be the user's date of birth on the user object, which is more reasonable, and then this date of birth should be placed in the DateOfBirth claim, here for simplicity. For the sake of it, put it in UserData first.
  3. In the constructor of ClaimsPrincipal, you can specify which claim type can be used as the user's name, and which claim type can be used as the user's role. For example, in the above code, we choose the NameIdentifier type as the user name, and the Role type as the user role, so in the following Controller code, the string value pointed to by the Claim such as NameIdentifier will be regarded as the user name and is bound to the Identity.Name property.
    Looking back at the BasicAuthenticationSchemeOptions class, its implementation is very simple:

public void ConfigureServices(IServiceCollection services)

{

services.AddControllers();

services.AddSwaggerGen(c =>

{

    c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIAuthSample", Version = "v1" });

});

services.AddAuthentication("Basic")

    .AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>(

        "Basic", options => { });

}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

if (env.IsDevelopment())

{

    app.UseDeveloperExceptionPage();

    app.UseSwagger();

    app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPIAuthSample v1"));

}



app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthentication();

app.UseEndpoints(endpoints => { endpoints.MapControllers(); });

}

Now, run the application, set a breakpoint on the WeatherForecastController's Get method, and execute the curl command above. When the breakpoint is hit, observe the this.User object and find that the IsAuthenticated property becomes true and the Name property is also set to username:

Most authentication frameworks will provide some helper methods to help developers register the AuthenticationHandler into the application. For example, the framework based on JWT holder authentication will provide an AddJwtBearer method to add the JWT authentication mechanism to the application. , which essentially calls the AddScheme method to complete the AuthenticationHandler registration. Here, we can also customize an extension method of AddBasicAuthentication:

public static class Extensions

{

public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder)

    => builder.AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>(

        "Basic",

        options => { });

}

Then modify the Starup.cs file and change the ConfigureServices method to the following:

public void ConfigureServices(IServiceCollection services)

{

services.AddControllers();

services.AddSwaggerGen(c =>

{

    c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIAuthSample", Version = "v1" });

});

services.AddAuthentication("Basic").AddBasicAuthentication();

}

The advantage of this is that you can provide developers with more targeted programming interfaces for configuring the authentication mechanism, which is a good design for the development of an authentication module/framework.
In the curl command, if we do not specify the Authorization Header, or the value of the Authorization Header is incorrect, the WeatherForecast API can still be called, but the IsAuthenticated property is false, and the user information cannot be obtained from the this.User object. In fact, preventing unauthenticated users from accessing the API is not a matter of authentication. It is also reasonable for the API to be accessed by unauthenticated (or not logged in) users. Therefore, to implement access restrictions for unauthenticated users, it is necessary to further implement ASP.NET Another security control component of Core Web API: authorization.

License

Compared with authentication, the logic of authorization is more complicated: authentication is more of a technical matter, while authorization is more business-related. There are at most a few or a dozen common authentication mechanisms on the market, and the authorization methods are diverse, because different apps and different businesses have different authorization requirements for app resource access. One of the most common authorization methods is RBAC (Role Based Access Control, role-based access control), which defines what roles have what access rights to what resources. In RBAC, different users are given different roles, and for the convenience of management, user groups are designed for users with the same resource access rights, and the access control is set on the user group, and further, groups and groups There can also be a parent-child relationship.

Please note the bolded words above, each bolded word is an authorization-related concept. In ASP.NET Core, each Authorization Requirement corresponds to a class that implements IAuthorizationRequirement, and the AuthorizationHandler is responsible for processing the corresponding authorization logic. Simply understand, authorization requirements indicate what kind of users can meet the authorization requirements, or what kind of users can access resources through authorization. An authorization requirement often only defines and handles a specific authorization logic. ASP.NET Core allows multiple authorization requirements to be combined into an authorization policy (Authorization Policy) and then applied to the accessed resources. This design can ensure the authorization requirements. Both design and implementation are small-grained to separate the concerns of different authorization requirements. At the level of authorization strategy, the purpose of flexible realization of authorization business is achieved by combining different authorization requirements.

For example: Suppose some APIs in the app only allow administrators to access, and some APIs only allow users who are 18 years old to access, and some other APIs require users to be both super administrators and 18 years old. Then you can define two kinds of Authorization Requirement: GreaterThan18Requirement and SuperAdminRequirement, and then design three kinds of Policies: the first one contains only GreaterThan18Requirement, the second only contains SuperAdminRequirement, the third one contains both requirements, and finally these different policies It can be applied to different APIs.

Back to our case code, first define two requirements: SuperAdminRequirement and GreaterThan18Requirement:

public class SuperAdminRequirement : IAuthorizationRequirement

{

}

public class GreaterThan18Requirement : IAuthorizationRequirement

{

}

Then implement SuperAdminAuthorizationHandle and GreaterThan18AuthorizationHandler respectively:

The implementation logic is also very clear: in the GreaterThan18AuthorizationHandler, the age information is obtained through the UserData claim, if the age is greater than 18, the authorization is successful; in the SuperAdminAuthorizationHandler, the role of the user is obtained through the Role claim, if the role contains super_admin, the authorization is successful. Next, you need to add these two requirements to the required Policy, and then register it with the application:

public void ConfigureServices(IServiceCollection services)

{

services.AddControllers();

services.AddSwaggerGen(c =>

{

    c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIAuthSample", Version = "v1" });

});

services.AddAuthentication("Basic").AddBasicAuthentication();

services.AddAuthorization(options =>

{

    options.AddPolicy("AgeMustBeGreaterThan18", builder =>

    {

        builder.Requirements.Add(new GreaterThan18Requirement());

    });

    options.AddPolicy("UserMustBeSuperAdmin", builder =>

    {

        builder.Requirements.Add(new SuperAdminRequirement());

    });

});

services.AddSingleton<IAuthorizationHandler, GreaterThan18AuthorizationHandler>();

services.AddSingleton<IAuthorizationHandler, SuperAdminAuthorizationHandler>();

}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

if (env.IsDevelopment())

{

    app.UseDeveloperExceptionPage();

    app.UseSwagger();

    app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPIAuthSample v1"));

}



app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthentication();

app.UseAuthorization();

app.UseEndpoints(endpoints => { endpoints.MapControllers(); });

}

In the ConfigureServices method, we define two policies: AgeMustBeGreaterThan18 and UserMustBeSuperAdmin. Finally, on the API Controller or Action, apply AuthorizeAttribute to specify the required Policy. For example, if you want the WeatherForecase API to be accessible only to users older than 18, you can do this:

[HttpGet]

[Authorize(Policy = "AgeMustBeGreaterThan18")]

public IEnumerable<WeatherForecast> Get()

{

var rng = new Random();

return Enumerable.Range(1, 5).Select(index => new WeatherForecast

    {

        Date = DateTime.Now.AddDays(index),

        TemperatureC = rng.Next(-20, 55),

        Summary = Summaries[rng.Next(Summaries.Length)]

    })

    .ToArray();

}

Run the program, assuming there are three users: daxnet, admin, and foo, and their BASE64 authentication information is:

  • daxnet:ZGF4bmV0OnBhc3N3b3Jk
  • admin:YWRtaW46YWRtaW4=
  • foo:Zm9vOmJhcg==

Then, the same curl command, when specifying different user authentication information, will get different results:

The age of the daxnet user is less than 18 years old, so the access to the API is unsuccessful, and the server returns 403:

The admin user meets the condition of being older than 18 years old, so it can successfully access the API:

The foo user itself is not registered in the system, so the server returns 401, indicating that the user has not been authenticated successfully:

Summary

This article briefly introduces the basic implementation methods of user authentication and authorization in ASP.NET Core, to help beginners or developers who need to use these functions quickly understand this part. The authentication and authorization system of ASP.NET Core is very flexible and can integrate various authentication mechanisms and authorization methods. This article cannot provide a comprehensive and detailed introduction. However, no matter which framework is implemented, its implementation basis is the content introduced in this article. If you plan to develop a set of authentication and authorization frameworks yourself, you can also refer to this article.

Click Get ASP.NET Core Super Full Information


微软技术栈
423 声望996 粉丝

微软技术生态官方平台。予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。


引用和评论

0 条评论