查看: 92|回复: 2

在ASP.NET Core中应用JWT

[复制链接]

1

主题

5

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2023-3-3 15:44:25 | 显示全部楼层 |阅读模式
1. JWT

1.1 JWT的结构

JWT是由三个dot (.) 分割的部分组成的:

  • Header通常由两部分组成:使用的加密算法 "alg" 以及Token的种类 "typ"。
{
  "alg": "HS256",
  "typ": "JWT"
}

  • Payload是JWT的第二个部分,主要包含了Claims。Claims是对实体及额外数据的描述,例如对用户身份、权限的描述,通常有以下三种Claims[1]:

    • Registered Claims: 是一些预先定义好的Claims,可以自由选择并使用,例如: iss (issuer), exp (expiration time), sub (subject)等等。
    • Public Claims: Claim的名称可以被任意定义。为了防止重复,任何新的Claim名称都应该被定义在IANA JSON Web Token Registry中或者使用一个包含不易重复命名空间的URI。
    • Private Claims: 是在团队中约定使用的自定义Claims,既不属于Registered也不属于Public。

  • Signature是JWT的最后一个部分,负责消息的校验。在签发者端,会有一个SecretKey,作为加密的密钥。先使用dot( . ) 将Based64Url编码后的Header和Payload与SecretKey拼接起来,在使用指定的加密算法进行加密,就得到了Signature。例如,使用HS256 也就是 HMAC SHA256进行加密,那么Signature等于[2]:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey)1.2 JWT结构总览

一个Encoded JWT长成这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJiMTlmZWNmOC0wOTU4LTRkNjYtODAwOS1lNmM2NzNiODQzYjciLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbiIsIm5iZiI6MTU5OTI5MTkxOSwiZXhwIjoxNTk5Mzc4MzE5LCJpc3MiOiJkdWtlLmNvbSIsImF1ZCI6ImR1a2UuY29tIn0.yEQtktSAy_Cgigfdg7FO8gqLtUHkT2UQYhS1S7d_kCQ可以在https://jwt.io/对JWT进行解码,如下图所示,左侧是Encoded JWT,右侧是经过解码的JWT,可以看出来已经被分为了Header,Payload与Signature三部分。可以看出,Header中包含了加密的算法,Payload中包含了一些信息,比如签发者"iss"等等。


2. 在http://ASP.NET Core中应用JWT

在本例中,基于WebAPI的模版项目来实现JWT的应用,先来看一下项目的结构:


在项目中我们主要用到的Nuget Package是 Microsoft.AspNetCore.Authentication.JwtBearer,可以让应用接收Bearer Token的一个http://ASP.NET Core中间件。AuthenticateController用于实现用户的登陆,生成JWT Token并返回给用户,OrderController用于对用户权限进行测试。
LoginDto是模拟用户登录的类:
    public class LoginDto
    {
        [Required]
        public string Email { get; set; }
        [Required]
        public string Password { get; set; }
    }
OrderAddDto是模拟商品添加的类:
    public class OrderAddDto
    {
        public string Name { get; set; }
        
        public string Price { get; set; }
    }
2.1 AuthenticateController

登陆方法主要分为两部分:用户名及密码的验证和JWT Token的生成。用户名及密码的验证在今后会使用数据库及SignInManager实现,目前先使用简单的判断,及邮箱和密码都不为空即可通过验证。
生成Token是由方法GenerateJWT实现的,请看注释部分:
    [ApiController]
    [Route("api")]
    public class AuthenticateController : ControllerBase
    {
        private readonly IConfiguration _configuration;
        
        public AuthenticateController(IConfiguration configuration)
        {
            _configuration = configuration;
        }
        
        [AllowAnonymous]
        [HttpPost("login")]
        public IActionResult Login([FromBody] LoginDto loginDto)
        {
            //User Authentication
            if (string.IsNullOrWhiteSpace(loginDto.Email) || string.IsNullOrWhiteSpace(loginDto.Password))
            {
                return BadRequest("Email or Password can not be empty");
            }

            //Generate Token
            var token = GenerateJWT();
            
            return Ok(token);
        }

         private string GenerateJWT()
        {
            // 1. 选择加密算法
            var algorithm = SecurityAlgorithms.HmacSha256;

            // 2. 定义需要使用到的Claims
            var claims = new[]
            {
                //sub user Id
                new Claim(JwtRegisteredClaimNames.Sub, "Duke"),
                //role Admin
                new Claim(ClaimTypes.Role, "Admin"),
            };

            // 3. 从 appsettings.json 中读取SecretKey
            var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:SecretKey"]));
            // 4. 生成Credentials
            var signingCredentials = new SigningCredentials(secretKey, algorithm);
            
            // 5. 根据以上组件,生成token
            var token = new JwtSecurityToken(
                _configuration["JWT:Issuer"],    //Issuer
                _configuration["JWT:Audience"],  //Audience
                claims,                          //Claims,
                DateTime.Now,                    //notBefore
                DateTime.Now.AddDays(1),         //expires
                signingCredentials
            );
            // 6. 将token变为string
            var jwtToken = new JwtSecurityTokenHandler().WriteToken(token);

            return jwtToken;
        }
    }
2.2 在Startup中添加服务

编写生成Token的Controller后,还需要在StartUp.ConfigureServices方法中添加相应的服务:
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        ValidIssuer = Configuration["JWT:Issuer"],
                        ValidateAudience = true,
                        ValidAudience = Configuration["JWT:Audience"],
                        ValidateLifetime = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:SecretKey"]))
                    };
                });
            services.AddControllers();
        }
不要忘了在StartUp.Configure中添加中间件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseAuthorization();
        }
2.3 测试 Token生成功能

在编写完主要代码后,使用OrderController对JWT生成及验证功能进行测试:
    [ApiController]
    [Route("api/orders")]
    public class OrderController : ControllerBase
    {
        [HttpPost]
        [Authorize(Roles = "Admin")]
        public IActionResult CreateOrder([FromBody] OrderAddDto orderAddDto)
        {
            return Ok(orderAddDto);
        }
        
        [HttpPost("superadmin")]
        [Authorize(Roles = "SuperAdmin")]
        public IActionResult CreateSuperOrder([FromBody] OrderAddDto orderAddDto)
        {
            return Ok(orderAddDto);
        }
    }
使用Postman,将一下登录信息加入body,选择POST方法进行请求https://localhost:5001/api/login:
{
    "Email":"Duke1234@gmail.com",
    "Password":"Duketest"
}得到返回的Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJEdWtlIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJuYmYiOjE1OTk1Nzc5NDUsImV4cCI6MTU5OTY2NDM0NSwiaXNzIjoiZHVrZS5jb20iLCJhdWQiOiJkdWtlLmZyb250ZW5kLmNvbSJ9.FrWImmpeNflGNeMPnG5AJgM76j2PnyPx9qQom0VHRSU可以将以上Token放到jwt.io上进行解码,SecretKey为“driedmeatflosscake“,如下图:


在Payload中,可以看到我们刚才设置的Claims,其中,"http://schemas.microsoft.com/ws/2008/06/identity/claims/role" 这个URI 对应的就是 ClaimTypes.Role,表明具体角色,详情请参考官方文档。
2.4 测试用户角色

最后,进行一下鉴权的测试,编写一个OrderController:
    [ApiController]
    [Route("api/orders")]
    public class OrderController : ControllerBase
    {
        [HttpPost]
        [Authorize(Roles = "Admin")]
        public IActionResult CreateOrder([FromBody] OrderAddDto orderAddDto)
        {
            return Ok(orderAddDto);
        }
        
        [HttpPost("superadmin")]
        [Authorize(Roles = "SuperAdmin")]
        public IActionResult CreateSuperOrder([FromBody] OrderAddDto orderAddDto)
        {
            return Ok(orderAddDto);
        }
    }
有两个方法,一个验证Role为Admin的CreateOrder,另一个是Role为SuperAdmin的CreateSuperOrder。由于签发Token时,设置的Role是Admin,所以理想状况是一个成功一个会失败。
在发送POST请求时,需要按照把Token添加到HTTP头部中:
Authorization : Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJEdWtlIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJuYmYiOjE1OTk1Nzc5NDUsImV4cCI6MTU5OTY2NDM0NSwiaXNzIjoiZHVrZS5jb20iLCJhdWQiOiJkdWtlLmZyb250ZW5kLmNvbSJ9.FrWImmpeNflGNeMPnG5AJgM76j2PnyPx9qQom0VHRSU在Postman中添加头部:


最后,发送请求进行测试,测试#CreateOrder,成功创建了Order:


而在CreateSuperOrder中,得到了403 Forbidden,说明权限不对,禁止访问。


3. 总结

在本文中,首先介绍了JWT的具体结构及组成部分:Header,Payload以及Signature。紧接着介绍了JWT在http://ASP.NET Core中的具体应用:生成Token,依赖注入,鉴权验证。限于篇幅,很多细节的地方没有具体展开,例如ClaimTypes,JwtSecurityTokenHandler的具体实现过程等等。并且,在本文中,关于用户名及密码的验证还没有实现,在后面的文章,会逐步完成对该功能的实现并使用MySQL对User进行存储。
参考


  • ^rfc7519 https://tools.ietf.org/html/rfc7519#section-4
  • ^jwt.io/introduction https://jwt.io/introduction/
回复

使用道具 举报

0

主题

7

帖子

0

积分

新手上路

Rank: 1

积分
0
发表于 2023-3-3 15:44:49 | 显示全部楼层
您好,我在最后一步测试时,两次都是401 Unauthorized,求问应该是哪方面的问题
回复

使用道具 举报

3

主题

8

帖子

14

积分

新手上路

Rank: 1

积分
14
发表于 2023-3-3 15:45:14 | 显示全部楼层
问题找到了,应该在 app.UseAuthorization() 之前加上 app.UseAuthentication()
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表