paint-brush
为什么 ASP.NET Core 项目中需要 JWT?by@igorlopushko
7,145
7,145

为什么 ASP.NET Core 项目中需要 JWT?

Igor Lopushko16m2024/02/13
Read on Terminal Reader

这个故事是关于如何创建一个Web API来生成JWT,然后在CRUD Web API中使用它进行授权。
featured image - 为什么 ASP.NET Core 项目中需要 JWT?
Igor Lopushko HackerNoon profile picture
0-item

JWT 代表JSON Web Token ,它是一种授权机制,而不是身份验证。那么让我们弄清楚这两者之间有什么区别。


身份验证是一种允许验证用户是否正是他所声称的用户的机制。这是一个登录过程,用户提供用户名和密码,系统对其进行验证。因此身份验证回答了这个问题:用户是谁?


授权是一种允许验证用户对特定资源具有哪些访问权限的机制。这是向用户授予某些角色以及特定角色所具有的一组权限的过程。那么,授权回答了这个问题:用户在系统中拥有什么权限?

身份验证与授权


重要的是要理解身份验证始终是第一位的,授权是第二位的。换句话说,在验证身份之前,您无法获得许可。但最流行的授权方法是什么?有两种主要方法来处理 Web 应用程序的授权。

会议

Web 上授权用户的传统方法是基于 cookie 的服务器端会话。当用户登录并且服务器对其进行身份验证时,该过程开始。之后,服务器创建一个带有会话 ID 的会话,并将其存储在服务器内存中的某个位置。服务器将 Session ID 发送回客户端,客户端将 Session ID 存储在 cookie 中。对于每个请求,客户端都会发送一个会话 ID 作为请求的一部分,服务器会验证其内存中的会话 ID 以及与此会话相关的用户权限。

基于会话的授权

代币

另一种流行的方法是使用令牌进行授权。当用户输入登录名和密码并且客户端向服务器发送登录请求时,该过程类似地开始。服务器不创建会话,而是生成使用秘密令牌签名的令牌。然后,服务器将令牌发送回客户端,客户端必须将其存储在本地存储中。与基于会话的方法类似,客户端必须为每个请求向服务器发送令牌。但是,服务器不存储有关用户会话的任何附加信息。服务器必须验证令牌自创建并使用密钥签名以来未曾更改。

基于令牌的授权

会话与令牌

基于会话的授权方法可能容易受到称为跨站点请求伪造 (CSRF) 的攻击。当攻击者指向他们登录的网站以执行他们无意的操作(例如提交付款或更改密码)时,这是一种攻击。


另一件事是,当使用基于会话的授权方法时,会在客户端和服务器之间创建有状态会话。问题是,如果客户端想要访问同一应用程序范围内的不同服务器,这些服务器必须共享会话状态。在另一种情况下,客户端需要在每台服务器上获得授权,因为会话会有所不同。

基于会话的授权状态共享


另一方面,基于令牌的授权方法不需要在服务器端存储会话数据,并且可以简化多个服务器之间的授权。


然而,令牌仍然可能被攻击者窃取,并且也很难使令牌失效。我们将在本文中进一步了解详细信息以及如何处理失效。

智威汤逊

JSON Web Token (JWT) 是一种开放标准,它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。 JWT 可以使用密钥(使用HMAC算法)或使用RSAECDSA的公钥/私钥对进行签名。

智威汤逊结构

JSON Web 令牌由用点分隔的三部分组成.


  • 标头
{ "alg": "HS256", "typ": "JWT" }

标头通常由两部分组成:令牌的类型和所使用的签名算法。


  • 有效载荷
{ "sub": "1234567890", "name": "John Doe", "admin": true }

有效负载包含声明,即有关用户的声明。然后对有效负载进行Base64Url编码以形成 JSON Web 令牌的第二部分。您可以在此处找到用作声明的标准字段的描述。


  • 签名
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

要创建签名部分,您必须获取编码的标头、编码的有效负载、秘密以及标头中指定的算法并对其进行签名。


该令牌通常如下所示:

xxxxx.yyyyy.zzzzz


您可以导航到jwt.io并调试示例令牌或您自己的令牌。只需将您的令牌粘贴到“编码”字段中,然后选择令牌签名的算法即可。

jwt.io 调试器

.NET项目

现在我们已经了解了 JWT 如何工作的理论知识,我们可以将其应用到现实项目中。假设我们有一个简单的 API 来表示咖啡实体的 CRUD 操作。我们将创建一个代表 Coffee API 的 ASP.NET Core API 项目。之后,我们将创建另一个 ASP.NET Core API 项目,该项目将代表可以生成 JWT 的 Identity API。在现实生活中,您可能会使用Identity ServerOktaAuth0来进行身份验证/授权。但是,我们将创建自己的身份 API 来演示如何生成 JWT。当Identity API完成后,我们可以调用它的控制器并根据用户的数据生成JWT。此外,我们可以通过授权配置来保护 Coffee API,该配置要求在每个请求中传递 JWT。

.NET 项目景观

咖啡API

首先,我们将创建一个代表 Coffee API 的简单 ASP.NET Core API 项目。这是该项目的结构:

咖啡 API - 项目结构


让我们从Model文件夹中的Coffee.cs开始。它是一个具有IdName属性的简单实体。

 namespace Hackernoon.Coffee.API.Model; public class Coffee { public int Id { get; set; } public string Name { get; set; } }


我们需要在使用 API 时存储我们的实体。那么,我们来介绍一个简单的内存存储。它位于Data文件夹中的Storage.cs文件中。

 namespace Hackernoon.Coffee.API.Data; public static class Storage { private static readonly List<Model.Coffee> Data = new(); public static List<Model.Coffee> GetAll() { return Data; } public static bool Create(Model.Coffee model) { if (Data.Any(c => c.Id == model.Id || c.Name == model.Name)) return false; Data.Add(new Model.Coffee { Id = model.Id, Name = model.Name }); return true; } public static bool Delete(int id) { if (Data.All(c => c.Id != id)) return false; Data.Remove(Storage.Data.First(c => c.Id == id)); return true; } public static bool Update(Model.Coffee model) { if (Data.All(c => c.Id != model.Id)) return false; Data.First(c => c.Id == model.Id).Name = model.Name; return true; } }


我们需要一个类来表示对 Coffee API 的请求。因此,让我们在Contracts文件夹中创建CoffeeRequest.cs

 namespace Hackernoon.Coffee.API.Contracts; public class CoffeeRequest { public int Id { get; set; } public string Name { get; set; } }


完成后,我们可以在Controller文件夹中实现CoffeeController.cs ,它表示咖啡实体的 CRUD 操作。

 using Hackernoon.Coffee.API.Contracts; using Hackernoon.Coffee.API.Data; using Microsoft.AspNetCore.Mvc; namespace Hackernoon.Coffee.API.Controllers; [Route("coffee")] [ApiController] public class CoffeeController : ControllerBase { [HttpGet] public IList<Model.Coffee> GetAll() { return Storage.GetAll(); } [HttpPost] public IActionResult Create([FromBody]CoffeeRequest request) { var model = new Model.Coffee { Id = request.Id, Name = request.Name }; if (!Storage.Create(model)) return new BadRequestResult(); return new OkResult(); } [HttpDelete] public IActionResult Delete(int id) { if (!Storage.Delete(id)) return new BadRequestResult(); return new OkResult(); } [HttpPut] public IActionResult Update([FromBody] CoffeeRequest request) { var model = new Model.Coffee() { Id = request.Id, Name = request.Name }; if (!Storage.Update(model)) return new BadRequestResult(); return new OkResult(); } }


Coffee API已经完成,我们可以运行项目,看到Swagger UI如下:

咖啡 API - Swagger UI

身份识别API

让我们创建另一个代表 Identity API 的 ASP.NET Core API 项目。这是该项目的结构:

Identity API - 项目结构

让我们从Contracts文件夹中的TokenGenerationRequest.cs开始,它表示生成具有EmailPassword属性的新 JWT 的请求。

 namespace Hackernoon.Identity.API.Contracts; public class TokenGenerationRequest { public string Email { get; set; } public string Password { get; set; } }


我们只需要实现代表生成JWT逻辑的TokenController.cs 。但在此之前,需要安装Microsoft.AspNetCore.Authentication.JwtBearer NuGet 包。

 using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Hackernoon.Identity.API.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace Hackernoon.Identity.API.Controllers; [Route("token")] public class TokenController : ControllerBase { private const string SecretKey = "VerySecretAndLongKey-NeedMoreSymbolsHere-123"; private const string Issuer = "IdentityServerIssuer"; private const string Audience = "IdentityServerClient"; private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(20); [HttpPost] public string Create([FromBody]TokenGenerationRequest request) { var claims = new List<Claim> {new Claim(ClaimTypes.Email, request.Email) }; var jwt = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: claims, expires: DateTime.UtcNow.Add(Lifetime), signingCredentials: CreateSigningCredentials()); return new JwtSecurityTokenHandler().WriteToken(jwt); } private static SigningCredentials CreateSigningCredentials() { return new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)), SecurityAlgorithms.HmacSha256); } }


请注意,敏感常量(例如SecretKeyIssuerAudience必须放置在配置中的某个位置。它们被硬编码只是为了简化这个测试项目。 Lifetime字段设置为 20 分钟,这意味着令牌在该时间内有效。您也可以配置此参数。


现在我们可以运行该项目并看到 Swagger UI 如下:

身份 API - Swagger UI


让我们调用/token端点并生成一个新的 JWT。尝试以下有效负载:

 { "email": "[email protected]", "password": "password" }


Identity API会生成对应的JWT:

 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJJc0dvdXJtZXQiOiJmYWxzZSIsImV4cCI6MTcwNzc4Mzk4MCwiaXNzIjoiSWRlbnRpdHlTZXJ2ZXJJc3N1ZXIiLCJhdWQiOiJJZGVudGl0eVNlcnZlckNsaWVudCJ9.4odXsbWak1C0uK3Ux-n7f58icYQQwlHjM54OjgMCVPM

在 Coffee API 中启用授权

现在,当 Identity API 准备就绪并向我们提供令牌时,我们可以通过授权来保护 Coffee API。再次需要安装Microsoft.AspNetCore.Authentication.JwtBearer NuGet 包。


我们需要通过认证服务来注册所需的服务。创建构建器后,立即将以下代码添加到Program.cs文件中。

 var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "IdentityServerIssuer", ValidateAudience = true, ValidAudience = "IdentityServerClient", ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("VerySecretAndLongKey-NeedMoreSymbolsHere-123")), ValidateIssuerSigningKey = true, }; }); builder.Services.AddAuthorization();


重要的是要记住中间件中的顺序很重要。我们通过调用AddAuthentication()方法并指定JwtBearerDefaults.AuthenticationScheme作为身份验证模式来启用身份验证。它是一个包含Bearer值的常量。

 namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// <summary>Default values used by bearer authentication.</summary> public static class JwtBearerDefaults { /// <summary> /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions /// </summary> public const string AuthenticationScheme = "Bearer"; } }


我们需要指定TokenValidationParameters来描述授权期间将验证 JWT 的哪些参数。我们还指定类似于Identity API中的signingCredentials IssuerSigningKey来验证JWT签名。 在此处查看有关TokenValidationParameters的更多详细信息。


下一段代码向构建器添加中间件,以实现身份验证和授权功能。它应该添加在UseHttpsRedirection()MapControllers()方法之间。

 app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();


现在,我们可以在控制器或其操作上使用Authorize属性。通过应用此代码,现在CoffeeController中的所有操作都受到授权机制的保护,并且 JWT 必须作为请求的一部分发送。

 [Route("coffee")] [ApiController] [Authorize] public class CoffeeController : ControllerBase { ..


如果我们调用 Coffee API 的任何端点,我们可以调试HttpContext.User并看到它已填充并且具有我们在 JWT 中指定的声明的Identity 。了解 ASP.NET Core 如何在幕后处理授权非常重要。

Coffee API - 声明由 JWT 填充

向 Swagger UI 添加授权

我们在保护 Coffee API 的授权方面做了很多工作。但是,如果您运行 Coffee API 项目并打开 Swagger UI,您将无法将 JWT 作为请求的一部分发送。为了解决这个问题,我们需要使用以下代码更新Program.cs文件:

 builder.Services.AddSwaggerGen(option => { option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter a valid token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "Bearer" }); option.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type=ReferenceType.SecurityScheme, Id="Bearer" } }, new string[]{} } }); });


之后我们就可以在右上角看到授权按钮:

Coffee API - 出现授权按钮


当您单击“授权”按钮时,您将能够输入 JWT,如下所示:

Coffee API - 输入 JWT 值

使用Postman进行测试

您可以不局限于使用 Swagger UI,还可以通过 Postman 工具执行 API 测试。我们首先调用 Identity API 的/token端点。我们需要在Headers部分中使用值application/json指定Content-Type标头,因为我们将使用 JSON 作为有效负载。

Identity API - 指定标头


之后,我们可以调用/token端点并获取新的 JWT。

身份 API - 生成 JWT


现在,我们可以复制 JWT 并用它来调用 Coffee API。如果我们想要测试、创建和更新端点,我们需要指定类似于 Identity API 的Content-Type标头。还必须使用值Bearer [your JWT value]设置Authorization标头。之后,只需点击“发送”按钮即可查看结果。

Coffee API - 获取所有实体

基于角色的授权

正如您所记得的,JWT 的有效负载部分是一组声明,其值正是键值对。基于角色的授权允许您根据用户所属的角色来区分对应用程序资源的访问。


如果我们使用为角色添加新声明的代码更新 Identity API 中TokenController.cs文件中的Create()方法;我们可以在 Coffee API 中处理基于角色的身份验证。 ClaimTypes.Role是角色声明的预定义名称。

 var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim(ClaimTypes.Role, "Barista") };


更新CoffeeController.cs文件中指定角色名称的Authorize属性:

 [Authorize(Roles = "Barista")]


现在,所有调用 Coffee API 的用户都必须拥有带有Barista值的角色声明。否则,他们将收到403 Forbidden状态代码。

基于声明的授权

Authorize属性可以轻松处理基于角色的身份验证。但是,如果这还不够,并且我们希望根据某些用户属性(例如年龄或任何其他属性)区分访问权限,该怎么办?您可能已经猜到可以将声明添加到 JWT 并使用它们来构建授权逻辑。基于角色的授权本身是基于声明的授权的特例,就像角色是预定义类型的相同声明对象一样。


让我们使用添加新声明IsGourmet的代码更新 Identity API 中TokenController.cs文件中的Create()方法。

 var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim("IsGourmet", "true") };


在 Coffee API 的 Program.cs 文件中,我们需要创建一个验证声明并可在Authorize属性中使用的策略。必须在AddAuthentication()方法调用之后添加以下代码。

 builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForGourmet", policy => { policy.RequireClaim("IsGourmet", "true"); }); });


更新CoffeeController.cs文件中指定策略名称的Authorize属性:

 [Authorize(Policy = "OnlyForGourmet")]

概括

恭喜!您在学习 .NET 中的 JWT 方面付出了巨大的努力。现在,您必须充分了解 JWT 原理以及为什么使用它在 .NET 应用程序中执行授权很重要。但我们只是触及了 ASP.NET Core 应用程序中身份验证和授权领域的皮毛。


我建议查看有关我们在本文中讨论的主题的 Microsoft 文档。 .NET 平台中还有很多用于授权和角色管理的内置功能。关于授权的 Microsoft 文档可能是对本文的一个很好的补充。