使用JWT建立安全的ASP.NET Core Web API

在本文中,你將學習如何在ASP.NET Core Web API中使用JWT身份驗證。我將在編寫代碼時逐步簡化。咱們將構建兩個終結點,一個用於客戶登陸,另外一個用於獲取客戶訂單。這些api將鏈接到在本地機器上運行的SQL Server Express數據庫。算法

JWT是什麼?數據庫

JWT或JSON Web Token基本上是格式化令牌的一種方式,令牌表示一種通過編碼的數據結構,該數據結構具備緊湊、url安全、安全且自包含特色。json

JWT身份驗證是api和客戶端之間進行通訊的一種標準方式,所以雙方能夠確保發送/接收的數據是可信的和可驗證的。windows

JWT應該由服務器發出,並使用加密的安全密鑰對其進行數字簽名,以便確保任何攻擊者都沒法篡改在令牌內發送的有效payload及模擬合法用戶。api

JWT結構包括三個部分,用點隔開,每一個部分都是一個base64 url編碼的字符串,JSON格式:瀏覽器

Header.Payload.Signature:安全

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiIxIiwicm9sZSI6IkFjY291bnQgTWFuYWdlciIsIm5iZiI6MTYwNDAxMDE4NSwiZXhwIjoxNjA0MDExMDg1LCJpYXQiOjE2MDQwMTAxODV9.XJLeLeUIlOZQjYyQ2JT3iZ-AsXtBoQ9eI1tEtOkpyj8

Header:表示用於對祕鑰進行哈希的算法(例如HMACSHA256)服務器

Payload:在客戶端和API之間傳輸的數據或聲明數據結構

Signature:Header和Payload鏈接的哈希app

由於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=Home\\SQLEXPRESS;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身份驗證有了很好的理解。

歡迎關注個人公衆號——碼農譯站,若是你有喜歡的外文技術文章,能夠經過公衆號留言推薦給我。

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

相關文章
相關標籤/搜索