|
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/
|
|