在ASP.NET Core Web API 6中应用JWT访问令牌和刷新令牌

目录

访问令牌与刷新令牌

开始教程

数据库准备

项目创建

实体框架核心和DbContext

实体

RefreshToken

Task

User

DbContext

PasswordHashHelper

实现JWT身份验证

用于构建访问令牌和刷新令牌的令牌助手

请求和响应

LoginRequest

RefreshTokenRequest

SignupRequest

TaskRequest

BaseResponse

DeleteTaskResponse

GetTasksResponse

LogoutResponse

SaveTaskResponse

SignupResponse

TaskResponse

TokenResponse

ValidateRefreshTokenResponse

接口

ITokenService

IUserService

ITasksInterface

服务

TokenService

UserService

TaskService

控制器

BaseApiController

UsersController

TasksController

在Postman上进行测试

refresh_token端点

恶意或已注销的用户

总结


在本教程中,我们将构建一个简单、安全且可靠的RESTful API项目,以正确验证用户并授权他们对API执行操作。

在本教程中,我们将学习如何在ASP.NET Core Web API 6中应用JWT访问令牌和刷新令牌。我们将构建一个简单、安全且可靠的RESTful API项目来正确验证用户并授权他们对API执行操作。

我们将使用最新最好的Visual Studio 2022社区版来构建我们的ASP.NET Core Web API,使用最快的.NET标准框架,即.NET 6C# 10

好消息是VS 2022捆绑了最新版本的.NETC#,因此您不必担心搜索和安装它们中的任何一个。因此,如果您还没有VS 2022,请确保下载并安装Visual Studio 2022,以便我们可以开始使用我们的教程在ASP.NET Core Web API 6中应用JWT访问令牌和刷新令牌。

在之前的教程中,我们学习了如何使用JWT身份验证来保护ASP.NET Core Web API,但这只是使用访问令牌。现在,这是一个从头开始构建的新教程,解释了使用访问令牌和刷新令牌时有关JWT身份验证的所有内容。

访问令牌与刷新令牌

Authorization标头中提供访问令牌时,我们使用访问令牌授予用户访问服务器上某些资源的适当授权。访问令牌通常是短期和签名的,对于JWT令牌,这将包括签名、声明、标头。

另一方面,刷新令牌通常是只能用于刷新访问令牌的引用。此类令牌通常保存在后端存储中,可用于撤销用户的访问权限,例如,不再有资格访问这些资源或恶意用户窃取访问令牌的情况。

在这种情况下,您只需删除这些设备的刷新令牌,因此一旦他们的访问令牌过期,他们将无法使用撤销的刷新令牌更新(刷新)它,因为他们曾经有效的刷新令牌不再有效,他们将无法再访问您的资源。因此,用户将在应用程序或Web中注销,因此他们必须重新登录并再次完成通常的登录过程。

现在已经足够僵硬的文本了,让我们直接开始构建我们的API,这些API将使用NET 6中的ASP.NET Core Web API使用访问令牌和刷新令牌来实现JWT身份验证。

开始教程

我们将构建一个简单的任务管理系统,允许经过身份验证的用户管理自己的任务。这只是任务管理系统的简单基本表示。

随意从Githubfork并为您的个人项目构建它。

数据库准备

在我的大部分教程中,我使用SQL Server Express创建所需的数据库和表。因此,请确保下载并安装最新版本的SQL Server Management StudioSQL Server Express

安装两者后,打开SQL Server Management Studio并连接到安装了SQL Server Express的本地计算机:

在对象资源管理器中,右键单击数据库并选择创建新数据库,为其命名TasksDb,如下所示:

然后运行以下命令来创建表并使用本教程所需的数据填充它:

USE [TasksDb]
GO
/****** Object:  Table [dbo].[RefreshToken]    Script Date: 1/18/2022 6:10:48 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[RefreshToken](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[UserId] [int] NOT NULL,
	[TokenHash] [nvarchar](1000) NOT NULL,
	[TokenSalt] [nvarchar](50) NOT NULL,
	[TS] [smalldatetime] NOT NULL,
	[ExpiryDate] [smalldatetime] NOT NULL,
 CONSTRAINT [PK_RefreshToken] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
 ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[Task]    Script Date: 1/18/2022 6:10:48 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Task](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[UserId] [int] NOT NULL,
	[Name] [nvarchar](100) NOT NULL,
	[IsCompleted] [bit] NOT NULL,
	[TS] [smalldatetime] NOT NULL,
 CONSTRAINT [PK_Task] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
 ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
/****** Object:  Table [dbo].[User]    Script Date: 1/18/2022 6:10:48 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[User](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Email] [nvarchar](50) NOT NULL,
	[Password] [nvarchar](255) NOT NULL,
	[PasswordSalt] [nvarchar](255) NOT NULL,
	[FirstName] [nvarchar](255) NOT NULL,
	[LastName] [nvarchar](255) NOT NULL,
	[TS] [smalldatetime] NOT NULL,
	[Active] [bit] NOT NULL,
 CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, _
 ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[Task] ON 
GO
INSERT [dbo].[Task] ([Id], [UserId], [Name], [IsCompleted], [TS]) _
VALUES (1, 1, N'Blog about Access Token and Refresh Token Authentication', _
1, CAST(N'2022-01-14T00:00:00' AS SmallDateTime))
GO
INSERT [dbo].[Task] ([Id], [UserId], [Name], [IsCompleted], [TS]) _
VALUES (3, 1, N'Vaccum the House', 0, CAST(N'2022-01-14T00:00:00' AS SmallDateTime))
GO
INSERT [dbo].[Task] ([Id], [UserId], [Name], [IsCompleted], [TS]) _
VALUES (4, 1, N'Farmers Market Shopping', 0, CAST(N'2022-01-14T00:00:00' AS SmallDateTime))
GO
INSERT [dbo].[Task] ([Id], [UserId], [Name], [IsCompleted], [TS]) _
VALUES (5, 1, N'Practice Juggling', 0, CAST(N'2022-01-15T00:00:00' AS SmallDateTime))
GO
SET IDENTITY_INSERT [dbo].[Task] OFF
GO
SET IDENTITY_INSERT [dbo].[User] ON 
GO
INSERT [dbo].[User] ([Id], [Email], [Password], [PasswordSalt], [FirstName], [LastName], _
[TS], [Active]) VALUES (1, N'coding@codingsonata.com', _
N'miLgvYoSVrotOON6/lRp8ACrrbAxCPCmsrsy355x/DI=', _
N'L5hziA8V93SNGTlYdz+meS0B6DPzB3IwsRhDf1vO1GM=', N'Coding', N'Sonata', _
CAST(N'2022-01-14T00:00:00' AS SmallDateTime), 1)
GO
INSERT [dbo].[User] ([Id], [Email], [Password], [PasswordSalt], [FirstName], [LastName], _
[TS], [Active]) VALUES (2, N'test@codingsonata.com', _
N'Fm7/SI9lYAFglzWXLD5oLz0cuq00MQmPkzDZ+nDZNmc=', _
N'kjgIDmRKgUbbWypCOOUHuxlQzZAszdEKw358ds4Xyc4=', N'test', N'postman', _
CAST(N'2022-01-16T14:23:00' AS SmallDateTime), 1)
GO
SET IDENTITY_INSERT [dbo].[User] OFF
GO
ALTER TABLE [dbo].[RefreshToken]  WITH CHECK ADD  CONSTRAINT [FK_RefreshToken_User] _
FOREIGN KEY([UserId])
REFERENCES [dbo].[User] ([Id])
GO
ALTER TABLE [dbo].[RefreshToken] CHECK CONSTRAINT [FK_RefreshToken_User]
GO
ALTER TABLE [dbo].[Task]  WITH CHECK ADD  CONSTRAINT [FK_Task_User] FOREIGN KEY([UserId])
REFERENCES [dbo].[User] ([Id])
GO
ALTER TABLE [dbo].[Task] CHECK CONSTRAINT [FK_Task_User]
GO

项目创建

打开Visual Studio 2022,并创建一个ASP.NET Core Web API类型的新项目:

给它起一个名字,如TasksApi

然后选择.NET 6.0并创建项目:

VS完成项目的初始化后,按F5对模板项目进行初始运行,以确保它工作正常。

现在,让我们从模板项目中删除一些不需要的类。从解决方案资源管理器,删除WeatherForecastControllerWeatherForecast文件。

实体框架核心和DbContext

让我们为EF CoreEF Core SQL添加Nuget包:

实体

现在让我们创建将通过EF Core DbContext类绑定到数据库表的所需实体。

我们将创建三个映射到Tasks数据库的实体。

RefreshToken

// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable

namespace TasksApi
{
    public partial class RefreshToken
    {
        public int Id { get; set; }
        public int UserId { get; set; }
        public string TokenHash { get; set; }
        public string TokenSalt { get; set; }
        public DateTime Ts { get; set; }
        public DateTime ExpiryDate { get; set; }
        public virtual User User { get; set; }
    }
}

Task

// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable
using System;
using System.Collections.Generic;

namespace TasksApi
{
    public partial class Task
    {
        public int Id { get; set; }
        public int UserId { get; set; }
        public string Name { get; set; }
        public bool IsCompleted { get; set; }
        public DateTime Ts { get; set; }

        public virtual User User { get; set; }
    }
}

User

// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable
using System;
using System.Collections.Generic;

namespace TasksApi
{
    public partial class User
    {
        public User()
        {
            RefreshTokens = new HashSet<RefreshToken>();
            Tasks = new HashSet<Task>();
        }

        public int Id { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }
        public string PasswordSalt { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime Ts { get; set; }
        public bool Active { get; set; }

        public virtual ICollection<RefreshToken> RefreshTokens { get; set; }
        public virtual ICollection<Task> Tasks { get; set; }
    }
}

DbContext

现在让我们添加将从EF CoreDbContext类继承的TasksDbContext类:

// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable
using Microsoft.EntityFrameworkCore;

namespace TasksApi
{
    public partial class TasksDbContext : DbContext
    {
        public TasksDbContext()
        {
        }

        public TasksDbContext(DbContextOptions<TasksDbContext> options)
            : base(options)
        {
        }

        public virtual DbSet<RefreshToken> RefreshTokens { get; set; }
        public virtual DbSet<Task> Tasks { get; set; }
        public virtual DbSet<User> Users { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<RefreshToken>(entity =>
            {
               
                entity.Property(e => e.ExpiryDate).HasColumnType("smalldatetime");

                entity.Property(e => e.TokenHash)
                    .IsRequired()
                    .HasMaxLength(1000);

                entity.Property(e => e.TokenSalt)
                    .IsRequired()
                    .HasMaxLength(1000);

                entity.Property(e => e.Ts)
                    .HasColumnType("smalldatetime")
                    .HasColumnName("TS");

                entity.HasOne(d => d.User)
                    .WithMany(p => p.RefreshTokens)
                    .HasForeignKey(d => d.UserId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_RefreshToken_User");

                entity.ToTable("RefreshToken");
            });

            modelBuilder.Entity<Task>(entity =>
            {               
                entity.Property(e => e.Name)
                    .IsRequired()
                    .HasMaxLength(100);

                entity.Property(e => e.Ts)
                    .HasColumnType("smalldatetime")
                    .HasColumnName("TS");

                entity.HasOne(d => d.User)
                    .WithMany(p => p.Tasks)
                    .HasForeignKey(d => d.UserId)
                    .OnDelete(DeleteBehavior.ClientSetNull)
                    .HasConstraintName("FK_Task_User");

                entity.ToTable("Task");
            });

            modelBuilder.Entity<User>(entity =>
            {
                entity.Property(e => e.Email)
                    .IsRequired()
                    .HasMaxLength(50);

                entity.Property(e => e.FirstName)
                    .IsRequired()
                    .HasMaxLength(255);

                entity.Property(e => e.LastName)
                    .IsRequired()
                    .HasMaxLength(255);

                entity.Property(e => e.Password)
                    .IsRequired()
                    .HasMaxLength(255);

                entity.Property(e => e.PasswordSalt)
                    .IsRequired()
                    .HasMaxLength(255);

                entity.Property(e => e.Ts)
                    .HasColumnType("smalldatetime")
                    .HasColumnName("TS");

                entity.ToTable("User");
            });

            OnModelCreatingPartial(modelBuilder);
        }

        partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
    }
}

在您的program.cs文件中,在调用builder.build()之前添加以下内容:

builder.Services.AddDbContext<TasksDbContext>(options => options.UseSqlServer
(builder.Configuration.GetConnectionString("TasksDbConnectionString")));

然后在您的appsettings.json中,确保包含数据库的连接string

{
  "ConnectionStrings": {
    "TasksDbConnectionString": "Server=Home\\SQLEXPRESS;Database=TasksDb;
                                Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

注意:我使用了出色的扩展EF Core Power Tools,它以一种神奇的方式将整个数据库结构和关系转换为整洁和适当的DbContext实体和配置。

您可以从Extensions选项卡 -> Manage Extensions of your Visual Studio 2022安装它

如果您遵循设计优先模型先构建数据库,然后再构建EF Core映射,我强烈建议您使用这个快速可靠的工具来执行此类操作,从而提高您的工作效率并减少手动创建实体和配置可能引入的错误。

PasswordHashHelper

为了将密码保存在数据库中,我们需要使用安全哈希HMAC 256和来自256位大小的安全随机字节的盐,这样我们就可以保护有价值的用户密码免受那些讨厌的潜伏小偷的侵害!

using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using System.Security.Cryptography;

namespace TasksApi.Helpers
{
    public class PasswordHelper
    {
        public static byte[] GetSecureSalt()
        {
            // Starting .NET 6, the Class RNGCryptoServiceProvider is obsolete,
            // so now we have to use the RandomNumberGenerator Class 
            // to generate a secure random number bytes

            return RandomNumberGenerator.GetBytes(32);
        }
        public static string HashUsingPbkdf2(string password, byte[] salt)
        {
            byte[] derivedKey = KeyDerivation.Pbkdf2
            (password, salt, KeyDerivationPrf.HMACSHA256, iterationCount: 100000, 32);

            return Convert.ToBase64String(derivedKey);
        }
    }
}

我们还将使用这些辅助方法以散列格式将刷新令牌与其关联的盐一起保存在数据库中。

实现JWT身份验证

让我们添加所需的JWT承载包:

用于构建访问令牌和刷新令牌的令牌助手

现在让我们添加TokenHelper,其中将包括两种方法来生成基于JWT的访问令牌,另一种方法来生成基于32字节的刷新令牌:

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;

namespace TasksApi.Helpers
{
    public class TokenHelper
    {
        public const string Issuer = "http://codingsonata.com";
        public const string Audience = "http://codingsonata.com";
        public const string Secret = 
        "p0GXO6VuVZLRPef0tyO9jCqK4uZufDa6LP4n8Gj+8hQPB30f94pFiECAnPeMi5N6VT3/uscoGH7+zJrv4AuuPg==";
        public static async Task<string> GenerateAccessToken(int userId)
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Convert.FromBase64String(Secret);

            var claimsIdentity = new ClaimsIdentity(new[] {
                new Claim(ClaimTypes.NameIdentifier, userId.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 securityToken = tokenHandler.CreateToken(tokenDescriptor);

            return await System.Threading.Tasks.Task.Run(() => 
                                          tokenHandler.WriteToken(securityToken));
        }
        public static async Task<string> GenerateRefreshToken()
        {
            var secureRandomBytes = new byte[32];

            using var randomNumberGenerator = RandomNumberGenerator.Create();
            await System.Threading.Tasks.Task.Run(() => 
                         randomNumberGenerator.GetBytes(secureRandomBytes));

            var refreshToken = Convert.ToBase64String(secureRandomBytes);
            return refreshToken;
        }
    }
}

现在让我们确保在program.cs文件中将所需的身份验证和授权中间件添加到管道中:

builder.build方法之前添加以下内容:

builder.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))
            };
        });

builder.Services.AddAuthorization();

然后在该app.run方法之前,确保应用程序将使用这两个中间件来验证和授权您的用户:

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

请求和响应

始终建议您接受和返回结构化对象而不是单独的数据,这就是为什么我们将准备一些RequestResponse类,我们将在整个API中使用它们:

让我们添加以下请求类:

LoginRequest

namespace TasksApi.Requests
{
    public class LoginRequest
    {
        public string Email { get; set; }
        public string Password { get; set; }
    }
}

RefreshTokenRequest

namespace TasksApi.Requests
{
    public class RefreshTokenRequest
    {
        public int UserId { get; set; }
        public string RefreshToken { get; set; }
    }
}

SignupRequest

using System.ComponentModel.DataAnnotations;

namespace TasksApi.Requests
{
    public class SignupRequest
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }
        [Required]
        public string Password { get; set; }
        [Required]
        public string ConfirmPassword { get; set; }
        [Required]
        public string FirstName { get; set; }
        [Required]
        public string LastName { get; set; }
        [Required]
        public DateTime Ts { get; set; }
    }
}

TaskRequest

namespace TasksApi.Requests
{
    public class TaskRequest
    {
        public string Name { get; set; }
        public bool IsCompleted { get; set; }
        public DateTime Ts { get; set; }
    }
}

现在让我们添加响应类,这些类将用于为调用APIUI客户端返回结构化响应:

BaseResponse

这将使用一个基类,以便其他响应类可以继承并扩展它们的属性:

using System.Text.Json.Serialization;

namespace TasksApi.Responses
{
    public abstract class BaseResponse
    {
        [JsonIgnore()]
        public bool Success { get; set; }
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
        public string ErrorCode { get; set; }
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
        public string Error { get; set; }
    }
}

DeleteTaskResponse

using System.Text.Json.Serialization;

namespace TasksApi.Responses
{
    public class DeleteTaskResponse : BaseResponse
    {
        [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
        public int TaskId { get; set; }
    }
}

GetTasksResponse

namespace TasksApi.Responses
{
    public class GetTasksResponse : BaseResponse
    {
        public List<Task> Tasks { get; set; }
    }
}

LogoutResponse

namespace TasksApi.Responses
{
    public class LogoutResponse : BaseResponse
    {
    }
}

SaveTaskResponse

namespace TasksApi.Responses
{
    public class SaveTaskResponse : BaseResponse
    {
        public Task Task { get; set; }
    }
}

SignupResponse

namespace TasksApi.Responses
{
    public class SignupResponse : BaseResponse
    {
        public string Email { get; set; }
    }
}

TaskResponse

namespace TasksApi.Responses
{
    public class TaskResponse
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsCompleted { get; set; }
        public DateTime Ts { get; set; }
    }
}

TokenResponse

namespace TasksApi.Responses
{
    public class TokenResponse: BaseResponse
    {
        public string AccessToken { get; set; }
        public string RefreshToken { get; set; }
       
    }
}

ValidateRefreshTokenResponse

namespace TasksApi.Responses
{
    public class ValidateRefreshTokenResponse : BaseResponse
    {
        public int UserId { get; set; }
    }
}

接口

我们将定义三个将在服务中实现的接口。接口是控制器需要用来处理相关业务逻辑和数据库调用的抽象,每个接口都将在运行时注入的服务中实现。

这是一种非常有用的策略(或设计模式),可以使您的API松散耦合且易于测试。

ITokenService

using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Interfaces
{
    public interface ITokenService
    {
        Task<Tuple<string, string>> GenerateTokensAsync(int userId);
        Task<ValidateRefreshTokenResponse> ValidateRefreshTokenAsync
                                           (RefreshTokenRequest refreshTokenRequest);
        Task<bool> RemoveRefreshTokenAsync(User user);
    }
}

IUserService

using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Interfaces
{
    public interface IUserService
    {
        Task<TokenResponse> LoginAsync(LoginRequest loginRequest);
        Task<SignupResponse> SignupAsync(SignupRequest signupRequest);
        Task<LogoutResponse> LogoutAsync(int userId);
    }
}

ITasksInterface

using TasksApi.Responses;

namespace TasksApi.Interfaces
{
    public interface ITaskService
    {
        Task<GetTasksResponse> GetTasks(int userId);
        Task<SaveTaskResponse> SaveTask(Task task);
        Task<DeleteTaskResponse> DeleteTask(int taskId, int userId);
    }
}

服务

服务充当你的控制器和你的DbContext之间的中间层,它还包括控制器不应该关心的任何与业务相关的逻辑。服务实现接口。

我们将添加三个服务:

TokenService

这将包括生成令牌、验证和删除刷新令牌的方法:

using Microsoft.EntityFrameworkCore;
using TasksApi.Helpers;
using TasksApi.Interfaces;
using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Services
{
    public class TokenService : ITokenService
    {        
        private readonly TasksDbContext tasksDbContext;

        public TokenService(TasksDbContext tasksDbContext)
        {
            this.tasksDbContext = tasksDbContext;
        }

        public async Task<Tuple<string, string>> GenerateTokensAsync(int userId)
        {
            var accessToken = await TokenHelper.GenerateAccessToken(userId);
            var refreshToken = await TokenHelper.GenerateRefreshToken();

            var userRecord = await tasksDbContext.Users.Include
                             (o => o.RefreshTokens).FirstOrDefaultAsync(e => e.Id == userId);

            if (userRecord == null)
            {
                return null;
            }

            var salt = PasswordHelper.GetSecureSalt();

            var refreshTokenHashed = PasswordHelper.HashUsingPbkdf2(refreshToken, salt);

            if (userRecord.RefreshTokens != null && userRecord.RefreshTokens.Any())
            {
                await RemoveRefreshTokenAsync(userRecord);
            }
            userRecord.RefreshTokens?.Add(new RefreshToken
            {
                ExpiryDate = DateTime.Now.AddDays(30),
                Ts = DateTime.Now,
                UserId = userId,
                TokenHash = refreshTokenHashed,
                TokenSalt = Convert.ToBase64String(salt)
            });

            await tasksDbContext.SaveChangesAsync();

            var token = new Tuple<string, string>(accessToken, refreshToken);

            return token;
        }

        public async Task<bool> RemoveRefreshTokenAsync(User user)
        {
            var userRecord = await tasksDbContext.Users.Include
                (o => o.RefreshTokens).FirstOrDefaultAsync(e => e.Id == user.Id);

            if (userRecord == null)
            {
                return false;
            }

            if (userRecord.RefreshTokens != null && userRecord.RefreshTokens.Any())
            {
                var currentRefreshToken = userRecord.RefreshTokens.First();

                tasksDbContext.RefreshTokens.Remove(currentRefreshToken);
            }

            return false;
        }

        public async Task<ValidateRefreshTokenResponse> ValidateRefreshTokenAsync
                     (RefreshTokenRequest refreshTokenRequest)
        {
            var refreshToken = await tasksDbContext.RefreshTokens.FirstOrDefaultAsync
                               (o => o.UserId == refreshTokenRequest.UserId);

            var response = new ValidateRefreshTokenResponse();
            if (refreshToken == null)
            {
                response.Success = false;
                response.Error = "Invalid session or user is already logged out";
                response.ErrorCode = "R02";
                return response;
            }

            var refreshTokenToValidateHash = PasswordHelper.HashUsingPbkdf2
                                             (refreshTokenRequest.RefreshToken, 
                                             Convert.FromBase64String(refreshToken.TokenSalt));

            if (refreshToken.TokenHash != refreshTokenToValidateHash)
            {
                response.Success = false;
                response.Error = "Invalid refresh token";
                response.ErrorCode = "R03";
                return response;
            }
          
            if (refreshToken.ExpiryDate < DateTime.Now)
            {
                response.Success = false;
                response.Error = "Refresh token has expired";
                response.ErrorCode = "R04";
                return response;
            }

            response.Success = true;
            response.UserId = refreshToken.UserId;

            return response;
        }
    }
}

UserService

这将包括与登录、注销和注册相关的方法:

using Microsoft.EntityFrameworkCore;
using TasksApi.Helpers;
using TasksApi.Interfaces;
using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Services
{
    public class UserService : IUserService
    {
        private readonly TasksDbContext tasksDbContext;
        private readonly ITokenService tokenService;

        public UserService(TasksDbContext tasksDbContext, ITokenService tokenService)
        {
            this.tasksDbContext = tasksDbContext;
            this.tokenService = tokenService;
        }

        public async Task<TokenResponse> LoginAsync(LoginRequest loginRequest)
        {
            var user = tasksDbContext.Users.SingleOrDefault
                       (user => user.Active && user.Email == loginRequest.Email);

            if (user == null)
            {
                return new TokenResponse
                {
                    Success = false,
                    Error = "Email not found",
                    ErrorCode = "L02"
                };
            }
            var passwordHash = PasswordHelper.HashUsingPbkdf2
            (loginRequest.Password, Convert.FromBase64String(user.PasswordSalt));

            if (user.Password != passwordHash)
            {
                return new TokenResponse
                {
                    Success = false,
                    Error = "Invalid Password",
                    ErrorCode = "L03"
                };
            }

            var token = await System.Threading.Tasks.Task.Run(() => 
                        tokenService.GenerateTokensAsync(user.Id));

            return new TokenResponse
            {
                Success = true,
                AccessToken = token.Item1,
                RefreshToken = token.Item2
            };
        }

        public async Task<LogoutResponse> LogoutAsync(int userId)
        {
            var refreshToken = await tasksDbContext.RefreshTokens.FirstOrDefaultAsync
                               (o => o.UserId == userId);

            if (refreshToken == null)
            {
                return new LogoutResponse { Success = true };
            }

            tasksDbContext.RefreshTokens.Remove(refreshToken);

            var saveResponse = await tasksDbContext.SaveChangesAsync();

            if (saveResponse >= 0)
            {
                return new LogoutResponse { Success = true };
            }

            return new LogoutResponse { Success = false, 
                                        Error = "Unable to logout user", ErrorCode = "L04" };
        }

        public async Task<SignupResponse> SignupAsync(SignupRequest signupRequest)
        {
            var existingUser = await tasksDbContext.Users.SingleOrDefaultAsync
                               (user => user.Email == signupRequest.Email);

            if (existingUser != null)
            {
                return new SignupResponse
                {
                    Success = false,
                    Error = "User already exists with the same email",
                    ErrorCode = "S02"
                };
            }

            if (signupRequest.Password != signupRequest.ConfirmPassword) {
                return new SignupResponse
                {
                    Success = false,
                    Error = "Password and confirm password do not match",
                    ErrorCode = "S03"
                };
            }

            if (signupRequest.Password.Length <= 7) // This can be more complicated than 
               // only length, you can check on alphanumeric and or special characters
            {
                return new SignupResponse
                {
                    Success = false,
                    Error = "Password is weak",
                    ErrorCode = "S04"
                };
            }

            var salt = PasswordHelper.GetSecureSalt();
            var passwordHash = PasswordHelper.HashUsingPbkdf2(signupRequest.Password, salt);

            var user = new User
            {
                Email = signupRequest.Email,
                Password = passwordHash,
                PasswordSalt = Convert.ToBase64String(salt),
                FirstName = signupRequest.FirstName,
                LastName = signupRequest.LastName,
                Ts = signupRequest.Ts,
                Active = true // You can save is false and send confirmation email 
                // to the user, then once the user confirms the email you can make it true
            };

            await tasksDbContext.Users.AddAsync(user);

            var saveResponse = await tasksDbContext.SaveChangesAsync();

            if (saveResponse >= 0)
            {
                return new SignupResponse { Success = true, Email = user.Email };
            }

            return new SignupResponse
            {
                Success = false,
                Error = "Unable to save the user",
                ErrorCode = "S05"
            };
        }
    }
}

TaskService

这包括添加、删除和获取任务的方法:

using Microsoft.EntityFrameworkCore;
using TasksApi.Interfaces;
using TasksApi.Responses;

namespace TasksApi.Services
{
    public class TaskService : ITaskService
    {
        private readonly TasksDbContext tasksDbContext;

        public TaskService(TasksDbContext tasksDbContext)
        {
            this.tasksDbContext = tasksDbContext;
        }

        public async Task<DeleteTaskResponse> DeleteTask(int taskId, int userId)
        {
            var task = await tasksDbContext.Tasks.FindAsync(taskId);

            if (task == null)
            {
                return new DeleteTaskResponse
                {
                    Success = false,
                    Error = "Task not found",
                    ErrorCode = "T01"
                };
            }

            if (task.UserId != userId)
            {
                return new DeleteTaskResponse
                {
                    Success = false,
                    Error = "You don't have access to delete this task",
                    ErrorCode = "T02"
                };
            }

            tasksDbContext.Tasks.Remove(task);

            var saveResponse = await tasksDbContext.SaveChangesAsync();

            if (saveResponse >= 0)
            {
                return new DeleteTaskResponse
                {
                    Success = true,
                    TaskId = task.Id
                };
            }

            return new DeleteTaskResponse
            {
                Success = false,
                Error = "Unable to delete task",
                ErrorCode = "T03"
            };
        }

        public async Task<GetTasksResponse> GetTasks(int userId)
        {
            var tasks = await tasksDbContext.Tasks.Where
                        (o => o.UserId == userId).ToListAsync();

            if (tasks.Count == 0)
            {
                return new GetTasksResponse
                { 
                    Success = false, 
                    Error = "No tasks found for this user", 
                    ErrorCode = "T04"
                };
            }

            return new GetTasksResponse { Success = true, Tasks = tasks };
        }

        public async Task<SaveTaskResponse> SaveTask(Task task)
        {
            await tasksDbContext.Tasks.AddAsync(task);

            var saveResponse = await tasksDbContext.SaveChangesAsync();
            
            if (saveResponse >= 0)
            {
                return new SaveTaskResponse
                {
                    Success = true,
                    Task = task
                };
            }
            return new SaveTaskResponse
            {
                Success = false,
                Error = "Unable to save task",
                ErrorCode = "T05"
            };
        }
    }
}

现在,一旦你添加了这些接口和任务,让我们确保我们在项目的构建器管道中配置它们:

builder.Services.AddTransient<ITokenService, TokenService>();
builder.Services.AddTransient<IUserService, UserService>();
builder.Services.AddTransient<ITaskService, TaskService>();

控制器

现在是API的最后一部分,即构建用户访问后端资源的端点:

首先,我们将创建一个新的Controller来继承ControllerBase和在它内部,我们将有一个小的方法和属性来检索登录UserId,无论何时提供访问令牌,都来自基于JWT的访问令牌声明:

所以让我们添加一个API Controller,如下所示:

BaseApiController

using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

namespace TasksApi.Controllers
{
    public class BaseApiController : ControllerBase
    {
        protected int UserID => int.Parse(FindClaim(ClaimTypes.NameIdentifier));
        private string FindClaim(string claimName)
        {
            var claimsIdentity = HttpContext.User.Identity as ClaimsIdentity;
            var claim = claimsIdentity.FindFirst(claimName);

            if (claim == null)
            {
                return null;
            }
            return claim.Value;
        }
    }
}

现在我们可以创建将从BaseApiController继承的控制器。

UsersController

让我们从UsersController开始,它将包括四个方法:loginlogoutsignuprefresh访问令牌:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TasksApi.Interfaces;
using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class UsersController : BaseApiController
    {
        private readonly IUserService userService;
        private readonly ITokenService tokenService;

        public UsersController(IUserService userService, ITokenService tokenService)
        {
            this.userService = userService;
            this.tokenService = tokenService;
        }

        [HttpPost]
        [Route("login")]
        public async Task<IActionResult> Login(LoginRequest loginRequest)
        {
            if (loginRequest == null || string.IsNullOrEmpty(loginRequest.Email) || 
                string.IsNullOrEmpty(loginRequest.Password))
            {
                return BadRequest(new TokenResponse
                {
                    Error = "Missing login details",
                    ErrorCode = "L01"
                });
            }

            var loginResponse = await userService.LoginAsync(loginRequest);

            if (!loginResponse.Success)
            {
                return Unauthorized(new
                {
                    loginResponse.ErrorCode,
                    loginResponse.Error
                });
            }

            return Ok(loginResponse);
        }

        [HttpPost]
        [Route("refresh_token")]
        public async Task<IActionResult> RefreshToken(RefreshTokenRequest refreshTokenRequest)
        {
            if (refreshTokenRequest == null || string.IsNullOrEmpty
            (refreshTokenRequest.RefreshToken) || refreshTokenRequest.UserId == 0)
            {
                return BadRequest(new TokenResponse
                {
                    Error = "Missing refresh token details",
                    ErrorCode = "R01"
                });
            }

            var validateRefreshTokenResponse = 
                await tokenService.ValidateRefreshTokenAsync(refreshTokenRequest);

            if (!validateRefreshTokenResponse.Success)
            {
                return UnprocessableEntity(validateRefreshTokenResponse);
            }

            var tokenResponse = await tokenService.GenerateTokensAsync
                                (validateRefreshTokenResponse.UserId);

            return Ok(new { AccessToken = tokenResponse.Item1, 
                            Refreshtoken = tokenResponse.Item2 });
        }

        [HttpPost]
        [Route("signup")]
        public async Task<IActionResult> Signup(SignupRequest signupRequest)
        {
            if (!ModelState.IsValid)
            {
                var errors = ModelState.Values.SelectMany
                             (x => x.Errors.Select(c => c.ErrorMessage)).ToList();
                if (errors.Any())
                {
                    return BadRequest(new TokenResponse
                    {
                        Error = $"{string.Join(",", errors)}",
                        ErrorCode = "S01"
                    });
                }
            }
         
            var signupResponse = await userService.SignupAsync(signupRequest);

            if (!signupResponse.Success)
            {
                return UnprocessableEntity(signupResponse);
            }

            return Ok(signupResponse.Email);
        }

        [Authorize]
        [HttpPost]
        [Route("logout")]
        public async Task<IActionResult> Logout()
        {
            var logout = await userService.LogoutAsync(UserID);

            if (!logout.Success)
            {
                return UnprocessableEntity(logout);
            }

            return Ok();
        }
    }
}

注意上面只有注销端点有Authorize装饰,这是因为我们知道用户只要登录就可以注销,这意味着他有一个有效的访问令牌和刷新令牌。

TasksController

这包括允许用户执行任务相关操作的所有端点,例如获取所有用户的任务、保存和删除该用户的任务。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TasksApi.Interfaces;
using TasksApi.Requests;
using TasksApi.Responses;

namespace TasksApi.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class TasksController : BaseApiController
    {
        private readonly ITaskService taskService;

        public TasksController(ITaskService taskService)
        {
            this.taskService = taskService;
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            var getTasksResponse = await taskService.GetTasks(UserID);

            if (!getTasksResponse.Success)
            {
                return UnprocessableEntity(getTasksResponse);
            }
            
            var tasksResponse = getTasksResponse.Tasks.ConvertAll(o => 
            new TaskResponse { Id = o.Id, IsCompleted = o.IsCompleted, 
                               Name = o.Name, Ts = o.Ts });

            return Ok(tasksResponse);
        }

        [HttpPost]
        public async Task<IActionResult> Post(TaskRequest taskRequest)
        {
            var task = new Task { IsCompleted = taskRequest.IsCompleted, 
            Ts = taskRequest.Ts, Name = taskRequest.Name, UserId = UserID };

            var saveTaskResponse = await taskService.SaveTask(task);

            if (!saveTaskResponse.Success)
            {
                return UnprocessableEntity(saveTaskResponse);
            }

            var taskResponse = new TaskResponse { Id = saveTaskResponse.Task.Id, 
            IsCompleted = saveTaskResponse.Task.IsCompleted, 
            Name = saveTaskResponse.Task.Name, Ts = saveTaskResponse.Task.Ts };
            
            return Ok(taskResponse);
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> Delete(int id)
        {
            var deleteTaskResponse = await taskService.DeleteTask(id, UserID);
            if (!deleteTaskResponse.Success)
            {
                return UnprocessableEntity(deleteTaskResponse);
            }

            return Ok(deleteTaskResponse.TaskId);
        }
    }
}

请注意,该[Authorize]属性正在装饰整体Controller,因为所有这些操作都需要具有有效访问令牌的经过身份验证的用户。

现在我们完成了开发部分。按F5并检查浏览器是否为你的UI显示Swagger API

Postman上进行测试

现在是测试我们整个工作的QA部分,以确保一切正常并符合要求。

当然,请确保您安装并打开了最新版本的Postman

让我们创建一个新集合并将其命名为Tasks Api

我们需要测试的第一件事是登录端点,因为我们已经在数据库中插入了一些测试用户(包括在教程开头的脚本部分中)。

让我们尝试使电子邮件无效并查看结果:

现在让我们测试该signup 方法:

看一下数据库User表:

注意第3条记录,密码从未以纯格式保存,并且随机盐与它相关联。

访问令牌的持续时间通常很短,为1015分钟,一旦过期,您必须使用刷新令牌静默刷新访问令牌,其持续时间要长得多,例如10天或3周,并且这些令牌是在时间上滑动的,所以每当你想刷新和访问令牌时,你可以使用下面的端点来生成一对新的令牌。

refresh_token端点

现在让我们测试刷新令牌端点。在通过任何授权的API调用过期后,您将需要此端点来刷新用户的访问令牌,如下所示:

您将收到401响应,因为访问令牌不再有效,您必须使用第一次登录时拥有的刷新令牌请求新的访问令牌。

让我们尝试刷新令牌:

如果我们尝试刷新之前使用的同一个令牌,它不会起作用,只是因为刷新令牌触发同时生成新的访问令牌和新的刷新令牌,所以之前的刷新令牌将失效(从数据库上的RefreshToken表中删除)。

现在让我们注销用户:

恶意或已注销的用户

现在让我们试试这个场景,一个有效用户退出系统,但顺便说一下,一个恶意用户已经获得了该用户的刷新令牌(以某种方式),并尝试刷新该用户的令牌,以便他可以获得未经授权的访问,我们的API将通过为恶意用户返回以下响应来保护我们的有效用户。

这样,恶意用户就无法访问有效用户的数据。

现在让我们为用户执行获取任务

让我们尝试添加一个新任务

现在让我们删除一个任务

总结

今天,我们学习了如何使用SQL Server Express从数据库开始构建一个小型简单的任务管理系统,将其与.NET 6中的ASP.NET Core Web APIVisual Studio 2022中的C# 10连接。我们使用EF映射数据库Core(在强大的EF Core Power Tools 的慷慨帮助下),然后使用JWTBearer Nuget包,我们设法在API项目上设置和实施基于JWT的身份验证,同时应用刷新令牌以使其更加实用用户无需每1015分钟重新执行一次登录过程即可获得API的身份验证和授权。

您可以在我的GitHub帐户中找到本教程的源代码。

https://www.codeproject.com/Articles/5325297/Apply-JWT-Access-Tokens-and-Refresh-Tokens-in-ASP

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ASP Core API JWT是一种安全认证协议,用于在ASP Core API实现用户身份验证和授权管理。JWT代表JSON Web Token,是一种基于JSON的开放标准,可用于将声明作为JSON对象安全地传输。这种协议通常用于基于RESTful API应用程序,允许某个HTTP请求访问资源或操作,只要它包含有效的JWT。 ASP Core提供了一种功能丰富的身份验证和授权系统,可以轻松地使用JWT来保护APIJWT可以在用户登录成功后生成,并在每个HTTP请求发送给服务器。服务器可以通过验证JWT来验证用户的身份和授权。JWT有关用户的信息,例如用户ID、角色、权限等,可以在服务器端使用。 ASP Core已经包含了JWT验证的间件,程序员只需配置来指定验证规则,就可以轻松地在ASP Core API实现JWT身份验证和授权管理。此外,ASP Core还支持通过JWT来检查对API资源的访问,以确保只有具有特定角色/权限的用户可以访问这些资源。 总之,ASP Core API JWT提供了一种安全、高效和灵活的身份验证机制,是现代Web应用程序所必需的。它可以为应用程序提供可扩展性和安全性,并帮助程序员构建更加强大的Web应用程序。 ### 回答2: ASP Core 是一个开源的框架,用于构建跨平台的 Web 应用程序。API 用于处理网络请求并返回响应。JWT(JSON Web Tokens)是一种用于在网络应用程序之间传输信息的安全方式,它基于 JSON 格式。ASP Core API 可以使用 JWT 来验证和保护 API访问。 在 ASP Core ,使用 JWT 可以提供安全性,特别是在通过 API 传输敏感信息时。为了使用 JWT,我们需要编写一个服务,该服务将用于验证和签署 JWT。这个服务是通过依赖注入来实现的。 一旦创建了 JWT 服务,我们可以在 API 控制器使用它来验证用户凭据,并授权他们访问 API 端点。JWT 服务将验证 JWT 的有效性,以确保它没有被篡改或过期。如果 JWT 是有效的,服务将返回一个 Claim,这个 Claim 将用于授权用户访问请求 API 端点的权限。 使用 JWT 还可以提高应用程序的性能。由于 JWT 是已签名的,服务器不需要在每个请求API 用户进行身份验证。相反,它只需要验证 JWT,并从获取用户凭据。这使服务器可以处理更多的请求,并减少身份验证来的开销。 综上所述,ASP Core API 使用 JWT 可以提供更好的 API 访问安全性和性能。在编写 API 时,务必确保 JWT 包含足够的信息来验证请求,并限制访问 API 的权限。 ### 回答3: ASP Core API是一种框架,用于创建高性能、跨平台、轻量级的Web服务。其JWT是JSON Web Tokens的缩写,它是一种数字签名的令牌,可以用于身份验证和授权。 在ASP Core API,使用JWT可以提供一种简单而安全的身份验证和授权方式。我们可以在API上使用JWT身份验证,以便在请求发送之前验证用户身份,并授权其对资源的访问权限。 使用JWT需要在ASP Core API安装Microsoft.AspNetCore.Authentication.JwtBearer包。通过该包,可以配置JWT验证的规则和设置JSON Web Key (JWK)。 在API的控制器上,我们可以使用[Authorize]特性来限制访问,以确保只有授权用户才能访问资源。可以使用角色、策略或声明来进一步控制访问权限。 ASP Core APIJWT的结合可以提供一种安全可靠的Web服务解决方案,适用于各类应用程序,包括移动应用程序、Web应用程序和单页应用程序等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值