当前位置 博文首页 > 码农译站:使用JWT创建安全的ASP.NET Core Web API

    码农译站:使用JWT创建安全的ASP.NET Core Web API

    作者:码农译站 时间:2021-01-30 16:00

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