The Workflow Principle of JWT Authorization and Authentication in ASP.NET Core

2020年3月15日 3190点热度 0人点赞 0条评论
内容目录

1. Quick Implementation of Authorization Verification

What is JWT? Why use JWT? What are the components of JWT?

These can be easily found on Baidu, so I won't elaborate here.

In fact, it's sufficient to know that the JWT authentication mode uses a Token as the basis for authentication.

Let's look at the location for setting the Token in Postman.

file

So, how do we use C#'s HttpClient to access a JWT-authenticated Web API?

file

Next, let's create an ASP.NET Core project and try to add JWT authentication functionality.

1.1 Adding JWT Service Configuration

In the ConfigureServices method of Startup.cs, add a service:

            // Set the authentication method to Bearer Token
            // You can also add using Microsoft.AspNetCore.Authentication.JwtBearer;
            // Use JwtBearerDefaults.AuthenticationScheme instead of the string "Bearer"
            services.AddAuthentication("Bearer")
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdABCD1234abcdABCD1234")),    // The key for encrypting and decrypting the Token

                        // Should the issuer be validated?
                        ValidateIssuer = true,
                        // Issuer name
                        ValidIssuer = "server",  

                        // Should the audience be validated?
                        // Audience name
                        ValidateAudience = true,
                        ValidAudience = "client007",

                        // Should the token validity duration be validated?
                        ValidateLifetime = true,
                        // Valid duration of the token for each issuance
                        ClockSkew = TimeSpan.FromMinutes(120)
                    };
                });

Modify the middleware in Configure:

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthentication();		// Note here
            app.UseAuthorization();

It's that simple. With these settings, requests will require permission verification.

1.2 Issuing a Token

The issued Token will not be saved by ASP.NET Core.

Once ASP.NET Core has enabled Token authentication, you can place the code for generating the Token in different console applications. As long as the key, Issuer, and Audience remain consistent, the generated Token can log into this ASP.NET Core application.

That is to say, you can create console applications at will to generate Tokens, and the generated Tokens can successfully log into the ASP.NET Core program.

As for why, we'll discuss that later.

In Program.cs, add a method like this:

        static void ConsoleToke()
        {

            // Define user information
            var claims = new Claim[]
            {
                new Claim(ClaimTypes.Name, "痴者工良"),
                new Claim(JwtRegisteredClaimNames.Email, "66666666666@qq.com"),
            };

            // Consistent with the configuration in Startup
            SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdABCD1234abcdABCD1234"));

            JwtSecurityToken token = new JwtSecurityToken(
                issuer: "server",
                audience: "client007",
                claims: claims,
                notBefore: DateTime.Now,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
            );

            string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
            Console.WriteLine(jwtToken);
        }

Call this method in Main():

        public static void Main(string[] args)
        {
            ConsoleToke();
            CreateHostBuilder(args).Build().Run();
        }

1.3 Adding API Access

We will add an API.

The [Authorize] attribute is used to indicate that this Controller or Action requires a compliant Token for login.

    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class HomeController : ControllerBase
    {
        public string Get()
        {
            Console.WriteLine(User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name));
            return "Access Successful";
        }
    }

Then start ASP.NET Core and test access to https://localhost/api/home in Postman.

file

An HTTP 401 (Unauthorized) status code is returned, which is because the request did not carry a Token, leading to an inability to access the API.

Copy the generated Token from the console and paste it into Postman. Upon reaccessing, the response status code is 200, and the response is successful.

file

That's roughly how JWT authentication works in ASP.NET Core.

So, how is this implemented internally in ASP.NET Core? What features and pitfalls are there? Read on below~

2. Exploring Authorization and Authentication Middleware

In the operations above, we configured two middleware in the pipeline:

            app.UseAuthentication();
            app.UseAuthorization();

The role of app.UseAuthentication(); is to read the identity from the client (Cookie, Token, etc.) as configured in ASP.NET Core and parse it, storing it in context.User.

The role of app.UseAuthorization(); is to determine whether the current access Endpoint (Controller or Action) uses [Authorize] and has configured roles or policies, and then verifies whether the Cookie or Token is valid.

Using the attribute to set relevant access requires authentication generally has the following situations:

    // No attributes applied, can be accessed directly
    public class AController : ControllerBase
    {
        public string Get() { return "666"; }
    }

    /// <summary>
    /// The entire controller requires authorization to access
    /// </summary>
    [Authorize]
    public class BController : ControllerBase
    {
        public string Get() { return "666"; }
    }

    public class CController : ControllerBase
    {
        // Only Get requires authorization
        [Authorize]
        public string Get() { return "666"; }
        public string GetB() { return "666"; }
    }

    /// <summary>
    /// The entire controller requires authorization, but Get does not
    /// </summary>
    [Authorize]
    public class DController : ControllerBase
    {
        [AllowAnonymous]
        public string Get() { return "666"; }
    }

2.1 Implementing Token Parsing

As for the source code of app.UseAuthentication(); and app.UseAuthorization(); in ASP.NET Core, it's implemented in a verbose manner across various projects. To understand the roles of these two middleware, let's manually implement their functions.

The parsed Token is a ClaimsPrincipal object that can be assigned to context.User, allowing the API to use the User instance to retrieve user information.

In the middleware, you can retrieve and parse the Token from client requests using the following code:

            context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, JwtBearerDefaults.AuthenticationScheme);

So, how can we manually parse it from the raw Http request? Let's break down the steps slowly.

First, create a file named TestMiddleware to act as middleware.

    public class TestMiddleware
    {
        private readonly RequestDelegate _next;
        jwtSecurityTokenHandler = new JwtSecurityTokenHandler();
        public TestMiddleware(RequestDelegate next)
        {
            _next = next;
        }
        public async Task Invoke(HttpContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            // Area to write your code


            // Area to write your code
            await _next(context);
        }
    }

2.1.1 Retrieve the Token from Http

The following code can be used to retrieve the Token from the header in the http request.

Of course, the client might not carry the Token, and the result might be null, so it's wise to add a check.

Insert the following code where indicated:

            string tokenStr = context.Request.Headers["Authorization"].ToString();

The Authorization key in the Header is a string composed of Bearer {Token}.

2.1.2 Check if the Token is Valid

After obtaining the Token, you also need to check whether this Token is valid.

Since the Authorization is composed of Bearer {Token}, we need to remove the prefix Bearer to obtain the actual Token.

        /// <summary>
        /// Checks if the Token conforms to the standard JSON Web token requirements
        /// </summary>
        /// <param name="tokenStr"></param>
        /// <returns></returns>
        public bool IsCanReadToken(ref string tokenStr)
        {
            if (string.IsNullOrWhiteSpace(tokenStr) || tokenStr.Length < 7)
                return false;
            if (!tokenStr.Substring(0, 6).Equals(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme))
                return false;
            tokenStr = tokenStr.Substring(7);
            bool isCan = jwtSecurityTokenHandler.CanReadToken(tokenStr);

            return isCan;
        }

After obtaining the Token, use JwtSecurityTokenHandler.CanReadToken(tokenStr); to check if the Token meets the protocol's specifications.

Add the following check where indicated:

            if (!IsCanReadToken(ref tokenStr))
                return ;

2.1.3 Parse the Token

The following code can transform the contents of the Authorization Header into a JwtSecurityToken object.

(There are many ways to substring; you can choose whichever you like...)

        /// <summary>
        /// Decrypts the Token to get the JwtSecurityToken, JwtSecurityToken: SecurityToken
        /// </summary>
        /// <param name="tokenStr"></param>
        /// <returns></returns>
        public JwtSecurityToken GetJwtSecurityToken(string tokenStr)
        {
            var jwt = jwtSecurityTokenHandler.ReadJwtToken(tokenStr);
            return jwt;
        }

However, GetJwtSecurityToken isn't what we are focused on; we need to extract Claims.

JwtSecurityToken.Claims

Insert the following code where indicated:

            JwtSecurityToken jst = GetJwtSecurityToken(tokenStr);
            IEnumerable<Claim> claims = jst.Claims;

2.1.4 Generate context.User

context.User is of type ClaimsPrincipal, and we generate ClaimsPrincipal through the parsed Claims.

            JwtSecurityToken jst = GetJwtSecurityToken(tokenStr);
            IEnumerable<Claim> claims = jst.Claims;

            List<ClaimsIdentity> ci = new List<ClaimsIdentity>() { new ClaimsIdentity(claims) };
            context.User = new ClaimsPrincipal(ci);

最终的代码块是这样的

            // 我们写代码的区域
            string tokenStr = context.Request.Headers["Authorization"].ToString();
            string requestUrl = context.Request.Path.Value;
            if (!IsCanReadToken(ref tokenStr))
                return;
            JwtSecurityToken jst = GetJwtSecurityToken(tokenStr);
            IEnumerable<Claim> claims = jst.Claims;
            List<ClaimsIdentity> ci = new List<ClaimsIdentity>() { new ClaimsIdentity(claims) };

            context.User = new ClaimsPrincipal(ci);
            var x = new ClaimsPrincipal(ci);
            // 我们写代码的区域

2.2 实现校验认证

app.UseAuthentication(); 的大概实现过程已经做出了说明,现在我们来继续实现 app.UseAuthorization(); 中的功能。

继续使用上面的中间件,在原代码块区域添加新的区域。

            // 我们写代码的区域

            // 我们写的代码块 2

2.2.1 Endpoint

Endpoint 标识了一个 http 请求所访问的路由信息和 Controller 、Action 及其特性等信息。

[Authorize] 特性继承了 IAuthorizeData[AllowAnonymous] 特性继承了 IAllowAnonymous

以下代码可以获取所访问的节点信息。

            var endpoint = context.GetEndpoint();

那么如何判断所访问的 Controller 和 Action 是否使用了认证相关的特性?

            var authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();

Metadata 是一个 ASP.NET Core 实现的集合对象,GetOrderedMetadata<T> 可以找出需要的特性信息。

这个集合不会区分是 Controller 还是 Action 的 [Authorize] 特性。

那么判断是否有 [AllowAnonymous] 特性,可以这样使用。

            if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
            {
                await _next(context);
                return;
            }

痴者工良

高级程序员劝退师

文章评论