所有文章 > API开发 > Asp.Net Core 5 REST API

Asp.Net Core 5 REST API

Asp.Net Core 5 REST API – Step by Step(一)

在本文中,我们将创建一个简单的 Asp.Net Core REST API Todo 应用程序,在其中我们可以添加、编辑、删除和查看待办事项,并且将使用 SQLite 来存储数据。

你也可以在 YouTube 上观看完整的视频[2],还可以下载源代码[3]

这是 API 开发系列的第一部分,后面还有:

  • Part 2:Asp.Net Core 5 REST API 使用 JWT 身份验证 – Step by Step
  • Part 3:Asp Net Core 5 REST API 中使用 RefreshToken 刷新 JWT – Step by Step

在开始之前,我们需要准备的四样东西:

  • Visual Studio code (https://code.visualstudio.com/)
  • Dotnet core SDK (https://dotnet.microsoft.com/download)
  • Postman (https://www.postman.com/downloads/)
  • DBeaver (https://dbeaver.io/download/)

下载并安装了所有必需的工具后,我们需要确保 dotnet SDK 已成功安装,我们需要打开终端并通过检查 dotnet 版本来检查 dotnet SDK 是否已成功安装。

打开终端并输入以下命令:

dotnet --version

现在,我们需要安装 EntityFramework 工具:

dotnet tool install --global dotnet-ef

完成后,我们需要创建我们的应用程序:

dotnet new webapi -n "TodoApp" -lang "C#" -au none

现在让我们添加需要使用的依赖包,以便可以使用 EntityFramrwork 和 SQLite:

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Tools

现在,请打开 VS Code 并检查我们的应用程序和源代码,然后,让我们构建应用程序并查看其是否可以运行:

dotnet build
dotnet run

确认可以正常运行后,我们删除由 .Net Core 框架为我们生成的默认模板代码,即删除 WeatherForcastController 和WeatherForcast 类。

接着,我们创建自己的控制器,将其命名为 TodoController

然后,我们创建第一个简单的 Action,将其命名为 TestRun,让我们开始为我们的控制器编码:

[Route("api/[controller]")] // 我们定义控制器要使用的路由
[ApiController] // 我们需要指定控制器的类型以让 .Net Core 知道
public class TodoController : ControllerBase
{
[Route("TestRun")] // 定义此 Action 的路由
[HttpGet]
public ActionResult TestRun()
{
return Ok("success");
}
}

创建完成后,我们需要对其进行测试,为了测试,我们需要执行以下操作:

dotnet build
dotnet run

应用程序运行起来后,我们可以打开 Postman 试一下看看我们得到的响应。

我们在 Postman 中创建一个新请求,并将类型设置为 GET,然后请求以下 URL

https://localhost:5001/api/todo/testrun

正如您在 TestRun 中看到的那样,我们在 Postman 中得到了 “success” 响应。

测试完之后,我们现在需要开始添加模型,在根目录中添加一个 Models 文件夹,并在其中添加一个名为 ItemData 的类。这是一个非常简单的模型,它表示我们的待办事项的列表项。

public class ItemData
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public bool Done { get; set; }
}

添加好模型后,我们需要构建 ApiDbContext。在根目录中创建一个 Data 文件夹,然后在该文件夹中创建一个名为 ApiDbContext 的新类。

public class ApiDbContext : DbContext
{
public virtual DbSet<ItemData> Items {get;set;}

public ApiDbContext(DbContextOptions<ApiDbContext> options)
: base(options)
{

}
}

然后,我们需要在 appsetting.json 中指定应用程序的连接字符串:

"ConnectionStrings": {
"DefaultConnection" : "DataSource=app.db; Cache=Shared"
}

完善 DbContext 和连接字符串后,我们需要更新 Startup 类,以便可以在应用程序中使用 Application DbContext。在我们的根目录中打开 Startup 类,然后添加以下代码:

services.AddDbContext<ApiDbContext>(options =>
options.UseSqlite(
Configuration.GetConnectionString("DefaultConnection")
));

添加好 DbContext 中间件后,我们需要添加初始化迁移来创建数据库:

dotnet ef migrations add "Initial Migrations"
dotnet ef database update

成功完成数据库更新后,我们可以看到有一个名为 Migrations 的新文件夹,它将包含 C# 脚本,该脚本将负责创建数据库及其表 Items。我们可以在根目录中看到 app.db 文件,也可以使用 SQLite 查看工具来验证表是否已成功创建,由此我们可以验证数据库是否已创建。

现在,我们已经完成了控制器的所有基础设施的搭建。现在,我们需要开始构建 TodoController 并将其连接到ApiDbContext

我们从添加获取待办事项中的所有项的方法 GetItems 开始,依次添加所有需要的方法:

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApp.Data;
using TodoApp.Models;

namespace TodoApp.Controllers
{
[Route("api/[controller]")] // api/todo
[ApiController]
public class TodoController : ControllerBase
{
private readonly ApiDbContext _context;

public TodoController(ApiDbContext context)
{
_context = context;
}

[HttpGet]
public async Task<IActionResult> GetItems()
{
var items = await _context.Items.ToListAsync();
return Ok(items);
}

[HttpPost]
public async Task<IActionResult> CreateItem(ItemData data)
{
if (ModelState.IsValid)
{
await _context.Items.AddAsync(data);
await _context.SaveChangesAsync();

return CreatedAtAction("GetItem", new { data.Id }, data);
}

return new JsonResult("Something went wrong") { StatusCode = 500 };
}

[HttpGet("{id}")]
public async Task<IActionResult> GetItem(int id)
{
var item = await _context.Items.FirstOrDefaultAsync(x => x.Id == id);

if (item == null)
return NotFound();

return Ok(item);
}

[HttpPut("{id}")]
public async Task<IActionResult> UpdateItem(int id, ItemData item)
{
if (id != item.Id)
return BadRequest();

var existItem = await _context.Items.FirstOrDefaultAsync(x => x.Id == id);

if (existItem == null)
return NotFound();

existItem.Title = item.Title;
existItem.Description = item.Description;
existItem.Done = item.Done;

// 在数据库级别实施更改
await _context.SaveChangesAsync();

return NoContent();
}

[HttpDelete("{id}")]
public async Task<IActionResult> DeleteItem(int id)
{
var existItem = await _context.Items.FirstOrDefaultAsync(x => x.Id == id);

if (existItem == null)
return NotFound();

_context.Items.Remove(existItem);
await _context.SaveChangesAsync();

return Ok(existItem);
}
}
}

然后,我们可以在 Postman 中一个一个地对它们进行测试。

最后,由于我们在创建 Web API 项目时使用的是 .Net 5,因此 Swagger 已经集成到了我们的应用程序中,要查看 Swagger 界面,可以在浏览器中导航到 http://localhost:5000/swagger/index.html。

Swagger 允许您描述 API 的结构,以便程序可以自动读取它们,而无需我们额外的工作。Swagger 能够读取 API 结构并为我们生成一个 UI,我们可以借此来改善开发体验。

Asp.Net Core 5 REST API 使用 JWT 身份验证 – Step by Step(二)

在本文中,我将向您展示如何向我们的 Asp.Net Core REST API 添加 JWT 身份验证。

我们将介绍的主题包含注册、登录功能以及如何使用 JWT (Json Web Tokens)[2]和 Bearer 身份验证。

你也可以在 YouTube 上观看完整的视频[3],还可以下载源代码[4]

这是 API 开发系列的第二部分,本系列还包含:

我们将基于上一篇文章中创建的 Todo REST API 应用程序进行当前的讲述,您可以通过阅读上一篇文章并与我一起构建应用程序,或者可以从 github 下载上一篇中的源代码。

前一篇文章中的代码准备好以后,就让我们开始本文吧。

首先,我们需要安装一些依赖包以使用身份验证:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer 
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI

然后,我们需要更新 appsettings.json,在 appsettings 中添加 JWT 的设置部分,在该设置中添加一个 JWT secret(密钥)。

"JwtConfig": {
"Secret" : "ijurkbdlhmklqacwqzdxmkkhvqowlyqa"
},

为了生成 secret,我们可以使用一个免费的 Web 工具(https://www.browserling.com/tools/random-string)来生成一个随机的 32 个字符的字符串。

我们在 appsettings 中添加完随机生成的 32 个字符的字符串后,接着需要在根目录中创建一个名为 Configuration 的新文件夹。

在这个 Configuration 文件夹中,我们将创建一个名为 JwtConfig 的新类:

public class JwtConfig
{
public string Secret { get; set; }
}

现在我们需要更新 Startup 类,在 ConfigureServices 方法内,我们需要添加以下内容,以便将 JWT 配置注入到应用程序中:

services.Configure<JwtConfig>(Configuration.GetSection("JwtConfig"));

将这些配置添加到我们的 Startup 类中,即可在 Asp.Net Core 中间件和 IoC 容器中注册配置。

下一步是在我们的 Startup 类中添加和配置身份验证,在我们的 ConfigureServices 方法中,我们需要添加以下内容:

// 在本段中,我们将配置身份验证并设置默认方案
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt => {
var key = Encoding.ASCII.GetBytes(Configuration["JwtConfig:Secret"]);

jwt.SaveToken = true;
jwt.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuerSigningKey = true, //这将使用我们在 appsettings 中添加的 secret 来验证 JWT token 的第三部分,并验证 JWT token 是由我们生成的
IssuerSigningKey = new SymmetricSecurityKey(key), //将密钥添加到我们的 JWT 加密算法中
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
RequireExpirationTime = false
};
});

services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApiDbContext>();

更新好 ConfigureServices 之后,我们需要更新 Configure 方法,添加身份验证:

app.UseAuthentication();

配置添加完成后,我们需要构建应用程序,检查是否所有的内容都可以正常构建:

dotnet build
dotnet run

下一步是更新我们的 ApiDbContext,以便使用 Asp.Net 为我们提供的身份提供程序,导航到 Data 文件夹中的ApiDbContext,然后按以下内容更新 ApiDbContext 类:

public class ApiDbContext : IdentityDbContext

通过从 IdentityDbContext 而不是 DbContext 继承,EntityFramework 将知道我们正在使用身份验证,并且将为我们构建基础设施以使用默认身份表。

要在我们的数据库中生成身份表,我们需要准备迁移脚本并运行它们。也就是说,我们需要在终端中输入并运行以下命令:

dotnet ef migrations add "Adding authentication to our Api"
dotnet ef database update

迁移完成后,我们可以使用 Dbeaver 打开数据库 app.db,我们可以看到 EntityFramework 已经为我们创建了身份表。

下一步是设置控制器并为用户构建注册流程。我们需要在 Controllers 文件夹中创建一个新的控制器,并创建对应的 DTO 类(Data Transfer Objects)。

先在根目录中的 Configuration 文件夹中添加一个名为 AuthResult 的类:

// Configuration\AuthResult.cs

public class AuthResult
{
public string Token { get; set; }
public bool Success { get; set; }
public List<string> Errors { get; set; }
}

然后我将添加一些文件夹来组织 DTOs,在 Models 文件夹中添加一个名为 DTOs 的文件夹,然后在此文件夹中创建两个子文件夹 Requests 和 Responses

我们需要添加供我们在控制器中的注册 Action 使用的 UserRegistrationDto。导航到 Models/DTO/Requests,添加一个新类 UserRegistrationDto

// Models\DTOs\Requests\UserRegistrationDto.cs

public class UserRegistrationDto
{
[Required]
public string Username { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}

添加 RegistrationResponse 响应类。

// Models\DTOs\Responses\RegistrationResponse.cs

public class RegistrationResponse : AuthResult
{

}

现在,我们需要添加用户注册控制器,在控制器文件夹中添加一个新类,命名为 AuthManagementController,并使用以下代码更新它:

// Controllers\AuthManagementController.cs

[Route("api/[controller]")] // api/authmanagement
[ApiController]
public class AuthManagementController : ControllerBase
{
private readonly UserManager<IdentityUser> _userManager;
private readonly JwtConfig _jwtConfig;

public AuthManagementController(
UserManager<IdentityUser> userManager,
IOptionsMonitor<JwtConfig> optionsMonitor)
{
_userManager = userManager;
_jwtConfig = optionsMonitor.CurrentValue;
}

[HttpPost]
[Route("Register")]
public async Task<IActionResult> Register([FromBody] UserRegistrationDto user)
{
// 检查传入请求是否有效
if(ModelState.IsValid)
{
// 检查使用相同电子邮箱的用户是否存在
var existingUser = await _userManager.FindByEmailAsync(user.Email);

if(existingUser != null)
{
return BadRequest(new RegistrationResponse()
{
Errors = new List<string>()
{
"Email already in use"
},
Success = false
});
}

var newUser = new IdentityUser() { Email = user.Email, UserName = user.Username };
var isCreated = await _userManager.CreateAsync(newUser, user.Password);
if(isCreated.Succeeded)
{
var jwtToken = GenerateJwtToken( newUser);

return Ok(new RegistrationResponse()
{
Success = true,
Token = jwtToken
});
}
else
{
return BadRequest(new RegistrationResponse()
{
Errors = isCreated.Errors.Select(x => x.Description).ToList(),
Success = false
});
}
}

return BadRequest(new RegistrationResponse()
{
Errors = new List<string>()
{
"Invalid payload"
},
Success = false
});
}

private string GenerateJwtToken(IdentityUser user)
{
//现在,是时候定义 jwt token 了,它将负责创建我们的 tokens
var jwtTokenHandler = new JwtSecurityTokenHandler();

// 从 appsettings 中获得我们的 secret
var key = Encoding.ASCII.GetBytes(_jwtConfig.Secret);

// 定义我们的 token descriptor
// 我们需要使用 claims (token 中的属性)给出关于 token 的信息,它们属于特定的用户,
// 因此,可以包含用户的 Id、名字、邮箱等。
// 好消息是,这些信息由我们的服务器和 Identity framework 生成,它们是有效且可信的。
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new []
{
new Claim("Id", user.Id),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
// Jti 用于刷新 token,我们将在下一篇中讲到
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
}),
// token 的过期时间需要缩短,并利用 refresh token 来保持用户的登录状态,
// 不过由于这只是一个演示应用,我们可以对其进行延长以适应我们当前的需求
Expires = DateTime.UtcNow.AddHours(6),
// 这里我们添加了加密算法信息,用于加密我们的 token
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};

var token = jwtTokenHandler.CreateToken(tokenDescriptor);
var jwtToken = jwtTokenHandler.WriteToken(token);

return jwtToken;
}
}

添加完注册的 Action 后,我们可以在 Postman 中对其进行测试并获得 JWT token。

接下来是创建用户登录请求:

// Models\DTOs\Requests\UserLoginRequest.cs

public class UserLoginRequest
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}

然后,我们需要在 AuthManagementController 中添加 Login 方法:

[HttpPost]
[Route("Login")]
public async Task<IActionResult> Login([FromBody] UserLoginRequest user)
{
if(ModelState.IsValid)
{
// 检查使用相同电子邮箱的用户是否存在
var existingUser = await _userManager.FindByEmailAsync(user.Email);

if(existingUser == null)
{
// 出于安全原因,我们不想透露太多关于请求失败的信息
return BadRequest(new RegistrationResponse()
{
Errors = new List<string>()
{
"Invalid login request"
},
Success = false
});
}

// 现在我们需要检查用户是否输入了正确的密码
var isCorrect = await _userManager.CheckPasswordAsync(existingUser, user.Password);

if(!isCorrect)
{
// 出于安全原因,我们不想透露太多关于请求失败的信息
return BadRequest(new RegistrationResponse()
{
Errors = new List<string>()
{
"Invalid login request"
},
Success = false
});
}

var jwtToken = GenerateJwtToken(existingUser);

return Ok(new RegistrationResponse()
{
Success = true,
Token = jwtToken
});
}

return BadRequest(new RegistrationResponse()
{
Errors = new List<string>()
{
"Invalid payload"
},
Success = false
});
}

现在,我们可以在 Postman 中对其进行测试,我们将会看到 JWT token 已经成功生成。

下一步是保护我们的控制器,需要做的就是向控制器添加 Authorize 属性。

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[Route("api/[controller]")] // api/todo
[ApiController]
public class TodoController : ControllerBase

此时,如果我们再对 Todo 进行测试,则由于未获得授权,我们将会无法执行任何请求。为了发送带授权的请求,我们需要添加带有 Bearer token 的授权 Header,以便 Asp.Net 可以验证它,并授予我们执行操作的权限。

译者注:
添加 Bearer token 请求头的方法是:在 Headers 中,添加一个名称为 Authorization 的 Header 项,值为 Bearer <token>(需将 <token> 替换为真实的 token 值)。使用 Postman 测试时,可参考 Postman 官方文档:https://learning.postman.com/docs/sending-requests/authorization/#bearer-token。

至此,我们已经完成了使用 JWT REST API 添加身份验证的功能。


Asp Net Core 5 REST API 使用 RefreshToken 刷新 JWT – Step by Step(三)

在本文中,我将向您演示如何在 Asp.Net Core REST API 中将 Refresh Token 添加到 JWT 身份验证。

我们将覆盖的一些主题包含:Refresh Token、一些新的 Endpoints 功能和 JWTJSON Web Token)。

你也可以在 YouTube 上观看完整的视频[2],还可以下载源代码[3]

这是 REST API 开发系列的第三部分,前面还有:

  • Part 1:Asp.Net Core 5 REST API – Step by Step
  • Part 2:Asp.Net Core 5 REST API 使用 JWT 身份验证 – Step by Step

我将基于在章中创建的 Todo REST API 应用程序进行当前的讲述。您可以通过阅读上一篇文章并与我一起构建应用程序,或者可以从 github 下载上一篇中的源代码。

在开始实现 Refresh Token 功能之前,让我们先来了解一下 Refresh Token 的运行逻辑是怎样的。

本质上,JWT token 有一个过期时间,时间越短越安全。在 JWT token 过期后,有两种方法可以获取新的 token:

  1. 要求用户重新登录(这不是一个好的用户体验)。
  2. 使用 Refresh Token 自动重新验证用户并生成新的 JWT token。

那么,Refresh Token 是什么呢?一个 Refresh Token 可以是任何东西,从字符串到 Guid 到任意组合,只要它是唯一的

为什么短暂生命周期的 JWT token 很重要,这是因为如果有人窃取了我们的 JWT token 并开始请求我们的服务器,那么该 token 在过期(变得不可用)之前只会持续一小段时间。获取新 token 的唯一方法是使用 Refresh Token 或登录。

另一个重点是,如果用户更改了密码,则根据之前的用户凭据生成的所有 token 会怎样呢。我们并不想使所有会话都失效,我们只需请求刷新 Token,那么将生成一个基于新凭证的新 JWT token。

另外,实现自动刷新 token 的一个好办法是,在客户端发出每个请求之前,都需要检查 token 的过期时间,如果已过期,我们就请求一个新的 token,否则就使用现有的 token 执行请求。

因此,我们将在应用程序中添加一个 Refresh Token,而不仅仅是在每次授权时都只生成一个 JWT token。

那就让我们开始吧,首先我们将更新 Startup 类,通过将 TokenValidationParameters 添加到依赖注入容器,使它在整个应用程序中可用。

var key = Encoding.ASCII.GetBytes(Configuration["JwtConfig:Secret"]);

var tokenValidationParams = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
RequireExpirationTime = false,
ClockSkew = TimeSpan.Zero
};

services.AddSingleton(tokenValidationParams);

services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwt =>
{
jwt.SaveToken = true;
jwt.TokenValidationParameters = tokenValidationParams;
});

更新完 Startup 类以后,我们需要更新 AuthManagementController 中的 GenerateJwtToken 函数,将 TokenDescriptor 的 Expires 值从之前的值更新为 30 秒(比较合理的值为 5~10 分钟,这里设置为 30 秒只是作演示用),我们需要把它指定的更短一些。

译者注:
实际使用时,可以在 appsettings.json 中为 JwtConfig 添加一个代表 token 过期时间的 ExpiryTimeFrame 配置项,对应的在 JwtConfig 类中添加一个 ExpiryTimeFrame 属性,然后赋值给 TokenDescriptor 的 Expires,这样 token 的过期时间就变得可配置了。

private string GenerateJwtToken(IdentityUser user)
{
var jwtTokenHandler = new JwtSecurityTokenHandler();

var key = Encoding.ASCII.GetBytes(_jwtConfig.Secret);

var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("Id", user.Id),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
}),
Expires = DateTime.UtcNow.AddSeconds(30), // 比较合理的值为 5~10 分钟,这里设置 30 秒只是作演示用
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};

var token = jwtTokenHandler.CreateToken(tokenDescriptor);
var jwtToken = jwtTokenHandler.WriteToken(token);

return jwtToken;
}

接下来的步骤是更新 Configuration 文件夹中的 AuthResult,我们需要为 Refresh Token 添加一个新属性:

// Configuration\AuthResult.cs

public class AuthResult
{
public string Token { get; set; }
public string RefreshToken { get; set; }
public bool Success { get; set; }
public List<string> Errors { get; set; }
}

我们将在 Models/DTOs/Requests 中添加一个名为 TokenRequest 的新类,该类负责接收稍后我们将创建的新 Endpoint 的请求参数,用于管理刷新 Token。

// Models\DTOs\Requests\TokenRequest.cs

public class TokenRequest
{
/// <summary>
/// 原 Token
/// </summary>
[Required]
public string Token { get; set; }
/// <summary>
/// Refresh Token
/// </summary>
[Required]
public string RefreshToken { get; set; }
}

下一步是在我们的 Models 文件夹中创建一个名为 RefreshToken 的新模型。

// Models\RefreshToken.cs

public class RefreshToken
{
public int Id { get; set; }
public string UserId { get; set; } // 连接到 ASP.Net Identity User Id
public string Token { get; set; } // Refresh Token
public string JwtId { get; set; } // 使用 JwtId 映射到对应的 token
public bool IsUsed { get; set; } // 如果已经使用过它,我们不想使用相同的 refresh token 生成新的 JWT token
public bool IsRevorked { get; set; } // 是否出于安全原因已将其撤销
public DateTime AddedDate { get; set; }
public DateTime ExpiryDate { get; set; } // refresh token 的生命周期很长,可以持续数月

[ForeignKey(nameof(UserId))]
public IdentityUser User {get;set;}
}

添加 RefreshToken 模型后,我们需要更新 ApiDbContext 类:

public virtual DbSet<RefreshToken> RefreshTokens { get; set; }

现在让我们为 ApiDbContext 创建数据库迁移,以便可以反映数据库中的更改:

dotnet ef migrations add "Added refresh tokens table"
dotnet ef database update

下一步是在 AuthManagementController 中创建一个新的名为 RefreshToken 的 Endpoind。需要做的第一件事是注入 TokenValidationParameters

private readonly UserManager<IdentityUser> _userManager;
private readonly JwtConfig _jwtConfig;
private readonly TokenValidationParameters _tokenValidationParams;
private readonly ApiDbContext _apiDbContext;

public AuthManagementController(
UserManager<IdentityUser> userManager,
IOptionsMonitor<JwtConfig> optionsMonitor,
TokenValidationParameters tokenValidationParams,
ApiDbContext apiDbContext)
{
_userManager = userManager;
_jwtConfig = optionsMonitor.CurrentValue;
_tokenValidationParams = tokenValidationParams;
_apiDbContext = apiDbContext;
}

注入所需的参数后,我们需要更新 GenerateJwtToken 函数以包含 Refresh Token:

private async Task<AuthResult> GenerateJwtToken(IdentityUser user)
{
var jwtTokenHandler = new JwtSecurityTokenHandler();

var key = Encoding.ASCII.GetBytes(_jwtConfig.Secret);

var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new[]
{
new Claim("Id", user.Id),
new Claim(JwtRegisteredClaimNames.Email, user.Email),
new Claim(JwtRegisteredClaimNames.Sub, user.Email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
}),
Expires = DateTime.UtcNow.AddSeconds(30), // 比较合理的值为 5~10 分钟,这里设置 30 秒只是作演示用
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};

var token = jwtTokenHandler.CreateToken(tokenDescriptor);
var jwtToken = jwtTokenHandler.WriteToken(token);

var refreshToken = new RefreshToken()
{
JwtId = token.Id,
IsUsed = false,
IsRevorked = false,
UserId = user.Id,
AddedDate = DateTime.UtcNow,
ExpiryDate = DateTime.UtcNow.AddMonths(6),
Token = RandomString(25) + Guid.NewGuid()
};

await _apiDbContext.RefreshTokens.AddAsync(refreshToken);
await _apiDbContext.SaveChangesAsync();

return new AuthResult()
{
Token = jwtToken,
Success = true,
RefreshToken = refreshToken.Token
};
}

private string RandomString(int length)
{
var random = new Random();
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(x => x[random.Next(x.Length)]).ToArray());
}

现在,让我们更新两个现有 Action 的返回值,因为我们已经更改了 GenerateJwtToken 的返回类型

Login Action:

return Ok(await GenerateJwtToken(existingUser));

Register Action:

return Ok(await GenerateJwtToken(newUser));

然后,我们可以开始构建 RefreshToken Action:

[HttpPost]
[Route("RefreshToken")]
public async Task<IActionResult> RefreshToken([FromBody] TokenRequest tokenRequest)
{
if (ModelState.IsValid)
{
var result = await VerifyAndGenerateToken(tokenRequest);

if (result == null)
{
return BadRequest(new RegistrationResponse()
{
Errors = new List<string>()
{
"Invalid tokens"
},
Success = false
});
}

return Ok(result);
}

return BadRequest(new RegistrationResponse()
{
Errors = new List<string>()
{
"Invalid payload"
},
Success = false
});
}

private async Task<AuthResult> VerifyAndGenerateToken(TokenRequest tokenRequest)
{
var jwtTokenHandler = new JwtSecurityTokenHandler();

try
{
// Validation 1 - Validation JWT token format
// 此验证功能将确保 Token 满足验证参数,并且它是一个真正的 token 而不仅仅是随机字符串
var tokenInVerification = jwtTokenHandler.ValidateToken(tokenRequest.Token, _tokenValidationParams, out var validatedToken);

// Validation 2 - Validate encryption alg
// 检查 token 是否有有效的安全算法
if (validatedToken is JwtSecurityToken jwtSecurityToken)
{
var result = jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase);

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

// Validation 3 - validate expiry date
// 验证原 token 的过期时间,得到 unix 时间戳
var utcExpiryDate = long.Parse(tokenInVerification.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Exp).Value);

var expiryDate = UnixTimeStampToDateTime(utcExpiryDate);

if (expiryDate > DateTime.UtcNow)
{
return new AuthResult()
{
Success = false,
Errors = new List<string>()
{
"Token has not yet expired"
}
};
}

// validation 4 - validate existence of the token
// 验证 refresh token 是否存在,是否是保存在数据库的 refresh token
var storedRefreshToken = await _apiDbContext.RefreshTokens.FirstOrDefaultAsync(x => x.Token == tokenRequest.RefreshToken);

if (storedRefreshToken == null)
{
return new AuthResult()
{
Success = false,
Errors = new List<string>()
{
"Refresh Token does not exist"
}
};
}

// Validation 5 - 检查存储的 RefreshToken 是否已过期
// Check the date of the saved refresh token if it has expired
if (DateTime.UtcNow > storedRefreshToken.ExpiryDate)
{
return new AuthResult()
{
Errors = new List<string>() { "Refresh Token has expired, user needs to re-login" },
Success = false
};
}

// Validation 6 - validate if used
// 验证 refresh token 是否已使用
if (storedRefreshToken.IsUsed)
{
return new AuthResult()
{
Success = false,
Errors = new List<string>()
{
"Refresh Token has been used"
}
};
}

// Validation 7 - validate if revoked
// 检查 refresh token 是否被撤销
if (storedRefreshToken.IsRevorked)
{
return new AuthResult()
{
Success = false,
Errors = new List<string>()
{
"Refresh Token has been revoked"
}
};
}

// Validation 8 - validate the id
// 这里获得原 JWT token Id
var jti = tokenInVerification.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Jti).Value;

// 根据数据库中保存的 Id 验证收到的 token 的 Id
if (storedRefreshToken.JwtId != jti)
{
return new AuthResult()
{
Success = false,
Errors = new List<string>()
{
"The token doesn't mateched the saved token"
}
};
}

// update current token
// 将该 refresh token 设置为已使用
storedRefreshToken.IsUsed = true;
_apiDbContext.RefreshTokens.Update(storedRefreshToken);
await _apiDbContext.SaveChangesAsync();

// 生成一个新的 token
var dbUser = await _userManager.FindByIdAsync(storedRefreshToken.UserId);
return await GenerateJwtToken(dbUser);
}
catch (Exception ex)
{
if (ex.Message.Contains("Lifetime validation failed. The token is expired."))
{
return new AuthResult()
{
Success = false,
Errors = new List<string>()
{
"Token has expired please re-login"
}
};
}
else
{
return new AuthResult()
{
Success = false,
Errors = new List<string>()
{
"Something went wrong."
}
};
}
}
}

private DateTime UnixTimeStampToDateTime(long unixTimeStamp)
{
var dateTimeVal = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
dateTimeVal = dateTimeVal.AddSeconds(unixTimeStamp).ToLocalTime();
return dateTimeVal;
}

最后,我们需要确保一切可以正常构建和运行。

dotnet build
dotnet run

当我们确定一切 OK 后,我们可以使用 Postman 测试应用程序,测试场景如下所示:

  • 登录,生成带有刷新令牌的 JWT 令牌 ⇒ 成功
  • 不等待令牌过期而直接尝试刷新令牌 ⇒ 失败
  • 等待 JWT 令牌过期然后请求刷新令牌 ⇒ 成功
  • 重新使用相同的刷新令牌 ⇒ 失败

相关链接:

  1. https://dev.to/moe23/asp-net-core-5-rest-api-step-by-step-2mb6 Asp.Net Core 5 Rest API Step by Step ↩︎
  2. https://youtu.be/p_wUdWshYc8 ↩︎
  3. https://github.com/mohamadlawand087/v6-RestApiNetCore5 ↩︎
  4. https://dev.to/moe23/asp-net-core-5-rest-api-authentication-with-jwt-step-by-step-140d Asp Net Core 5 Rest API Authentication with JWT Step by Step ↩︎
  5. https://youtu.be/LgpC4tYtc6Y ↩︎
  6. https://github.com/mohamadlawand087/v7-RestApiNetCoreAuthentication ↩︎

文章转自微信公众号@老不懂先生

#你可能也喜欢这些API文章!