在本文中,你将學習如何在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'[email protected]', _
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'[email protected]', _
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