
node.js + express + docker + mysql + jwt 實現用戶管理restful api
在開始之前,我們需要準備的四樣東西:
下載并安裝了所有必需的工具后,我們需要確保 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 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 REST API 中將 Refresh Token 添加到 JWT 身份驗證。
我們將覆蓋的一些主題包含:Refresh Token、一些新的 Endpoints 功能和 JWT(JSON Web Token)。
你也可以在 YouTube 上觀看完整的視頻[2],還可以下載源代碼[3]。
這是 REST API 開發系列的第三部分,前面還有:
我將基于在章中創建的 Todo REST API 應用程序進行當前的講述。您可以通過閱讀上一篇文章并與我一起構建應用程序,或者可以從 github 下載上一篇中的源代碼。
在開始實現 Refresh Token 功能之前,讓我們先來了解一下 Refresh Token 的運行邏輯是怎樣的。
本質上,JWT token 有一個過期時間,時間越短越安全。在 JWT token 過期后,有兩種方法可以獲取新的 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 測試應用程序,測試場景如下所示:
相關鏈接:
文章轉自微信公眾號@老不懂先生
node.js + express + docker + mysql + jwt 實現用戶管理restful api
nodejs + mongodb 編寫 restful 風格博客 api
表格插件wpDataTables-將 WordPress 表與 Google Sheets API 連接
手把手教你用Python和Flask創建REST API
使用 Django 和 Django REST 框架構建 RESTful API:實現 CRUD 操作
ASP.NET Web API快速入門介紹
2024年在線市場平臺的11大最佳支付解決方案
完整指南:如何在應用程序中集成和使用ChatGPT API
選擇AI API的指南:ChatGPT、Gemini或Claude,哪一個最適合你?