在本文中,你将学习如何在ASP.NET Core Web API中使用JWT身份验证。我将在编写代码时逐步简化。我们将构建两个终结点,一个用于客户登录,另一个用于获取客户订单。这些api将连接到在本地机器上运行的SQL Server Express数据库。

JWT是什么?

JWT或JSON Web Token基本上是格式化令牌的一种方式,令牌表示一种经过编码的数据结构,该数据结构具有紧凑、url安全、安全且自包含特点。

JWT身份验证是api和客户端之间进行通信的一种标准方式,因此双方可以确保发送/接收的数据是可信的和可验证的。

JWT应该由服务器发出,并使用加密的安全密钥对其进行数字签名,以便确保任何攻击者都无法篡改在令牌内发送的有效payload及模拟合法用户。

JWT结构包括三个部分,用点隔开,每个部分都是一个base64 url编码的字符串,JSON格式:

Header.Payload.Signature:

  1. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwicm9sZSI6IkFjY291bnQgTWFuYWdlciIsIm5iZiI6MTYwNDAxMDE4NSwiZXhwIjoxNjA0MDExMDg1LCJpYXQiOjE2MDQwMTAxODV9.XJLeLeUIlOZQjYyQ2JT3iZ-AsXtBoQ9eI1tEtOkpyj8

Header:表示用于对秘钥进行哈希的算法(例如HMACSHA256)

Payload:在客户端和API之间传输的数据或声明

Signature:Header和Payload连接的哈希

因为JWT标记是用base64编码的,所以可以使用jwt.io简单地研究它们或通过任何在线base64解码器。

由于这个特殊的原因,你不应该在JWT中保存关于用户的机密信息。

准备工作:

下载并安装Visual Studio 2019的最新版本(我使用的是Community Edition)。

下载并安装SQL Server Management Studio和SQL Server Express的最新更新。

开始我们的教程

让我们在Visual Studio 2019中创建一个新项目。项目命名为SecuringWebApiUsingJwtAuthentication。我们需要选择ASP.NET Core Web API模板,然后按下创建。Visual Studio现在将创建新的ASP.NET Core Web API模板项目。让我们删除WeatherForecastController.cs和WeatherForecast.cs文件,这样我们就可以开始创建我们自己的控制器和模型。

准备数据库

在你的机器上安装SQL Server Express和SQL Management Studio,

现在,从对象资源管理器中,右键单击数据库并选择new database,给数据库起一个类似CustomersDb的名称。

为了使这个过程更简单、更快,只需运行下面的脚本,它将创建表并将所需的数据插入到CustomersDb中。

  1. USE [CustomersDb]
  2. GO
  3. /****** Object: Table [dbo].[Customer] Script Date: 11/9/2020 1:56:38 AM ******/
  4. SET ANSI_NULLS ON
  5. GO
  6. SET QUOTED_IDENTIFIER ON
  7. GO
  8. CREATE TABLE [dbo].[Customer](
  9. [Id] [int] IDENTITY(1,1) NOT NULL,
  10. [Username] [nvarchar](255) NOT NULL,
  11. [Password] [nvarchar](255) NOT NULL,
  12. [PasswordSalt] [nvarchar](50) NOT NULL,
  13. [FirstName] [nvarchar](255) NOT NULL,
  14. [LastName] [nvarchar](255) NOT NULL,
  15. [Email] [nvarchar](255) NOT NULL,
  16. [TS] [smalldatetime] NOT NULL,
  17. [Active] [bit] NOT NULL,
  18. [Blocked] [bit] NOT NULL,
  19. CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED
  20. (
  21. [Id] ASC
  22. )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
  23. ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
  24. ) ON [PRIMARY]
  25. GO
  26. /****** Object: Table [dbo].[Order] Script Date: 11/9/2020 1:56:38 AM ******/
  27. SET ANSI_NULLS ON
  28. GO
  29. SET QUOTED_IDENTIFIER ON
  30. GO
  31. CREATE TABLE [dbo].[Order](
  32. [Id] [int] IDENTITY(1,1) NOT NULL,
  33. [Status] [nvarchar](50) NOT NULL,
  34. [Quantity] [int] NOT NULL,
  35. [Total] [decimal](19, 4) NOT NULL,
  36. [Currency] [char](3) NOT NULL,
  37. [TS] [smalldatetime] NOT NULL,
  38. [CustomerId] [int] NOT NULL,
  39. CONSTRAINT [PK_Order] PRIMARY KEY CLUSTERED
  40. (
  41. [Id] ASC
  42. )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
  43. ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
  44. ) ON [PRIMARY]
  45. GO
  46. SET IDENTITY_INSERT [dbo].[Customer] ON
  47. GO
  48. INSERT [dbo].[Customer] ([Id], [Username], [Password], [PasswordSalt], _
  49. [FirstName], [LastName], [Email], [TS], [Active], [Blocked]) _
  50. VALUES (1, N'coding', N'ezVOZenPoBHuLjOmnRlaI3Q3i/WcGqHDjSB5dxWtJLQ=', _
  51. N'MTIzNDU2Nzg5MTIzNDU2Nw==', N'Coding', N'Sonata', N'coding@codingsonata.com', _
  52. CAST(N'2020-10-30T00:00:00' AS SmallDateTime), 1, 1)
  53. GO
  54. INSERT [dbo].[Customer] ([Id], [Username], [Password], [PasswordSalt], _
  55. [FirstName], [LastName], [Email], [TS], [Active], [Blocked]) _
  56. VALUES (2, N'test', N'cWYaOOxmtWLC5DoXd3RZMzg/XS7Xi89emB7jtanDyAU=', _
  57. N'OTUxNzUzODUyNDU2OTg3NA==', N'Test', N'Testing', N'testing@codingsonata.com', _
  58. CAST(N'2020-10-30T00:00:00' AS SmallDateTime), 1, 0)
  59. GO
  60. SET IDENTITY_INSERT [dbo].[Customer] OFF
  61. GO
  62. SET IDENTITY_INSERT [dbo].[Order] ON
  63. GO
  64. INSERT [dbo].[Order] ([Id], [Status], [Quantity], [Total], [Currency], [TS], _
  65. [CustomerId]) VALUES (1, N'Processed', 5, CAST(120.0000 AS Decimal(19, 4)), _
  66. N'USD', CAST(N'2020-10-25T00:00:00' AS SmallDateTime), 1)
  67. GO
  68. INSERT [dbo].[Order] ([Id], [Status], [Quantity], [Total], [Currency], [TS], _
  69. [CustomerId]) VALUES (2, N'Completed', 2, CAST(750.0000 AS Decimal(19, 4)), _
  70. N'USD', CAST(N'2020-10-25T00:00:00' AS SmallDateTime), 1)
  71. GO
  72. SET IDENTITY_INSERT [dbo].[Order] OFF
  73. GO
  74. ALTER TABLE [dbo].[Order] WITH CHECK ADD CONSTRAINT [FK_Order_Customer] _
  75. FOREIGN KEY([CustomerId])
  76. REFERENCES [dbo].[Customer] ([Id])
  77. GO
  78. ALTER TABLE [dbo].[Order] CHECK CONSTRAINT [FK_Order_Customer]
  79. GO

准备数据库模型和DbContext

创建实体文件夹,然后添加Customer.cs:

  1. using System;
  2. using System.Collections.Generic;

  3. namespace SecuringWebApiUsingJwtAuthentication.Entities
  4. {
  5. public class Customer
  6. {
  7. public int Id { get; set; }
  8. public string Username { get; set; }
  9. public string Password { get; set; }
  10. public string PasswordSalt { get; set; }
  11. public string FirstName { get; set; }
  12. public string LastName { get; set; }
  13. public string Email { get; set; }
  14. public DateTime TS { get; set; }
  15. public bool Active { get; set; }
  16. public bool Blocked { get; set; }
  17. public ICollection<Order> Orders { get; set; }
  18. }
  19. }

添加Order.cs:

  1. using System;
  2. using System.Text.Json.Serialization;

  3. namespace SecuringWebApiUsingJwtAuthentication.Entities
  4. {
  5. public class Order
  6. {
  7. public int Id { get; set; }
  8. public string Status { get; set; }
  9. public int Quantity { get; set; }
  10. public decimal Total { get; set; }
  11. public string Currency { get; set; }
  12. public DateTime TS { get; set; }
  13. public int CustomerId { get; set; }
  14. [JsonIgnore]
  15. public Customer Customer { get; set; }
  16. }
  17. }

我将JsonIgnore属性添加到Customer对象,以便在对order对象进行Json序列化时隐藏它。

JsonIgnore属性来自 System.Text.Json.Serialization 命名空间,因此请确保将其包含在Order类的顶部。

现在我们将创建一个新类,它继承了EFCore的DbContext,用于映射数据库。

创建一个名为CustomersDbContext.cs的类:

  1. using Microsoft.EntityFrameworkCore;

  2. namespace SecuringWebApiUsingJwtAuthentication.Entities
  3. {
  4. public class CustomersDbContext : DbContext
  5. {
  6. public DbSet<Customer> Customers { get; set; }
  7. public DbSet<Order> Orders { get; set; }
  8. public CustomersDbContext
  9. (DbContextOptions<CustomersDbContext> options) : base(options)
  10. {
  11. }
  12. protected override void OnModelCreating(ModelBuilder modelBuilder)
  13. {
  14. modelBuilder.Entity<Customer>().ToTable("Customer");
  15. modelBuilder.Entity<Order>().ToTable("Order");
  16. }
  17. }
  18. }

Visual Studio现在将开始抛出错误,因为我们需要为EntityFramework Core和EntityFramework SQL Server引用NuGet包。

所以右键单击你的项目名称,选择管理NuGet包,然后下载以下包:

  • Microsoft.EntityFrameworkCore

  • Microsoft.EntityFrameworkCore.SqlServer

一旦上述包在项目中被引用,就不会再看到VS的错误了。

现在转到Startup.cs文件,在ConfigureServices中将我们的dbcontext添加到服务容器:

  1. services.AddDbContext<CustomersDbContext>(options=> options.UseSqlServer(Configuration.GetConnectionString("CustomersDbConnectionString")));

让我们打开appsettings.json文件,并在ConnectionStrings中创建连接字符串:

  1. {
  2. "ConnectionStrings": {
  3. "CustomersDbConnectionString": "Server=Home\\SQLEXPRESS;Database=CustomersDb;
  4. Trusted_Connection=True;MultipleActiveResultSets=true"
  5. },
  6. "Logging": {
  7. "LogLevel": {
  8. "Default": "Information",
  9. "Microsoft": "Warning",
  10. "Microsoft.Hosting.Lifetime": "Information"
  11. }
  12. },
  13. "AllowedHosts": "*"
  14. }

现在我们已经完成了数据库映射和连接部分。

我们将继续准备服务中的业务逻辑。

创建服务

创建一个名称带有Requests的新文件夹。

我们在这里有一个LoginRequest.cs类,它表示客户将提供给登录的用户名和密码字段。

  1. namespace SecuringWebApiUsingJwtAuthentication.Requests
  2. {
  3. public class LoginRequest
  4. {
  5. public string Username { get; set; }
  6. public string Password { get; set; }
  7. }
  8. }

为此,我们需要一个特殊的Response对象,返回有效的客户包括基本用户信息和他们的access token(JWT格式),这样他们就可以通过Authorization Header在后续请求授权api作为Bearer令牌。

因此,创建另一个文件夹,名称为Responses ,在其中,创建一个新的文件,名称为LoginResponse.cs:

  1. namespace SecuringWebApiUsingJwtAuthentication.Responses
  2. {
  3. public class LoginResponse
  4. {
  5. public string Username { get; set; }
  6. public string FirstName { get; set; }
  7. public string LastName { get; set; }
  8. public string Token { get; set; }
  9. }
  10. }

创建一个Interfaces文件夹:

添加一个新的接口ICustomerService.cs,这将包括客户登录的原型方法:

  1. using SecuringWebApiUsingJwtAuthentication.Requests;
  2. using SecuringWebApiUsingJwtAuthentication.Responses;
  3. using System.Threading.Tasks;

  4. namespace SecuringWebApiUsingJwtAuthentication.Interfaces
  5. {
  6. public interface ICustomerService
  7. {
  8. Task<LoginResponse> Login(LoginRequest loginRequest);
  9. }
  10. }

现在是实现ICustomerService的部分。

创建一个新文件夹并将其命名为Services。

添加一个名为CustomerService.cs的新类:

  1. using SecuringWebApiUsingJwtAuthentication.Entities;
  2. using SecuringWebApiUsingJwtAuthentication.Helpers;
  3. using SecuringWebApiUsingJwtAuthentication.Interfaces;
  4. using SecuringWebApiUsingJwtAuthentication.Requests;
  5. using SecuringWebApiUsingJwtAuthentication.Responses;
  6. using System.Linq;
  7. using System.Threading.Tasks;

  8. namespace SecuringWebApiUsingJwtAuthentication.Services
  9. {
  10. public class CustomerService : ICustomerService
  11. {
  12. private readonly CustomersDbContext customersDbContext;
  13. public CustomerService(CustomersDbContext customersDbContext)
  14. {
  15. this.customersDbContext = customersDbContext;
  16. }

  17. public async Task<LoginResponse> Login(LoginRequest loginRequest)
  18. {
  19. var customer = customersDbContext.Customers.SingleOrDefault
  20. (customer => customer.Active && customer.Username == loginRequest.Username);

  21. if (customer == null)
  22. {
  23. return null;
  24. }
  25. var passwordHash = HashingHelper.HashUsingPbkdf2
  26. (loginRequest.Password, customer.PasswordSalt);

  27. if (customer.Password != passwordHash)
  28. {
  29. return null;
  30. }

  31. var token = await Task.Run( () => TokenHelper.GenerateToken(customer));

  32. return new LoginResponse { Username = customer.Username,
  33. FirstName = customer.FirstName, LastName = customer.LastName, Token = token };
  34. }
  35. }
  36. }

上面的登录函数在数据库中检查客户的用户名、密码,如果这些条件匹配,那么我们将生成一个JWT并在LoginResponse中为调用者返回它,否则它将在LoginReponse中返回一个空值。

首先,让我们创建一个名为Helpers的新文件夹。

添加一个名为HashingHelper.cs的类。

这将用于检查登录请求中的密码的哈希值,以匹配数据库中密码的哈希值和盐值的哈希值。

在这里,我们使用的是基于派生函数(PBKDF2),它应用了HMac函数结合一个散列算法(sha - 256)将密码和盐值(base64编码的随机数与大小128位)重复多次后作为迭代参数中指定的参数(是默认的10000倍),运用在我们的示例中,并获得一个随机密钥的产生结果。

派生函数(或密码散列函数),如PBKDF2或Bcrypt,由于随着salt一起应用了大量的迭代,需要更长的计算时间和更多的资源来破解密码。

注意:千万不要将密码以纯文本保存在数据库中,要确保计算并保存密码的哈希,并使用一个键派生函数散列算法有一个很大的尺寸(例如,256位或更多)和随机大型盐值(64位或128位),使其难以破解。

此外,在构建用户注册屏幕或页面时,应该确保应用强密码(字母数字和特殊字符的组合)的验证规则以及密码保留策略,这甚至可以最大限度地提高存储密码的安全性。

  1. using System;
  2. using System.Security.Cryptography;
  3. using System.Text;

  4. namespace SecuringWebApiUsingJwtAuthentication.Helpers
  5. {
  6. public class HashingHelper
  7. {
  8. public static string HashUsingPbkdf2(string password, string salt)
  9. {
  10. using var bytes = new Rfc2898DeriveBytes
  11. (password, Convert.FromBase64String(salt), 10000, HashAlgorithmName.SHA256);
  12. var derivedRandomKey = bytes.GetBytes(32);
  13. var hash = Convert.ToBase64String(derivedRandomKey);
  14. return hash;
  15. }
  16. }
  17. }

生成 JSON Web Token (JWT)

在Helpers 文件夹中添加另一个名为TokenHelper.cs的类。

这将包括我们的令牌生成函数:

  1. using Microsoft.IdentityModel.Tokens;
  2. using SecuringWebApiUsingJwtAuthentication.Entities;
  3. using System;
  4. using System.IdentityModel.Tokens.Jwt;
  5. using System.Security.Claims;
  6. using System.Security.Cryptography;

  7. namespace SecuringWebApiUsingJwtAuthentication.Helpers
  8. {
  9. public class TokenHelper
  10. {
  11. public const string Issuer = "http://codingsonata.com";
  12. public const string Audience = "http://codingsonata.com";
  13.  
  14. public const string Secret =
  15. "OFRC1j9aaR2BvADxNWlG2pmuD392UfQBZZLM1fuzDEzDlEpSsn+
  16. btrpJKd3FfY855OMA9oK4Mc8y48eYUrVUSw==";
  17.  
  18. //Important note***************
  19. //The secret is a base64-encoded string, always make sure to
  20. //use a secure long string so no one can guess it. ever!.a very recommended approach
  21. //to use is through the HMACSHA256() class, to generate such a secure secret,
  22. //you can refer to the below function
  23. //you can run a small test by calling the GenerateSecureSecret() function
  24. //to generate a random secure secret once, grab it, and use it as the secret above
  25. //or you can save it into appsettings.json file and then load it from them,
  26. //the choice is yours

  27. public static string GenerateSecureSecret()
  28. {
  29. var hmac = new HMACSHA256();
  30. return Convert.ToBase64String(hmac.Key);
  31. }

  32. public static string GenerateToken(Customer customer)
  33. {
  34. var tokenHandler = new JwtSecurityTokenHandler();
  35. var key = Convert.FromBase64String(Secret);

  36. var claimsIdentity = new ClaimsIdentity(new[] {
  37. new Claim(ClaimTypes.NameIdentifier, customer.Id.ToString()),
  38. new Claim("IsBlocked", customer.Blocked.ToString())
  39. });
  40. var signingCredentials = new SigningCredentials
  41. (new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature);

  42. var tokenDescriptor = new SecurityTokenDescriptor
  43. {
  44. Subject = claimsIdentity,
  45. Issuer = Issuer,
  46. Audience = Audience,
  47. Expires = DateTime.Now.AddMinutes(15),
  48. SigningCredentials = signingCredentials,
  49.  
  50. };
  51. var token = tokenHandler.CreateToken(tokenDescriptor);
  52. return tokenHandler.WriteToken(token);
  53. }
  54. }
  55. }

我们需要引用这里的另一个库

  • Microsoft.AspNetCore.Authentication.JwtBearer

让我们仔细看看GenerateToken函数:

在传递customer对象时,我们可以使用任意数量的属性,并将它们添加到将嵌入到令牌中的声明里。但在本教程中,我们将只嵌入客户的id属性。

JWT依赖于数字签名算法,其中推荐的算法之一,我们在这里使用的是HMac哈希算法使用256位的密钥大小。

我们从之前使用HMACSHA256类生成的随机密钥生成密钥。你可以使用任何随机字符串,但要确保使用长且难以猜测的文本,最好使用前面代码示例中所示的HMACSHA256类。

你可以将生成的秘钥保存在常量或appsettings中,并将其加载到Startup.cs。

创建控制器

现在我们需要在CustomersController使用CustomerService的Login方法。

创建一个新文件夹并将其命名为Controllers。

添加一个新的文件CustomersController.cs。如果登录成功,它将有一个POST方法接收用户名和密码并返回JWT令牌和其他客户细节,否则它将返回404。

  1. using Microsoft.AspNetCore.Mvc;
  2. using SecuringWebApiUsingJwtAuthentication.Interfaces;
  3. using SecuringWebApiUsingJwtAuthentication.Requests;
  4. using System.Threading.Tasks;

  5. namespace SecuringWebApiUsingJwtAuthentication.Controllers
  6. {
  7. [Route("api/[controller]")]
  8. [ApiController]
  9. public class CustomersController : ControllerBase
  10. {
  11. private readonly ICustomerService customerService;

  12. public CustomersController(ICustomerService customerService)
  13. {
  14. this.customerService = customerService;
  15. }
  16. [HttpPost]
  17. [Route("login")]
  18. public async Task<IActionResult> Login(LoginRequest loginRequest)
  19. {
  20. if (loginRequest == null || string.IsNullOrEmpty(loginRequest.Username) ||
  21. string.IsNullOrEmpty(loginRequest.Password))
  22. {
  23. return BadRequest("Missing login details");
  24. }

  25. var loginResponse = await customerService.Login(loginRequest);

  26. if (loginResponse == null)
  27. {
  28. return BadRequest($"Invalid credentials");
  29. }

  30. return Ok(loginResponse);
  31. }
  32. }
  33. }

正如这里看到的,我们定义了一个POST方法用来接收LoginRequest(用户名和密码),它对输入进行基本验证,并调用客户服务的 Login方法。

我们将使用接口ICustomerService通过控制器的构造函数注入CustomerService,我们需要在启动的ConfigureServices函数中定义此注入:

  1. services.AddScoped<ICustomerService, CustomerService>();

现在,在运行API之前,我们可以配置启动URL,还可以知道IIS Express对象中http和https的端口号。

这就是你的launchsettings.json文件:

  1. {
  2. "schema": "http://json.schemastore.org/launchsettings.json",
  3. "iisSettings": {
  4. "windowsAuthentication": false,
  5. "anonymousAuthentication": true,
  6. "iisExpress": {
  7. "applicationUrl": "http://localhost:60057",
  8. "sslPort": 44375
  9. }
  10. },
  11. "profiles": {
  12. "IIS Express": {
  13. "commandName": "IISExpress",
  14. "launchBrowser": true,
  15. "launchUrl": "",
  16. "environmentVariables": {
  17. "ASPNETCORE_ENVIRONMENT": "Development"
  18. }
  19. },
  20. "SecuringWebApiUsingJwtAuthentication": {
  21. "commandName": "Project",
  22. "launchBrowser": true,
  23. "launchUrl": "",
  24. "applicationUrl": "https://localhost:5001;http://localhost:5000",
  25. "environmentVariables": {
  26. "ASPNETCORE_ENVIRONMENT": "Development"
  27. }
  28. }
  29. }
  30. }

现在,如果你在本地机器上运行API,应该能够调用login方法并生成第一个JSON Web Token。

通过PostMan测试Login

打开浏览器,打开PostMan。

打开新的request选项卡,运行应用程序后,填写设置中的本地主机和端口号。

从body中选择raw和JSON,并填写JSON对象,这将使用该对象通过我们的RESTful API登录到客户数据库。

以下是PostMan的请求/回应

这是我们的第一个JWT。

让我们准备API来接收这个token,然后验证它,在其中找到一个声明,然后为调用者返回一个响应。

可以通过许多方式验证你的api、授权你的用户:

1.根据.net core团队的说法,基于策略的授权还可以包括定义角色和需求,这是通过细粒度方法实现API身份验证的推荐方法。

2.拥有一个自定义中间件来验证在带有Authorize属性修饰的api上传递的请求头中的JWT。

3.在为JWT授权标头验证请求标头集合的一个或多个控制器方法上设置自定义属性。

在本教程中,我将以最简单的形式使用基于策略的身份验证,只是为了向你展示可以应用基于策略的方法来保护您的ASP.NET Core Web api。

身份验证和授权之间的区别

身份验证是验证用户是否有权访问api的过程。

通常,试图访问api的未经身份验证的用户将收到一个http 401未经授权的响应。

授权是验证经过身份验证的用户是否具有访问特定API的正确权限的过程。

通常,试图访问仅对特定角色或需求有效的API的未授权用户将收到http 403 Forbidden响应。

配置身份验证和授权

现在,让我们在startup中添加身份验证和授权配置。

在ConfigureServices方法中,我们需要定义身份验证方案及其属性,然后定义授权选项。

在身份验证部分中,我们将使用默认JwtBearer的方案,我们将定义TokenValidationParamters,以便我们验证IssuerSigningKey确保签名了使用正确的Security Key。

在授权部分中,我们将添加一个策略,当指定一个带有Authorize属性的终结点上时,它将只对未被阻止的客户进行授权。

被阻止的登录客户仍然能够访问没有定义策略的其他端点,但是对于定义了 OnlyNonBlockedCustomer策略的端点,被阻塞的客户将被403 Forbidden响应拒绝访问。

首先,创建一个文件夹并将其命名为Requirements。

添加一个名为 CustomerStatusRequirement.cs的新类。

  1. using Microsoft.AspNetCore.Authorization;

  2. namespace SecuringWebApiUsingJwtAuthentication.Requirements
  3. {
  4. public class CustomerBlockedStatusRequirement : IAuthorizationRequirement
  5. {
  6. public bool IsBlocked { get; }
  7. public CustomerBlockedStatusRequirement(bool isBlocked)
  8. {
  9. IsBlocked = isBlocked;
  10. }
  11. }
  12. }

然后创建另一个文件夹并将其命名为Handlers。

添加一个名为CustomerBlockedStatusHandler.cs的新类:

  1. using Microsoft.AspNetCore.Authorization;
  2. using SecuringWebApiUsingJwtAuthentication.Helpers;
  3. using SecuringWebApiUsingJwtAuthentication.Requirements;
  4. using System;
  5. using System.Threading.Tasks;

  6. namespace SecuringWebApiUsingJwtAuthentication.Handlers
  7. {
  8. public class CustomerBlockedStatusHandler :
  9. AuthorizationHandler<CustomerBlockedStatusRequirement>
  10. {
  11. protected override Task HandleRequirementAsync
  12. (AuthorizationHandlerContext context, CustomerBlockedStatusRequirement requirement)
  13. {
  14. var claim = context.User.FindFirst(c => c.Type == "IsBlocked" &&
  15. c.Issuer == TokenHelper.Issuer);
  16. if (!context.User.HasClaim(c => c.Type == "IsBlocked" &&
  17. c.Issuer == TokenHelper.Issuer))
  18. {
  19. return Task.CompletedTask;
  20. }

  21. string value = context.User.FindFirst(c => c.Type == "IsBlocked" &&
  22. c.Issuer == TokenHelper.Issuer).Value;
  23. var customerBlockedStatus = Convert.ToBoolean(value);

  24. if (customerBlockedStatus == requirement.IsBlocked)
  25. {
  26. context.Succeed(requirement);
  27. }

  28. return Task.CompletedTask;
  29. }
  30. }
  31. }

最后,让我们将所有身份验证和授权配置添加到服务集合:

  1. services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  2. .AddJwtBearer(options =>
  3. {
  4. options.TokenValidationParameters = new TokenValidationParameters
  5. {
  6. ValidateIssuer = true,
  7. ValidateAudience = true,
  8. ValidateIssuerSigningKey = true,
  9. ValidIssuer = TokenHelper.Issuer,
  10. ValidAudience = TokenHelper.Audience,
  11. IssuerSigningKey = new SymmetricSecurityKey
  12. (Convert.FromBase64String(TokenHelper.Secret))
  13. };
  14. });

  15. services.AddAuthorization(options =>
  16. {
  17. options.AddPolicy("OnlyNonBlockedCustomer", policy =>
  18. {
  19. policy.Requirements.Add(new CustomerBlockedStatusRequirement(false));

  20. });
  21. });

  22. services.AddSingleton<IAuthorizationHandler, CustomerBlockedStatusHandler>();

为此,我们需要包括以下命名空间:

  • using Microsoft.AspNetCore.Authorization;

  • using Microsoft.IdentityModel.Tokens;

  • using SecuringWebApiUsingJwtAuthentication.Helpers;

  • using SecuringWebApiUsingJwtAuthentication.Handlers;

  • using SecuringWebApiUsingJwtAuthentication.Requirements;

现在,上面的方法不能单独工作,身份验证和授权必须通过Startup中的Configure 方法包含在ASP.NET Core API管道:

  1. app.UseAuthentication();
  2. app.UseAuthorization();

这里,我们完成了ASP.NET Core Web API使用JWT身份验证。

创建OrderService

我们将需要一种专门处理订单的新服务。

在Interfaces文件夹下创建一个名为IOrderService.cs的新接口:

  1. using SecuringWebApiUsingJwtAuthentication.Entities;
  2. using System.Collections.Generic;
  3. using System.Threading.Tasks;

  4. namespace SecuringWebApiUsingJwtAuthentication.Interfaces
  5. {
  6. public interface IOrderService
  7. {
  8. Task<List<Order>> GetOrdersByCustomerId(int id);
  9. }
  10. }

该接口包括一个方法,该方法将根据客户Id检索指定客户的订单。

让我们实现这个接口。

在Services 文件夹下创建一个名为OrderService.cs的新类:

  1. using SecuringWebApiUsingJwtAuthentication.Entities;
  2. using SecuringWebApiUsingJwtAuthentication.Interfaces;
  3. using System.Collections.Generic;
  4. using System.Threading.Tasks;
  5. using System.Linq;
  6. using Microsoft.EntityFrameworkCore;

  7. namespace SecuringWebApiUsingJwtAuthentication.Services
  8. {
  9. public class OrderService : IOrderService
  10. {
  11. private readonly CustomersDbContext customersDbContext;

  12. public OrderService(CustomersDbContext customersDbContext)
  13. {
  14. this.customersDbContext = customersDbContext;
  15. }
  16. public async Task<List<Order>> GetOrdersByCustomerId(int id)
  17. {
  18. var orders = await customersDbContext.Orders.Where
  19. (order => order.CustomerId == id).ToListAsync();
  20.  
  21. return orders;
  22. }
  23. }
  24. }

创建OrdersController

现在我们需要创建一个新的终结点,它将使用Authorize属性和OnlyNonBlockedCustomer策略。

在Controllers文件夹下添加一个新控制器,命名为OrdersController.cs:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Security.Claims;
  5. using System.Threading.Tasks;
  6. using Microsoft.AspNetCore.Authorization;
  7. using Microsoft.AspNetCore.Http;
  8. using Microsoft.AspNetCore.Mvc;
  9. using SecuringWebApiUsingJwtAuthentication.Interfaces;

  10. namespace SecuringWebApiUsingJwtAuthentication.Controllers
  11. {
  12. [Route("api/[controller]")]
  13. [ApiController]
  14. public class OrdersController : ControllerBase
  15. {
  16. private readonly IOrderService orderService;
  17. public OrdersController(IOrderService orderService)
  18. {
  19. this.orderService = orderService;
  20. }

  21. [HttpGet()]
  22. [Authorize(Policy = "OnlyNonBlockedCustomer")]
  23. public async Task<IActionResult> Get()
  24. {
  25. var claimsIdentity = HttpContext.User.Identity as ClaimsIdentity;
  26. var claim = claimsIdentity.FindFirst(ClaimTypes.NameIdentifier);
  27. if (claim == null)
  28. {
  29. return Unauthorized("Invalid customer");
  30. }
  31. var orders = await orderService.GetOrdersByCustomerId(int.Parse(claim.Value));
  32. if (orders == null || !orders.Any())
  33. {
  34. return BadRequest($"No order was found");
  35. }
  36. return Ok(orders);
  37. }
  38. }
  39. }

我们将创建一个GET方法,用于检索客户的订单。

此方法将使用Authorize属性进行修饰,并仅为非阻塞客户定义访问策略。

任何试图获取订单的被阻止的登录客户,即使该客户经过了正确的身份验证,也会收到一个403 Forbidden请求,因为该客户没有被授权访问这个特定的端点。

我们需要在Startup.cs文件中包含OrderService。

将下面的内容添加到CustomerService行下面。

  1. services.AddScoped<IOrderService, OrderService>();

这是Startup.cs文件的完整视图,需要与你的文件进行核对。

  1. using System;
  2. using Microsoft.AspNetCore.Authentication.JwtBearer;
  3. using Microsoft.AspNetCore.Authorization;
  4. using Microsoft.AspNetCore.Builder;
  5. using Microsoft.AspNetCore.Hosting;
  6. using Microsoft.EntityFrameworkCore;
  7. using Microsoft.Extensions.Configuration;
  8. using Microsoft.Extensions.DependencyInjection;
  9. using Microsoft.Extensions.Hosting;
  10. using Microsoft.IdentityModel.Tokens;
  11. using SecuringWebApiUsingJwtAuthentication.Entities;
  12. using SecuringWebApiUsingJwtAuthentication.Handlers;
  13. using SecuringWebApiUsingJwtAuthentication.Helpers;
  14. using SecuringWebApiUsingJwtAuthentication.Interfaces;
  15. using SecuringWebApiUsingJwtAuthentication.Requirements;
  16. using SecuringWebApiUsingJwtAuthentication.Services;

  17. namespace SecuringWebApiUsingJwtAuthentication
  18. {
  19. public class Startup
  20. {
  21. public Startup(IConfiguration configuration)
  22. {
  23. Configuration = configuration;
  24. }

  25. public IConfiguration Configuration { get; }

  26. // This method gets called by the runtime.
  27. // Use this method to add services to the container.
  28. public void ConfigureServices(IServiceCollection services)
  29. {
  30. services.AddDbContext<CustomersDbContext>
  31. (options => options.UseSqlServer(Configuration.GetConnectionString
  32. ("CustomersDbConnectionString")));
  33. services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  34. .AddJwtBearer(options =>
  35. {
  36. options.TokenValidationParameters = new TokenValidationParameters
  37. {
  38. ValidateIssuer = true,
  39. ValidateAudience = true,
  40. ValidateIssuerSigningKey = true,
  41. ValidIssuer = TokenHelper.Issuer,
  42. ValidAudience = TokenHelper.Audience,
  43. IssuerSigningKey = new SymmetricSecurityKey
  44. (Convert.FromBase64String(TokenHelper.Secret))
  45. };
  46.  
  47. });
  48. services.AddAuthorization(options =>
  49. {
  50. options.AddPolicy("OnlyNonBlockedCustomer", policy => {
  51. policy.Requirements.Add(new CustomerBlockedStatusRequirement(false));
  52. });
  53. });
  54. services.AddSingleton<IAuthorizationHandler, CustomerBlockedStatusHandler>();
  55. services.AddScoped<ICustomerService, CustomerService>();
  56. services.AddScoped<IOrderService, OrderService>();
  57. services.AddControllers();
  58. }

  59. // This method gets called by the runtime.
  60. // Use this method to configure the HTTP request pipeline.
  61. public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
  62. {
  63. if (env.IsDevelopment())
  64. {
  65. app.UseDeveloperExceptionPage();
  66. }
  67. app.UseHttpsRedirection();
  68. app.UseRouting();
  69. app.UseAuthentication();
  70. app.UseAuthorization();
  71. app.UseEndpoints(endpoints =>
  72. {
  73. endpoints.MapControllers();
  74. });
  75. }
  76. }
  77. }

通过PostMan测试

运行应用程序并打开Postman。

让我们尝试用错误的密码登录:

现在让我们尝试正确的凭证登录:

如果你使用上面的令牌并在jwt.io中进行验证,你将看到header和payload细节:

现在让我们测试get orders终结点,我们将获取令牌字符串并将其作为Bearer Token 在授权头传递:

为什么我们的API没有返回403?

如果你回到前面的一步,你将注意到我们的客户被阻止了(“IsBlocked”:True),即只有非阻止的客户才被授权访问该端点。

为此,我们将解除该客户的阻止,或者尝试与另一个客户登录。

返回数据库,并将用户的Blocked更改为False。

现在再次打开Postman并以相同的用户登录,这样我们就得到一个新的JWT,其中包括IsBlocked类型的更新值。

接下来在jwt.io中重新查看:

你现在注意到区别了吗?

现在不再被阻止,因为我们获得了一个新的JWT,其中包括从数据库读取的声明。

让我们尝试使用这个新的JWT访问我们的终结点。

它工作了!

已经成功通过了策略的要求,因此订单现在显示了。

让我们看看如果用户试图访问这个终结点而不传递授权头会发生什么:

JWT是防篡改的,所以没有人可以糊弄它。

我希望本教程使你对API安全和JWT身份验证有了很好的理解。

欢迎关注我的公众号——码农译站,如果你有喜欢的外文技术文章,可以通过公众号留言推荐给我。

原文链接:https://www.codeproject.com/Articles/5287315/Secure-ASP-NET-Core-Web-API-using-JWT-Authenticati

使用JWT创建安全的ASP.NET Core Web API的更多相关文章

  1. 【译】使用Jwt身份认证保护 Asp.Net Core Web Api

    原文出自Rui Figueiredo的博客,原文链接<Secure a Web Api in ASP.NET Core> 摘要:这边文章阐述了如何使用 Json Web Token (Jw ...

  2. ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程

    ASP.NET Core Web API中带有刷新令牌的JWT身份验证流程 翻译自:地址 在今年年初,我整理了有关将JWT身份验证与ASP.NET Core Web API和Angular一起使用的详 ...

  3. List多个字段标识过滤 IIS发布.net core mvc web站点 ASP.NET Core 实战:构建带有版本控制的 API 接口 ASP.NET Core 实战:使用 ASP.NET Core Web API 和 Vue.js 搭建前后端分离项目 Using AutoFac

    List多个字段标识过滤 class Program{  public static void Main(string[] args) { List<T> list = new List& ...

  4. 如何在ASP.NET Core Web API中使用Mini Profiler

    原文如何在ASP.NET Core Web API中使用Mini Profiler 由Anuraj发表于2019年11月25日星期一阅读时间:1分钟 ASPNETCoreMiniProfiler 这篇 ...

  5. 在 ASP.NET Core Web API中使用 Polly 构建弹性容错的微服务

    在 ASP.NET Core Web API中使用 Polly 构建弹性容错的微服务 https://procodeguide.com/programming/polly-in-aspnet-core ...

  6. ASP.NET Core Web API + Angular 仿B站(三)后台配置 JWT 的基于 token 的验证

    前言: 本系列文章主要为对所学 Angular 框架的一次微小的实践,对 b站页面作简单的模仿. 本系列文章主要参考资料: 微软文档: https://docs.microsoft.com/zh-cn ...

  7. 在Mac下创建ASP.NET Core Web API

    在Mac下创建ASP.NET Core Web API 这系列文章是参考了.NET Core文档和源码,可能有人要问,直接看官方的英文文档不就可以了吗,为什么还要写这些文章呢? 原因如下: 官方文档涉 ...

  8. ASP.NET Core Web API 最佳实践指南

    原文地址: ASP.NET-Core-Web-API-Best-Practices-Guide 介绍 当我们编写一个项目的时候,我们的主要目标是使它能如期运行,并尽可能地满足所有用户需求. 但是,你难 ...

  9. Azure AD(二)调用受Microsoft 标识平台保护的 ASP.NET Core Web API 下

    一,引言 上一节讲到如何在我们的项目中集成Azure AD 保护我们的API资源,以及在项目中集成Swagger,并且如何把Swagger作为一个客户端进行认证和授权去访问我们的WebApi资源的?本 ...

随机推荐

  1. jdbc编程学习之增删改查(2)

    一,enum类型的使用 在SQL中没有布尔类型的数据,我们都使用过布尔类型,当属性的值只用两种情况时.例如性别等.那在数据库对这些属性的值个数比较少时我们应该使用什么数据类型呢?SQL给我们提供了枚举 ...

  2. 关于客户和供应商预制凭证添加WBS字段

    客户和供应商的预制凭证的对应标准屏幕SAPLF0400301和SAPLF0400302并没有提供WBS字段,有的需求需要增强WBS字段到屏幕上,本文会介绍增强WBS字段的步骤,也请读者多多指教.为了不 ...

  3. three.js 显示中文字体 和 tween应用

    今天郭先生说一下如何在three中显示中文字体,然后结合tween实现文字位置的动画.线案例请点击博客原文. 1. 生成中文字体 我们都使用过three.js的FontLoader加载typeface ...

  4. String StringBuffer StringBuilder之间的区别

    String:

  5. volatile 关键字精讲

    1.错误案例 通过一个案例引出volatile关键字,例如以下代码示例 : 此时没有加volatile关键字两个线程间的通讯就会有问题 public class ThreadsShare { priv ...

  6. i5 11300H和i5 10300H 的区别

    i5-11300H 为 4 核 8 线程,主频 3.1GHz,睿频 4.4GHz,三级缓存 8MB 选 i5-11300H还是i5 10300h 这些点很重要!看完你就知道了https://list. ...

  7. Java基础--接口回调(接口 对象名 = new 类名)理解

    接口 对象名1 = new 类名和类名 对象名2 = new 类名的区别是什么? 实例 /** *Person.java 接口 */ public interface Person { void in ...

  8. mysql 应用 持续更新

    1.安装方法 贴出,my.ini # MySQL Server Instance Configuration File # -------------------------------------- ...

  9. WebApi 中请求的 JSON 数据字段作为 POST 参数传入

    使用 POST 方式请求 JSON 数据到服务器 WebAPI 接口时需要将 JSON 格式封装成数据模型接收参数.即使参数较少,每个接口仍然需要单独创建模型接收.下面方法实现了将 JSON 参数中的 ...

  10. 搭乘“AI大数据”快车,肌肤管家,助力美业数字化发展

    经过疫情的发酵,加速推动各行各业进入数据时代的步伐.美业,一个通过自身技术.产品让用户变美的行业,在AI大数据的加持下表现尤为突出. 对于美妆护肤企业来说,一边是进入存量市场,一边是疫后的复苏期,一边 ...