关于c#:使用JWT创建安全的ASPNET-Core-Web-API

2次阅读

共计 21716 个字符,预计需要花费 55 分钟才能阅读完成。

在本文中,你将学习如何在 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:

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 中。

USE [CustomersDb]
GO
/****** Object:  Table [dbo].[Customer]    Script Date: 11/9/2020 1:56:38 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Customer]([Id] [int] IDENTITY(1,1) NOT NULL,
  [Username] [nvarchar](255) NOT NULL,
  [Password] [nvarchar](255) NOT NULL,
  [PasswordSalt] [nvarchar](50) NOT NULL,
  [FirstName] [nvarchar](255) NOT NULL,
  [LastName] [nvarchar](255) NOT NULL,
  [Email] [nvarchar](255) NOT NULL,
  [TS] [smalldatetime] NOT NULL,
  [Active] [bit] NOT NULL,
  [Blocked] [bit] NOT NULL,
 CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED 
([Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
 ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[Order]    Script Date: 11/9/2020 1:56:38 AM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Order]([Id] [int] IDENTITY(1,1) NOT NULL,
  [Status] [nvarchar](50) NOT NULL,
  [Quantity] [int] NOT NULL,
  [Total] [decimal](19, 4) NOT NULL,
  [Currency] [char](3) NOT NULL,
  [TS] [smalldatetime] NOT NULL,
  [CustomerId] [int] NOT NULL,
 CONSTRAINT [PK_Order] PRIMARY KEY CLUSTERED 
([Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
       ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[Customer] ON 
GO
INSERT [dbo].[Customer] ([Id], [Username], [Password], [PasswordSalt], _
       [FirstName], [LastName], [Email], [TS], [Active], [Blocked]) _
       VALUES (1, N'coding', N'ezVOZenPoBHuLjOmnRlaI3Q3i/WcGqHDjSB5dxWtJLQ=', _
       N'MTIzNDU2Nzg5MTIzNDU2Nw==', N'Coding', N'Sonata', N'coding@codingsonata.com', _
       CAST(N'2020-10-30T00:00:00' AS SmallDateTime), 1, 1)
GO
INSERT [dbo].[Customer] ([Id], [Username], [Password], [PasswordSalt], _
       [FirstName], [LastName], [Email], [TS], [Active], [Blocked]) _
       VALUES (2, N'test', N'cWYaOOxmtWLC5DoXd3RZMzg/XS7Xi89emB7jtanDyAU=', _
       N'OTUxNzUzODUyNDU2OTg3NA==', N'Test', N'Testing', N'testing@codingsonata.com', _
       CAST(N'2020-10-30T00:00:00' AS SmallDateTime), 1, 0)
GO
SET IDENTITY_INSERT [dbo].[Customer] OFF
GO
SET IDENTITY_INSERT [dbo].[Order] ON 
GO
INSERT [dbo].[Order] ([Id], [Status], [Quantity], [Total], [Currency], [TS], _
       [CustomerId]) VALUES (1, N'Processed', 5, CAST(120.0000 AS Decimal(19, 4)), _
       N'USD', CAST(N'2020-10-25T00:00:00' AS SmallDateTime), 1)
GO
INSERT [dbo].[Order] ([Id], [Status], [Quantity], [Total], [Currency], [TS], _
       [CustomerId]) VALUES (2, N'Completed', 2, CAST(750.0000 AS Decimal(19, 4)), _
       N'USD', CAST(N'2020-10-25T00:00:00' AS SmallDateTime), 1)
GO
SET IDENTITY_INSERT [dbo].[Order] OFF
GO
ALTER TABLE [dbo].[Order]  WITH CHECK ADD  CONSTRAINT [FK_Order_Customer] _
      FOREIGN KEY([CustomerId])
REFERENCES [dbo].[Customer] ([Id])
GO
ALTER TABLE [dbo].[Order] CHECK CONSTRAINT [FK_Order_Customer]
GO

筹备数据库模型和 DbContext

创立实体文件夹,而后增加 Customer.cs:

using System;
using System.Collections.Generic;
​
namespace SecuringWebApiUsingJwtAuthentication.Entities
{
    public class Customer
    {public int Id { get; set;}
        public string Username {get; set;}
        public string Password {get; set;}
        public string PasswordSalt {get; set;}
        public string FirstName {get; set;}
        public string LastName {get; set;}
        public string Email {get; set;}
        public DateTime TS {get; set;}
        public bool Active {get; set;}
        public bool Blocked {get; set;}
        public ICollection<Order> Orders {get; set;}
    }
}

增加 Order.cs:

using System;
using System.Text.Json.Serialization;
​
namespace SecuringWebApiUsingJwtAuthentication.Entities
{
    public class Order
    {public int Id { get; set;}
        public string Status {get; set;}
        public int Quantity {get; set;}
        public decimal Total {get; set;}
        public string Currency {get; set;}
        public DateTime TS {get; set;}
        public int CustomerId {get; set;}
        [JsonIgnore]
        public Customer Customer {get; set;}
    }
}

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

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

当初咱们将创立一个新类,它继承了 EFCore 的 DbContext,用于映射数据库。

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

using Microsoft.EntityFrameworkCore;
​
namespace SecuringWebApiUsingJwtAuthentication.Entities
{
    public class CustomersDbContext : DbContext
    {public DbSet<Customer> Customers { get; set;}
        public DbSet<Order> Orders {get; set;}
        public CustomersDbContext
               (DbContextOptions<CustomersDbContext> options) : base(options)
        { }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {modelBuilder.Entity<Customer>().ToTable("Customer");
            modelBuilder.Entity<Order>().ToTable("Order");
        }
    }
}

Visual Studio 当初将开始抛出谬误,因为咱们须要为 EntityFramework Core 和 EntityFramework SQL Server 援用 NuGet 包。

所以右键单击你的项目名称,抉择治理 NuGet 包,而后下载以下包:

  • Microsoft.EntityFrameworkCore
  • Microsoft.EntityFrameworkCore.SqlServer

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

当初转到 Startup.cs 文件,在 ConfigureServices 中将咱们的 dbcontext 增加到服务容器:

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

让咱们关上 appsettings.json 文件,并在 ConnectionStrings 中创立连贯字符串:

{
  "ConnectionStrings": {
    "CustomersDbConnectionString": "Server=HomeSQLEXPRESS;Database=CustomersDb;
     Trusted_Connection=True;MultipleActiveResultSets=true"},"Logging": {"LogLevel": {"Default":"Information","Microsoft":"Warning","Microsoft.Hosting.Lifetime":"Information"}
  },
  "AllowedHosts": "*"
}

当初咱们曾经实现了数据库映射和连贯局部。

咱们将持续筹备服务中的业务逻辑。

创立服务

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

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

namespace SecuringWebApiUsingJwtAuthentication.Requests
{
    public class LoginRequest
    {public string Username { get; set;}
        public string Password {get; set;}
    }
}

为此, 咱们须要一个非凡的 Response 对象, 返回无效的客户包含根本用户信息和他们的 access token(JWT 格局), 这样他们就能够通过 Authorization Header 在后续申请受权 api 作为 Bearer 令牌。

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

namespace SecuringWebApiUsingJwtAuthentication.Responses
{
    public class LoginResponse
    {public string Username { get; set;}
        public string FirstName {get; set;}
        public string LastName {get; set;}
        public string Token {get; set;}
    }
}

创立一个 Interfaces 文件夹:

增加一个新的接口 ICustomerService.cs,这将包含客户登录的原型办法:

using SecuringWebApiUsingJwtAuthentication.Requests;
using SecuringWebApiUsingJwtAuthentication.Responses;
using System.Threading.Tasks;
​
namespace SecuringWebApiUsingJwtAuthentication.Interfaces
{
    public interface ICustomerService
    {Task<LoginResponse> Login(LoginRequest loginRequest);
    }
}

当初是实现 ICustomerService 的局部。

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

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

using SecuringWebApiUsingJwtAuthentication.Entities;
using SecuringWebApiUsingJwtAuthentication.Helpers;
using SecuringWebApiUsingJwtAuthentication.Interfaces;
using SecuringWebApiUsingJwtAuthentication.Requests;
using SecuringWebApiUsingJwtAuthentication.Responses;
using System.Linq;
using System.Threading.Tasks;
​
namespace SecuringWebApiUsingJwtAuthentication.Services
{
    public class CustomerService : ICustomerService
    {
        private readonly CustomersDbContext customersDbContext;
        public CustomerService(CustomersDbContext customersDbContext)
        {this.customersDbContext = customersDbContext;}
​
        public async Task<LoginResponse> Login(LoginRequest loginRequest)
        {
            var customer = customersDbContext.Customers.SingleOrDefault
            (customer => customer.Active && customer.Username == loginRequest.Username);
​
            if (customer == null)
            {return null;}
            var passwordHash = HashingHelper.HashUsingPbkdf2
                               (loginRequest.Password, customer.PasswordSalt);
​
            if (customer.Password != passwordHash)
            {return null;}
​
            var token = await Task.Run(() => TokenHelper.GenerateToken(customer));
​
            return new LoginResponse { Username = customer.Username, 
            FirstName = customer.FirstName, LastName = customer.LastName, Token = token };
        }
    }
}

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

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

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

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

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

派生函数(或明码散列函数),如 PBKDF2 或 Bcrypt,因为随着 salt 一起利用了大量的迭代,须要更长的计算工夫和更多的资源来破解明码。

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

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

using System;
using System.Security.Cryptography;
using System.Text;
​
namespace SecuringWebApiUsingJwtAuthentication.Helpers
{
    public class HashingHelper
    {public static string HashUsingPbkdf2(string password, string salt)
        {
            using var bytes = new Rfc2898DeriveBytes
            (password, Convert.FromBase64String(salt), 10000, HashAlgorithmName.SHA256);
            var derivedRandomKey = bytes.GetBytes(32);
            var hash = Convert.ToBase64String(derivedRandomKey);
            return hash;
        }
    }
}

生成 JSON Web Token (JWT)

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

这将包含咱们的令牌生成函数:

using Microsoft.IdentityModel.Tokens;
using SecuringWebApiUsingJwtAuthentication.Entities;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
​
namespace SecuringWebApiUsingJwtAuthentication.Helpers
{
    public class TokenHelper
{
        public const string Issuer = "http://codingsonata.com";
        public const string Audience = "http://codingsonata.com";
      
        public const string Secret = 
        "OFRC1j9aaR2BvADxNWlG2pmuD392UfQBZZLM1fuzDEzDlEpSsn+
         btrpJKd3FfY855OMA9oK4Mc8y48eYUrVUSw==";
      
        //Important note***************
        //The secret is a base64-encoded string, always make sure to 
        //use a secure long string so no one can guess it. ever!.a very recommended approach 
        //to use is through the HMACSHA256() class, to generate such a secure secret, 
        //you can refer to the below function 
        //you can run a small test by calling the GenerateSecureSecret() function 
        //to generate a random secure secret once, grab it, and use it as the secret above 
        //or you can save it into appsettings.json file and then load it from them, 
        //the choice is yours
​
        public static string GenerateSecureSecret()
        {var hmac = new HMACSHA256();
            return Convert.ToBase64String(hmac.Key);
        }
​
        public static string GenerateToken(Customer customer)
        {var tokenHandler = new JwtSecurityTokenHandler();
            var key =  Convert.FromBase64String(Secret);
​
            var claimsIdentity = new ClaimsIdentity(new[] {new Claim(ClaimTypes.NameIdentifier, customer.Id.ToString()),
                new Claim("IsBlocked", customer.Blocked.ToString())
            });
            var signingCredentials = new SigningCredentials
            (new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature);
​
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = claimsIdentity,
                Issuer = Issuer,
                Audience = Audience,
                Expires = DateTime.Now.AddMinutes(15),
                SigningCredentials = signingCredentials,
                
            };
            var token = tokenHandler.CreateToken(tokenDescriptor);
            return tokenHandler.WriteToken(token);
        }
    }
}

咱们须要援用这里的另一个库

  • Microsoft.AspNetCore.Authentication.JwtBearer

让咱们认真看看 GenerateToken 函数:

在传递 customer 对象时,咱们能够应用任意数量的属性,并将它们增加到将嵌入到令牌中的申明里。但在本教程中,咱们将只嵌入客户的 id 属性。

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

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

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

创立控制器

当初咱们须要在 CustomersController 应用 CustomerService 的 Login 办法。

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

增加一个新的文件 CustomersController.cs。如果登录胜利,它将有一个 POST 办法接管用户名和明码并返回 JWT 令牌和其余客户细节,否则它将返回 404。

using Microsoft.AspNetCore.Mvc;
using SecuringWebApiUsingJwtAuthentication.Interfaces;
using SecuringWebApiUsingJwtAuthentication.Requests;
using System.Threading.Tasks;
​
namespace SecuringWebApiUsingJwtAuthentication.Controllers
{[Route("api/[controller]")]
    [ApiController]
    public class CustomersController : ControllerBase
    {
        private readonly ICustomerService customerService;
​
        public CustomersController(ICustomerService customerService)
        {this.customerService = customerService;}
        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login(LoginRequest loginRequest)
        {if (loginRequest == null || string.IsNullOrEmpty(loginRequest.Username) || 
                string.IsNullOrEmpty(loginRequest.Password))
            {return BadRequest("Missing login details");
            }
​
            var loginResponse = await customerService.Login(loginRequest);
​
            if (loginResponse == null)
            {return BadRequest($"Invalid credentials");
            }
​
            return Ok(loginResponse);
        }
    }
}

正如这里看到的,咱们定义了一个 POST 办法用来接管 LoginRequest(用户名和明码),它对输出进行根本验证,并调用客户服务的 Login 办法。

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

services.AddScoped<ICustomerService, CustomerService>();

当初,在运行 API 之前,咱们能够配置启动 URL,还能够晓得 IIS Express 对象中 http 和 https 的端口号。

这就是你的 launchsettings.json 文件:

{
  "schema": "http://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication": false,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:60057",
      "sslPort": 44375
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "","environmentVariables": {"ASPNETCORE_ENVIRONMENT":"Development"}
    },
    "SecuringWebApiUsingJwtAuthentication": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "","applicationUrl":"https://localhost:5001;http://localhost:5000","environmentVariables": {"ASPNETCORE_ENVIRONMENT":"Development"}
    }
  }
}

当初,如果你在本地机器上运行 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 的新类。

using Microsoft.AspNetCore.Authorization;
​
namespace SecuringWebApiUsingJwtAuthentication.Requirements
{
    public class CustomerBlockedStatusRequirement : IAuthorizationRequirement
    {public bool IsBlocked { get;}
        public CustomerBlockedStatusRequirement(bool isBlocked)
        {IsBlocked = isBlocked;}
    }
}

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

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

using Microsoft.AspNetCore.Authorization;
using SecuringWebApiUsingJwtAuthentication.Helpers;
using SecuringWebApiUsingJwtAuthentication.Requirements;
using System;
using System.Threading.Tasks;
​
namespace SecuringWebApiUsingJwtAuthentication.Handlers
{
    public class CustomerBlockedStatusHandler : 
           AuthorizationHandler<CustomerBlockedStatusRequirement>
    {
        protected override Task HandleRequirementAsync
        (AuthorizationHandlerContext context, CustomerBlockedStatusRequirement requirement)
        {
            var claim = context.User.FindFirst(c => c.Type == "IsBlocked" && 
                                               c.Issuer == TokenHelper.Issuer);
            if (!context.User.HasClaim(c => c.Type == "IsBlocked" && 
                                            c.Issuer == TokenHelper.Issuer))
            {return Task.CompletedTask;}
​
            string value = context.User.FindFirst(c => c.Type == "IsBlocked" && 
                                                  c.Issuer == TokenHelper.Issuer).Value;
            var customerBlockedStatus = Convert.ToBoolean(value);
​
            if (customerBlockedStatus == requirement.IsBlocked)
            {context.Succeed(requirement);
            }
​
            return Task.CompletedTask;
        }
    }
}

最初,让咱们将所有身份验证和受权配置增加到服务汇合:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = TokenHelper.Issuer,
                ValidAudience = TokenHelper.Audience,
                IssuerSigningKey = new SymmetricSecurityKey
                    (Convert.FromBase64String(TokenHelper.Secret))
            };
        });
​
services.AddAuthorization(options =>
{
    options.AddPolicy("OnlyNonBlockedCustomer", policy =>
    {policy.Requirements.Add(new CustomerBlockedStatusRequirement(false));
​
    });
});
​
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 管道:

app.UseAuthentication();
app.UseAuthorization();

这里,咱们实现了 ASP.NET Core Web API 应用 JWT 身份验证。

创立 OrderService

咱们将须要一种专门解决订单的新服务。

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

using SecuringWebApiUsingJwtAuthentication.Entities;
using System.Collections.Generic;
using System.Threading.Tasks;
​
namespace SecuringWebApiUsingJwtAuthentication.Interfaces
{
    public interface IOrderService
    {Task<List<Order>> GetOrdersByCustomerId(int id);
    }
}

该接口包含一个办法,该办法将依据客户 Id 检索指定客户的订单。

让咱们实现这个接口。

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

using SecuringWebApiUsingJwtAuthentication.Entities;
using SecuringWebApiUsingJwtAuthentication.Interfaces;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using Microsoft.EntityFrameworkCore;
​
namespace SecuringWebApiUsingJwtAuthentication.Services
{
    public class OrderService : IOrderService
    {
        private readonly CustomersDbContext customersDbContext;
​
        public OrderService(CustomersDbContext customersDbContext)
        {this.customersDbContext = customersDbContext;}
        public async Task<List<Order>> GetOrdersByCustomerId(int id)
        {
            var orders = await customersDbContext.Orders.Where
                         (order => order.CustomerId == id).ToListAsync();
        
            return orders;
        }
    }
}

创立 OrdersController

当初咱们须要创立一个新的终结点,它将应用 Authorize 属性和 OnlyNonBlockedCustomer 策略。

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SecuringWebApiUsingJwtAuthentication.Interfaces;
​
namespace SecuringWebApiUsingJwtAuthentication.Controllers
{[Route("api/[controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        private readonly IOrderService orderService;
        public OrdersController(IOrderService orderService)
        {this.orderService = orderService;}
​
        [HttpGet()]
        [Authorize(Policy = "OnlyNonBlockedCustomer")]
        public async Task<IActionResult> Get()
        {
            var claimsIdentity = HttpContext.User.Identity as ClaimsIdentity;
            var claim = claimsIdentity.FindFirst(ClaimTypes.NameIdentifier);
            if (claim == null)
            {return Unauthorized("Invalid customer");
            }
            var orders = await orderService.GetOrdersByCustomerId(int.Parse(claim.Value));
            if (orders == null || !orders.Any())
            {return BadRequest($"No order was found");
            }
            return Ok(orders);
        }
    }
}

咱们将创立一个 GET 办法,用于检索客户的订单。

此办法将应用 Authorize 属性进行润饰,并仅为非阻塞客户定义拜访策略。

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

咱们须要在 Startup.cs 文件中蕴含 OrderService。

将上面的内容增加到 CustomerService 行上面。

services.AddScoped<IOrderService, OrderService>();

这是 Startup.cs 文件的残缺视图,须要与你的文件进行核查。

using System;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using SecuringWebApiUsingJwtAuthentication.Entities;
using SecuringWebApiUsingJwtAuthentication.Handlers;
using SecuringWebApiUsingJwtAuthentication.Helpers;
using SecuringWebApiUsingJwtAuthentication.Interfaces;
using SecuringWebApiUsingJwtAuthentication.Requirements;
using SecuringWebApiUsingJwtAuthentication.Services;
​
namespace SecuringWebApiUsingJwtAuthentication
{
    public class Startup
    {public Startup(IConfiguration configuration)
        {Configuration = configuration;}
​
        public IConfiguration Configuration {get;}
​
        // This method gets called by the runtime. 
        // Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {           
            services.AddDbContext<CustomersDbContext>
               (options => options.UseSqlServer(Configuration.GetConnectionString
               ("CustomersDbConnectionString")));
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(options =>
                    {
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            ValidateIssuer = true,
                            ValidateAudience = true,
                            ValidateIssuerSigningKey = true,
                            ValidIssuer = TokenHelper.Issuer,
                            ValidAudience = TokenHelper.Audience,
                            IssuerSigningKey = new SymmetricSecurityKey
                            (Convert.FromBase64String(TokenHelper.Secret))
                        };
                        
                    });
            services.AddAuthorization(options =>
            {
                options.AddPolicy("OnlyNonBlockedCustomer", policy => {policy.Requirements.Add(new CustomerBlockedStatusRequirement(false));
                });
            });
            services.AddSingleton<IAuthorizationHandler, CustomerBlockedStatusHandler>();
            services.AddScoped<ICustomerService, CustomerService>();
            services.AddScoped<IOrderService, OrderService>();
            services.AddControllers();}
​
        // This method gets called by the runtime. 
        // Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {if (env.IsDevelopment())
            {app.UseDeveloperExceptionPage();
            }
            app.UseHttpsRedirection();
            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {endpoints.MapControllers();
            });
        }
    }
}

通过 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 身份验证有了很好的了解。

欢送关注我的公众号——码农译站,如果你有喜爱的外文技术文章,能够通过公众号留言举荐给我。

正文完
 0