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

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

出处:https://www.cnblogs.com/hhhnicvscs/p/14348406.html

=======================================================================================

Dotnet core使用JWT认证授权最佳实践(一)

最近,团队的小伙伴们在做项目时,需要用到JWT认证。遂根据自己的经验,整理成了这篇文章,用来帮助理清JWT认证的原理和代码编写操作。

一、JWT

JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

JWT是什么,看上面这段网上抄来的话。

关于JWT以及优缺点,网上有很多详细的说法,我这儿就不重复了。

我们只需要知道以下的事实:

在一般的系统中,我们有时候会做个用户登录。用户登录完成进到系统后,需要根据用户的权限,来控制一些功能可用,而另一些功能不可用。

在SOA/AOP架构中,做为最重要的API端,其实也需要有类似登录或认证的内容,用来区分哪些用户可以使用某个API,哪些用户不行。

同时,我们希望这个登录或类似登录的过程,只发生在一个固定位置。这样,在我们写代码时,建立好这样一个过程后,在我们后边写代码时,简单引用即可,而不需要每个API程序都开发一次认证。这个需求,其实就是OAuth的由来。

最重要的是,这样的代码写出来,显得高大上

下面进入正题。

认证这个操作,就像我们最近的日子。

首先,我们要有一个出入证,或者绿码。这个证,我们称作令牌(Token)。我们去领这个证,这个操作称为发行(Issue)。

我们拿着这个证,去到一个地方。有专人会检查这个证,这称为用户身份验证(Authentication)。验证通过放行,称为授权(Authorization),验证不通过,叫作未授权错误(Unauthorized)。

如果这个证过期了,你就需要去重新办一个证。这个过程叫做刷新(RefreshToken)。

简言之,这就是认证的全部流程。

下面,我用一个Demo项目,来逐步完成这个过程。

二、开发环境&基础项目

这个Demo的开发环境是:Mac + VS Code + Dotnet Core 3.1.2。

$ dotnet --info
.NET Core SDK (reflecting any global.json):
 Version:   3.1.201
 Commit:    b1768b4ae7

Runtime Environment:
 OS Name:     Mac OS X
 OS Version:  10.15
 OS Platform: Darwin
 RID:         osx.10.15-x64
 Base Path:   /usr/local/share/dotnet/sdk/3.1.201/

Host (useful for support):
  Version: 3.1.3
  Commit:  4a9f85e9f8

.NET Core SDKs installed:
  3.1.201 [/usr/local/share/dotnet/sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 3.1.3 [/usr/local/share/dotnet/shared/Microsoft.NETCore.App]

首先,在这个环境下建立工程:

  1. 创建Solution
% dotnet new sln -o demo
The template "Solution File" was created successfully.
  1. 用Webapi模板创建项目
cd demo
% dotnet new webapi -o demo
The template "ASP.NET Core Web API" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on demo/demo.csproj...
  Restore completed in 179.13 ms for demo/demo.csproj.

Restore succeeded.
  1. 把Demo项目加到Solution中
% dotnet sln add demo/demo.csproj
Project `demo/demo.csproj` added to the solution.
  1. 安装Swagger(这步非必须,我习惯用Swagger,不习惯用Postman)
% dotnet add package Swashbuckle.AspNetCore
log  : Restore completed in 2.75 sec for demo/demo.csproj.
  1. 安装JWT认证支持库(必须引入)
% dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
log  : Restore completed in 3.09 sec for demo/demo.csproj.

五步做完,基础项目就建完了。

看一下整个的目录结构:

% tree .
.
├── demo
│   ├── Controllers
│   │   └── WeatherForecastController.cs
│   ├── Program.cs
│   ├── Properties
│   │   └── launchSettings.json
│   ├── Startup.cs
│   ├── WeatherForecast.cs
│   ├── appsettings.Development.json
│   ├── appsettings.json
│   ├── demo.csproj
│   └── obj
│       ├── demo.csproj.nuget.dgspec.json
│       ├── demo.csproj.nuget.g.props
│       ├── demo.csproj.nuget.g.targets
│       ├── project.assets.json
│       └── project.nuget.cache
└── demo.sln

  1. 在Startup.cs中补充代码,以启用Swagger

在ConfigureServices方法中加入以下代码:

services.AddSwaggerGen(c =>
{
        c.SwaggerDoc("v1"new OpenApiInfo { Title = "Demo", Version = "V1" });

        c.AddSecurityDefinition("Bearer"new OpenApiSecurityScheme
        {
                Name = "Authorization",
                Type = SecuritySchemeType.ApiKey,
                Scheme = "Bearer",
                BearerFormat = "JWT",
                In = ParameterLocation.Header,
                Description = "",
        });

        c.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
                {
                        new OpenApiSecurityScheme
                        {
                                Reference = new OpenApiReference
                                {
                                        Type = ReferenceType.SecurityScheme,
                                        Id = "Bearer"
                                }
                        },
                        new string[] {}
                }
        });
});

在Configure方法中加入以下代码

app.UseSwagger();
app.UseSwaggerUI(c =>
{
        c.SwaggerEndpoint("/swagger/v1/swagger.json""Demo V1");
});

关于Swagger的详细配置,这里不做说明,留着以后写。

三、签发Token

签发Token是认证的第一步。

用户进到系统,在验证用户帐号密码后,需要根据用户的数据,把Token返回给用户。

这个过程其实跟认证没什么关系,只是一个普通的API功能。

  1. 工程下加一个目录DTOModels,创建一个LoginRequestDTO的类,用于定义API的输入参数。
using System;

namespace demo.DTOModels
{
    public class LoginRequestDTO
    {

        public string username { get; set; }
        public string password { get; set; }
    }
}
  1. 创建一个控制器AuthenticationController,并在控制器里创建一个API方法RequestToken。
using Microsoft.AspNetCore.Mvc;
using demo.DTOModels;

namespace demo.Controllers
{
    public class AuthenticationController : ControllerBase
    {
        [HttpPost, Route("requesttoken")]
        public ActionResult RequestToken([FromBody] LoginRequestDTO request)
        
{
              //这儿待完善
            return Ok();
        }
    }
}
  1. 生成JWT Token需要预设一些参数。我们在appsetting.json里先设置好。
{
  "Logging": {
    "LogLevel": {
      "Default""Information",
      "Microsoft""Warning",
      "Microsoft.Hosting.Lifetime""Information"
    }
  },
  "AllowedHosts""*",
  "tokenParameter": {
    "secret""123456123456123456",
    "issuer""WangPlus",
    "accessExpiration"120,
    "refreshExpiration"1440
  }
}

这里,tokenParameter节是我们设置的参数。一般来说,是这几个:

secret: JWT加密的密钥。现在主流用SHA256加密,需要256位以上的密钥,unicode是16个字符以上,尽量复杂一些。密钥泄露,Token就会被破解,所以,你懂的。

issuer: 签发人的名称,如果没人注意,你可以把大名写在上面。

accessExpiration: Token的有效分钟数。过了这个时间,这个Token会过期。

refreshExpiration: refreshToken的有效分钟数。过了这个时间,用户需要重新登录。

Token过期后,可以让用户重新登录认证拿Token。但这个方式会比较Low。高大上的方式是签发Token的时候,同时也签发一个refreshToken给用户。用户Token过期后,可以拿refreshToken去申请新的Token,同时刷新refreshToken。如果用户长时间未使用系统,refreshToken也过期了,才让用户重新登录认证。

refreshToken可以用JWT生成,也可以自己生成,不影响认证。

  1. 建一个Models目录,创建一个映射tokenParameter的类。这个类不是必须,只是为了写着方便。不想这样写,也可以直接读配置,再转成数据。
using System;

namespace demo.Models
{
    public class tokenParameter
    {

        public string Secret { get; set; }
        public string Issuer { get; set; }
        public int AccessExpiration { get; set; }
        public int RefreshExpiration { get; set; }
    }
}
  1. 在前边建好的API - RequestToken中,完成Token和refreshToken的生成和返回。
using Microsoft.AspNetCore.Mvc;
using demo.DTOModels;
using Microsoft.Extensions.Configuration;
using System;
using System.Text;
using demo.Models;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.IdentityModel.Tokens.Jwt;

namespace demo.Controllers
{
    public class AuthenticationController : ControllerBase
    {
        private tokenParameter _tokenParameter = new tokenParameter();
        public AuthenticationController()
        
{
            var config = new ConfigurationBuilder()
                .SetBasePath(AppContext.BaseDirectory)
                .AddJsonFile("appsettings.json")
                .Build();

            _tokenParameter = config.GetSection("tokenParameter").Get<tokenParameter>();
        }

        [HttpPost, Route("requestToken")]
        public ActionResult RequestToken([FromBody] LoginRequestDTO request)
        
{
            //这儿在做用户的帐号密码校验。我这儿略过了。
            if (request.username == null && request.password == null)
                return BadRequest("Invalid Request");

              //生成Token和RefreshToken
            var token = GenUserToken(request.username, "testUser");
            var refreshToken = "123456";

            return Ok(new[] { token, refreshToken });
        }


          //这儿是真正的生成Token代码
          private string GenUserToken(string username, string role)
        
{
            var claims = new[]
            {
                new Claim(ClaimTypes.Name, username),
                new Claim(ClaimTypes.Role, role),
            };

            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenParameter.Secret));
            var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var jwtToken = new JwtSecurityToken(_tokenParameter.Issuer, null, claims, expires: DateTime.UtcNow.AddMinutes(_tokenParameter.AccessExpiration), signingCredentials: credentials);

            var token = new JwtSecurityTokenHandler().WriteToken(jwtToken);

            return token;
        }
    }
}

这个类里,验证帐号密码的代码我略过了。还有,refreshToken给了一个固定串。真实项目这儿就按需要做就好。

(未完待续)

 

出处:https://www.cnblogs.com/tiger-wang/p/12892383.html

=======================================================================================

Dotnet core使用JWT认证授权最佳实践(二)

最近,团队的小伙伴们在做项目时,需要用到JWT认证。遂根据自己的经验,整理成了这篇文章,用来帮助理清JWT认证的原理和代码编写操作。

第一部分:Dotnet core使用JWT认证授权最佳实践(一)

(接上文)

  1. 测试运行
% dotnet run

等程序运行起来后,在浏览器输入:http://localhost:5000/swagger/,会进到Swagger的API界面。选择requestToken,点击按钮”Try it out“->”Execute“,可以看到运行结果:

["eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoic3RyaW5nIiwiZXhwIjoxNTg5MzgxMzQ4LCJpc3MiOiJXYW5nUGx1cyJ9.ojGuWUk9i2Vp5qu3s2UZSLC64Sm95Cao2eGF3GDVvec","123456"]

好吧,不要在意这个返回的格式。返回的两个串中,第一个就是Token,第二个是refreshToken。

到这儿,我们成功拿到了用户的Token。

    为了防止不提供原网址的转载,特在这里加上原文链接:https://www.cnblogs.com/tiger-wang/p/12894021.html

四、Token认证

拿到Token后,我们就可以进行认证操作了。

既然是认证,那应该在每个API上进行。所以,认证的过程不会放到控制器里,而应该以MiddleWare的方式,放到主流程中。

这个MiddleWare,Microsoft.AspNetCore.Authentication.JwtBearer库已经帮我们做好了。我们只需要配置就好。

  1. 在Startup.cs中,ConfigureServices方法里,添加以下内容
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(option =>
{
    option.RequireHttpsMetadata = false;
        option.SaveToken = true;

        var token = Configuration.GetSection("tokenParameter").Get<tokenParameter>();

        option.TokenValidationParameters = new TokenValidationParameters
        {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(token.Secret)),
                ValidIssuer = token.Issuer,
                ValidateIssuer = true,
                ValidateAudience = false,
        };
});

这里面,有几个参数需要注意:

RequireHttpsMetadata: 限定认证操作是否必须通过https来做,这个要跟随项目在生产环境中的运行情况来定。如果WebServer是我前文15分钟从零开始搭建支持10w+用户的生产环境(三)中介绍的Jexus,采用对外https,对内http的方式,那这儿可以设为false。

SaveToken: 决定Token在认证完成后,是否需要保存到上下文里并向后传。这个设置也要看应用。我们Token生成后,用户的相关信息已经包含在里面了。API里如果有涉及用户的操作,按理可以不用往API里传相关用户的参数。一方面不安全,另一方面代码也不好看。这时就可以把这个参数设为True,然后API从上下文中直接取用户信息。

  1. 在Startup.cs里,Configure方法中,打开认证
app.UseAuthentication();
app.UseAuthorization();

这两步完成,我们就完成的认证的开发工具。

用别人的轮子还是很爽的,虽然轮子的挑选工作很复杂很费力。

  1. 设置API认证。

在这个Demo里,我们选代码生成时给的WeatherForecastController下的Get方法来测试。

在方法前边,我们加上Authorize:

[HttpGet]
[Authorize]
public IEnumerable<WeatherForecast> Get()
...
  1. 测试运行。

启动程序,跟上一章的方式一样。

程序运行后,打开:http://localhost:5000/swagger/,进入WeatherForecast,点”Try it out“->”Execute“,我们会得到一个401 - Error: Unauthorized的返回,因为我们没有做认证。

下面测试做认证后的访问。

先去requestToken拿一个Token(refreshToken这章不用),在前边加“Bearer ”,拼成一个串

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoic3RyaW5nIiwiZXhwIjoxNTg5MzgxMzQ4LCJpc3MiOiJXYW5nUGx1cyJ9.ojGuWUk9i2Vp5qu3s2UZSLC64Sm95Cao2eGF3GDVvec

要注意,Bearer后边要跟一个空格。这个串的格式是:Bearer + 空格 + Token。

在页面的右上角,有一个“Authorize”,点进去,在Value输入框中粘贴上面拼好的串,然后点按钮“Authorize”,保存认证信息。

下面进入WeatherForecast,点”Try it out“->”Execute“,这时候,我们就能拿到正确的返回数据。

五、扩展:用户角色认证

在上一章中,我们实现了用户的认证。但这个认证有个不漂亮的地方:用户只简单的被认证系统分成了通过认证的和不通过认证的。

在实际项目中,我们有时候会有这样的需求:对于某个API,我们希望只允许具有某种角色权限的用户去访问。

下面,我们对这个项目进行小量的修改,以完成这个需求。

  1. 在给用户签发Token的过程中,加入用户的角色数据。

在AuthenticationController的RequestToken中,我们构建了一个用户的Claims:

var claims = new[]
{
    new Claim(ClaimTypes.Name,request.username),
};

就是这儿。我们在这儿加入用户的角色:

var claims = new[]
{
    new Claim(ClaimTypes.Name,request.username),
      new Claim(ClaimTypes.Role, "testUser"),
};

实际应用中,这个角色的名称,可以根据需要,从用户系统中拿来。

在这个Demo里,就直接写成个字符串了。就是说,有一个角色,叫testUser。

  1. 给API增加认证的角色要求
[HttpGet]
[Authorize(Roles="testUser")]
public IEnumerable<WeatherForecast> Get()
...

在这里,这个Roles="testUser"里的testUser,就是这个方法授权所对应的角色名称。

  1. 测试运行

按正常的步骤,取Token,拼串,保存认证信息,然后去运行WeatherForecast,API能正常返回。

我们可以把代码中的testUser改成别的字符串进行测试,会返回403 - Error: Forbidden错误。

增加角色认证成功。

六、刷新Token

Token过期后,就需要刷新。

当然我们可以把Token设成永远不过期,但这不是个安全的做法。还可以在Token过期后重新请求一个新Token,但这样做会显得Low。

赏心悦目的做法是:用refreshToken来刷新Token。设置refreshToken的过期时间长于Token。Token过期后,让用户提交Token和refreshToken到服务器,服务器验证Token是否合法,并从中提取用户信息,根据用户信息和refreshToken核验是否匹配。如果匹配,就重新生成Token给用户。

至于refreshToken的过期时长,和是否需要在刷新Token时也刷新refreshToken,就看心情了,没有固定的做法。我自己的项目中,Token是2小时过期,refreshToken是24小时过期。在Token刷新时,如果refreshToken的过期时间少于6小时,则刷新refreshToken。供参考。

下面,按这个方式,做一下刷新Token。

  1. 在DTOModels下建一个RefreshTokenDTO,用作API的输入参数
using System;

namespace demo.DTOModels
{
    public class RefreshTokenDTO
    {

        public string Token { get; set; } 
        public string refreshToken { get; set; }
    }
  1. 在AuthenticationController里,创建一个RefreshToken的API,并补齐验证代码
[HttpPost, Route("refreshToken")]
public ActionResult RefreshToken([FromBody]RefreshTokenDTO request)
{
    if(request.Token == null && request.refreshToken == null)
        return BadRequest("Invalid Request");

        //这儿是验证Token的代码
    var handler = new JwtSecurityTokenHandler();
    try
    {
        ClaimsPrincipal claim = handler.ValidateToken(request.Token, new TokenValidationParameters{
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(_tokenParameter.Secret)),
            ValidateIssuer = false,
            ValidateAudience = false,
            ValidateLifetime = false,
        }, out SecurityToken securityToken);

        var username = claim.Identity.Name;

        //这儿是生成Token的代码
        var token = GenUserToken(username, "testUser");

        var refreshToken = "654321";

        return Ok(new[] { token, refreshToken });
    }
    catch(Exception)
    {
        return BadRequest("Invalid Request");
    }
}

这样,Token刷新就完成了。可以用生成Token运行测试,能正常认证通过。

  1. 单独说一下refreshToken

refreshToken,名义上是为了刷新Token,实际上用处主要是给用户重新登录做计时。refreshToken过期了,用户就必须重新登录。就是这么个作用。要不然,Token自己刷新岂不更好?

refreshToken可以采用跟Token一样的生成方式。但是,我们也看到,Token生成出来的串就很长,如果refreshToken也那样生成,那就也会是一个很长的串。这样会加大前端到API的传输量。因此,这不算是一个好主意。

一般来说,refreshToken会换一种生成方式。唯一序列、Hash,都是可以选择的,可以减少很多传输。

至于持久化和过期,依托数据库就好了。

七、彩蛋

最后,送大家一个彩蛋。

在生成Token时,我们把过期时间设置成少于五分钟的时长,比方3分钟。但这时,实测会发现,Token的过期失效了。

为什么呢?

TokenValidationParameters有一个属性叫ClockSkew,这个参数有个默认值是TimeSpan.FromMinutes(5)。

这个参数的意义是:考虑到各个服务器之间的时间不一定完全同步,系统给了个5分钟的误差时间。

这个误差时间导致的结果是:少于五分钟的过期时间,会在实际认证检查时被忽略。

这个情况,Microsoft上有N多人在讨论,可以自己去查。

所以,当Token的过期小于5分钟时,想要让认证对这个时间生效,可以把这个值设为TimeSpan.Zero。

option.TokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(token.Secret)),
    ValidIssuer = token.Issuer,
    ValidateIssuer = true,
    ValidateAudience = false,
    ClockSkew = TimeSpan.Zero,        //就是这一行
};

我把上面的代码,传到了Github上,需要了可以拉下来直接测试。

代码地址:https://github.com/humornif/Demo-Code/tree/master/0007/demo

(全文完)

 

出处:https://www.cnblogs.com/tiger-wang/p/12894021.html

posted on 2021-02-03 10:48  jack_Meng  阅读(290)  评论(0编辑  收藏  举报

导航