JWT 代表 ,它是一种授权机制,而不是身份验证。那么让我们弄清楚这两者之间有什么区别。 JSON Web Token 是一种允许验证用户是否正是他所声称的用户的机制。这是一个登录过程,用户提供用户名和密码,系统对其进行验证。因此身份验证回答了这个问题:用户是谁? 身份验证 是一种允许验证用户对特定资源具有哪些访问权限的机制。这是向用户授予某些角色以及特定角色所具有的一组权限的过程。那么,授权回答了这个问题:用户在系统中拥有什么权限? 授权 重要的是要理解身份验证始终是第一位的,授权是第二位的。换句话说,在验证身份之前,您无法获得许可。但最流行的授权方法是什么?有两种主要方法来处理 Web 应用程序的授权。 会议 Web 上授权用户的传统方法是基于 cookie 的服务器端会话。当用户登录并且服务器对其进行身份验证时,该过程开始。之后,服务器创建一个带有会话 ID 的会话,并将其存储在服务器内存中的某个位置。服务器将 Session ID 发送回客户端,客户端将 Session ID 存储在 cookie 中。对于每个请求,客户端都会发送一个会话 ID 作为请求的一部分,服务器会验证其内存中的会话 ID 以及与此会话相关的用户权限。 代币 另一种流行的方法是使用令牌进行授权。当用户输入登录名和密码并且客户端向服务器发送登录请求时,该过程类似地开始。服务器不创建会话,而是生成使用秘密令牌签名的令牌。然后,服务器将令牌发送回客户端,客户端必须将其存储在本地存储中。与基于会话的方法类似,客户端必须为每个请求向服务器发送令牌。但是,服务器不存储有关用户会话的任何附加信息。服务器必须验证令牌自创建并使用密钥签名以来未曾更改。 会话与令牌 基于会话的授权方法可能容易受到称为跨站点请求伪造 (CSRF) 的攻击。当攻击者指向他们登录的网站以执行他们无意的操作(例如提交付款或更改密码)时,这是一种攻击。 另一件事是,当使用基于会话的授权方法时,会在客户端和服务器之间创建有状态会话。问题是,如果客户端想要访问同一应用程序范围内的不同服务器,这些服务器必须共享会话状态。在另一种情况下,客户端需要在每台服务器上获得授权,因为会话会有所不同。 另一方面,基于令牌的授权方法不需要在服务器端存储会话数据,并且可以简化多个服务器之间的授权。 然而,令牌仍然可能被攻击者窃取,并且也很难使令牌失效。我们将在本文中进一步了解详细信息以及如何处理失效。 智威汤逊 JSON Web Token (JWT) 是一种开放标准,它定义了一种紧凑且独立的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。 JWT 可以使用密钥(使用 算法)或使用 或 的公钥/私钥对进行签名。 HMAC RSA ECDSA 智威汤逊结构 JSON Web 令牌由用点分隔的三部分组成 . 标头 { "alg": "HS256", "typ": "JWT" } 标头通常由两部分组成:令牌的类型和所使用的签名算法。 有效载荷 { "sub": "1234567890", "name": "John Doe", "admin": true } 有效负载包含声明,即有关用户的声明。然后对有效负载进行 编码以形成 JSON Web 令牌的第二部分。您可以 找到用作声明的标准字段的描述。 Base64Url 在此处 签名 HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 要创建签名部分,您必须获取编码的标头、编码的有效负载、秘密以及标头中指定的算法并对其进行签名。 该令牌通常如下所示: xxxxx.yyyyy.zzzzz 您可以导航到 并调试示例令牌或您自己的令牌。只需将您的令牌粘贴到 字段中,然后选择令牌签名的 即可。 jwt.io “编码” 算法 .NET项目 现在我们已经了解了 JWT 如何工作的理论知识,我们可以将其应用到现实项目中。假设我们有一个简单的 API 来表示咖啡实体的 CRUD 操作。我们将创建一个代表 Coffee API 的 ASP.NET Core API 项目。之后,我们将创建另一个 ASP.NET Core API 项目,该项目将代表可以生成 JWT 的 Identity API。在现实生活中,您可能会使用 或 或 来进行身份验证/授权。但是,我们将创建自己的身份 API 来演示如何生成 JWT。当Identity API完成后,我们可以调用它的控制器并根据用户的数据生成JWT。此外,我们可以通过授权配置来保护 Coffee API,该配置要求在每个请求中传递 JWT。 Identity Server Okta Auth0 咖啡API 首先,我们将创建一个代表 Coffee API 的简单 ASP.NET Core API 项目。这是该项目的结构: 让我们从 文件夹中的 开始。它是一个具有 和 属性的简单实体。 Model Coffee.cs Id Name 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; } } 完成后,我们可以在 文件夹中实现 ,它表示咖啡实体的 CRUD 操作。 Controller CoffeeController.cs 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 让我们创建另一个代表 Identity API 的 ASP.NET Core API 项目。这是该项目的结构: 让我们从 文件夹中的 开始,它表示生成具有 和 属性的新 JWT 的请求。 Contracts TokenGenerationRequest.cs Email Password namespace Hackernoon.Identity.API.Contracts; public class TokenGenerationRequest { public string Email { get; set; } public string Password { get; set; } } 我们只需要实现代表生成JWT逻辑的 。但在此之前,需要安装 NuGet 包。 TokenController.cs Microsoft.AspNetCore.Authentication.JwtBearer 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); } } 请注意,敏感常量(例如 、 和 必须放置在配置中的某个位置。它们被硬编码只是为了简化这个测试项目。 字段设置为 20 分钟,这意味着令牌在该时间内有效。您也可以配置此参数。 SecretKey Issuer Audience Lifetime 现在我们可以运行该项目并看到 Swagger UI 如下: 让我们调用 端点并生成一个新的 JWT。尝试以下有效负载: /token { "email": "john.doe@gmail.com", "password": "password" } Identity API会生成对应的JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJJc0dvdXJtZXQiOiJmYWxzZSIsImV4cCI6MTcwNzc4Mzk4MCwiaXNzIjoiSWRlbnRpdHlTZXJ2ZXJJc3N1ZXIiLCJhdWQiOiJJZGVudGl0eVNlcnZlckNsaWVudCJ9.4odXsbWak1C0uK3Ux-n7f58icYQQwlHjM54OjgMCVPM 在 Coffee API 中启用授权 现在,当 Identity API 准备就绪并向我们提供令牌时,我们可以通过授权来保护 Coffee API。再次需要安装 NuGet 包。 Microsoft.AspNetCore.Authentication.JwtBearer 我们需要通过认证服务来注册所需的服务。创建构建器后,立即将以下代码添加到 文件中。 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"; } } 我们需要指定 来描述授权期间将验证 JWT 的哪些参数。我们还指定类似于Identity API中的 来验证JWT签名。 查看有关 的更多详细信息。 TokenValidationParameters signingCredentials IssuerSigningKey 在此处 TokenValidationParameters 下一段代码向构建器添加中间件,以实现身份验证和授权功能。它应该添加在 和 方法之间。 UseHttpsRedirection() MapControllers() app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); 现在,我们可以在控制器或其操作上使用 属性。通过应用此代码,现在 中的所有操作都受到授权机制的保护,并且 JWT 必须作为请求的一部分发送。 Authorize CoffeeController [Route("coffee")] [ApiController] [Authorize] public class CoffeeController : ControllerBase { .. 如果我们调用 Coffee API 的任何端点,我们可以调试 并看到它已填充并且具有我们在 JWT 中指定的声明的 。了解 ASP.NET Core 如何在幕后处理授权非常重要。 HttpContext.User Identity 向 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[]{} } }); }); 之后我们就可以在右上角看到 按钮: 授权 当您单击 按钮时,您将能够输入 JWT,如下所示: “授权” 使用Postman进行测试 您可以不局限于使用 Swagger UI,还可以通过 Postman 工具执行 API 测试。我们首先调用 Identity API 的 端点。我们需要在 部分中使用值 指定 标头,因为我们将使用 JSON 作为有效负载。 /token Headers application/json Content-Type 之后,我们可以调用 端点并获取新的 JWT。 /token 现在,我们可以复制 JWT 并用它来调用 Coffee API。如果我们想要测试、创建和更新端点,我们需要指定类似于 Identity API 的 标头。还必须使用值 设置 标头。之后,只需点击 按钮即可查看结果。 Content-Type Bearer [your JWT value] Authorization “发送” 基于角色的授权 正如您所记得的,JWT 的有效负载部分是一组声明,其值正是键值对。基于角色的授权允许您根据用户所属的角色来区分对应用程序资源的访问。 如果我们使用为角色添加新声明的代码更新 Identity API 中 文件中的 方法;我们可以在 Coffee API 中处理基于角色的身份验证。 是角色声明的预定义名称。 TokenController.cs Create() 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 基于声明的授权 属性可以轻松处理基于角色的身份验证。但是,如果这还不够,并且我们希望根据某些用户属性(例如年龄或任何其他属性)区分访问权限,该怎么办?您可能已经猜到可以将声明添加到 JWT 并使用它们来构建授权逻辑。基于角色的授权本身是基于声明的授权的特例,就像角色是预定义类型的相同声明对象一样。 Authorize 让我们使用添加新声明 的代码更新 Identity API 中 文件中的 方法。 IsGourmet 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 可能是对本文的一个很好的补充。 文档