天天看点

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

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

作为全栈 .NET 开发人员,保护 API 是一项关键任务。利用 JWT(JSON Web 令牌)进行身份验证和授权是一种常见且有效的策略。本文将指导你在 ASP.NET Core Web API 中实现 JWT 访问令牌和刷新令牌。

为什么要使用 JWT?

JWT 令牌是紧凑的 URL 安全令牌,易于在各方之间转移。它们是自包含的,这意味着它们自身内部携带信息,从而减少了对服务器端会话存储的需求。

设置项目

首先,创建一个新的 ASP.NET Core Web API 项目:

dotnet new webapi -n JwtAuthDemo 
cd JwtAuthDemo
           

添加必要的 NuGet 包:

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer 
dotnet add package Microsoft.IdentityModel.Tokens 
dotnet add package System.IdentityModel.Tokens.Jwt
           

配置 JWT 身份验证

在 中,添加 JWT 设置:appsettings.json

{
"JwtSettings": {
"SecretKey": "your_secret_key",
"Issuer": "your_issuer",
"Audience": "your_audience",
"AccessTokenExpirationMinutes": 30,
"RefreshTokenExpirationDays": 7
 }
}
           

更新以配置 JWT 身份验证:Program.cs

var builder = WebApplication.CreateBuilder(args);

// Load JWT settings
var jwtSettings = builder.Configuration.GetSection("JwtSettings").Get<JwtSettings>();
var secretKey = Encoding.UTF8.GetBytes(jwtSettings.SecretKey);

builder.Services.AddAuthentication(options =>
{
 options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
 options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
 options.TokenValidationParameters = new TokenValidationParameters
 {
 ValidateIssuer = true,
 ValidateAudience = true,
 ValidateIssuerSigningKey = true,
 ValidateLifetime = true,
 ValidIssuer = jwtSettings.Issuer,
 ValidAudience = jwtSettings.Audience,
 IssuerSigningKey = new SymmetricSecurityKey(secretKey)
 };
});

builder.Services.AddAuthorization();

var app = builder.Build();

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

app.MapControllers();

app.Run();
           

创建 JWT 令牌服务

创建一个新类来处理令牌的创建和验证:JwtTokenService

public class JwtTokenService
{
private readonly JwtSettings _jwtSettings;

public JwtTokenService(IOptions<JwtSettings> jwtSettings)
 {
 _jwtSettings = jwtSettings.Value;
 }

public string GenerateAccessToken(IEnumerable<Claim> claims)
 {
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

var token = new JwtSecurityToken(
 issuer: _jwtSettings.Issuer,
 audience: _jwtSettings.Audience,
 claims: claims,
 expires: DateTime.Now.AddMinutes(_jwtSettings.AccessTokenExpirationMinutes),
 signingCredentials: creds);

return new JwtSecurityTokenHandler().WriteToken(token);
 }

public string GenerateRefreshToken()
 {
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
 {
 rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
 }
 }
}
           

实现身份验证端点

创建一个新控制器来处理登录和令牌刷新:AuthController

[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly JwtTokenService _jwtTokenService;

public AuthController(JwtTokenService jwtTokenService)
 {
 _jwtTokenService = jwtTokenService;
 }

 [HttpPost("login")]
public IActionResult Login([FromBody] LoginRequest request)
 {
// Assuming the user is authenticated successfully
var claims = new[]
 {
new Claim(ClaimTypes.Name, request.Username),
// Add other claims as needed
 };

var accessToken = _jwtTokenService.GenerateAccessToken(claims);
var refreshToken = _jwtTokenService.GenerateRefreshToken();

// Save or update the refresh token in the database

return Ok(new { AccessToken = accessToken, RefreshToken = refreshToken });
 }

 [HttpPost("refresh")]
public IActionResult Refresh([FromBody] TokenRequest request)
 {
// Validate the refresh token and generate a new access token
// This should include checking the refresh token against the stored value in the database

var claims = new[]
 {
new Claim(ClaimTypes.Name, "username") // Replace with actual username from the refresh token
 };

var newAccessToken = _jwtTokenService.GenerateAccessToken(claims);
           

刷新令牌终结点

在 中,我们需要实现刷新令牌逻辑。这通常涉及验证刷新令牌,确保其未过期,然后生成新的访问令牌。以下是您如何做到的:AuthController

[HttpPost("refresh")]
public IActionResult Refresh([FromBody] TokenRequest request)
{
// Validate the refresh token (retrieve the stored refresh token from the database)
var storedRefreshToken = GetStoredRefreshToken(request.RefreshToken);

if (storedRefreshToken ==  || storedRefreshToken.ExpirationDate < DateTime.UtcNow)
 {
return Unauthorized("Invalid or expired refresh token.");
 }

// Assuming you have the username or user ID stored with the refresh token
var claims = new[]
 {
new Claim(ClaimTypes.Name, storedRefreshToken.Username)
// Add other claims as needed
 };

var newAccessToken = _jwtTokenService.GenerateAccessToken(claims);
var newRefreshToken = _jwtTokenService.GenerateRefreshToken();

// Update the stored refresh token
 storedRefreshToken.Token = newRefreshToken;
 storedRefreshToken.ExpirationDate = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays);
SaveRefreshToken(storedRefreshToken);

return Ok(new { AccessToken = newAccessToken, RefreshToken = newRefreshToken });
}
           

模型

为您的请求和设置创建必要的模型。例如, , , 和 :LoginRequestTokenRequestJwtSettings

public class LoginRequest 
{ 
public string Username { get; set; } 
public string Password { get; set; } 
} 

public class TokenRequest 
{ 
public string AccessToken { get; set; } 
public string RefreshToken { get; set; } 
} 

public class JwtSettings 
{ 
public string SecretKey { get; set; } 
public string Issuer { get; set; } 
public string Audience { get; set; } 
public int AccessTokenExpirationMinutes { get; set; } 
public int RefreshTokenExpirationDays { get; set; } 
}
           

存储和验证刷新令牌

为简单起见,我们假设您有一种方法可以从数据库中存储和检索刷新令牌。下面是使用假设数据访问层的基本示例:

public class RefreshToken
{
public string Token { get; set; }
public string Username { get; set; }
public DateTime ExpirationDate { get; set; }
}

public RefreshToken GetStoredRefreshToken(string refreshToken)
{
// Implement your logic to retrieve the refresh token from the database
// For example, using Entity Framework Core:
// return _context.RefreshTokens.SingleOrDefault(rt => rt.Token == refreshToken);
return ;
}

public void SaveRefreshToken(RefreshToken refreshToken)
{
// Implement your logic to save the refresh token to the database
// For example, using Entity Framework Core:
// _context.RefreshTokens.Update(refreshToken);
// _context.SaveChanges();
}
           

总结

有了这些组件,就可以在 ASP.NET Core Web API 中为基于 JWT 的身份验证提供可靠的设置。以下是我们所涵盖内容的简要概述:

  1. 项目设置:创建了新的 ASP.NET Core Web API 项目,并添加了必要的 NuGet 包。
  2. JWT 配置:在 和 中配置了 JWT 设置。appsettings.jsonProgram.cs
  3. 令牌服务:用于生成和验证令牌。JwtTokenService
  4. 身份验证终结点:使用登录和令牌刷新终结点创建。AuthController
  5. 模型和存储:定义了请求的模型,并实现了用于存储和检索刷新令牌的方法。

通过执行这些步骤,您可以确保您的 API 是安全的,并且由于刷新令牌机制,用户可以维护其会话,而无需不断重新进行身份验证。此设置不仅可以增强安全性,还可以通过提供对受保护资源的无缝访问来改善用户体验。

保护您的端点

配置 JWT 身份验证后,您需要保护 API 端点,以确保只有经过身份验证的用户才能访问它们。您可以使用该属性来保护控制器或特定操作。[Authorize]

例如,要保护整个:WeatherForecastController

[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
// Your actions here
}
           

处理令牌过期

JWT 是有过期时间的,一旦过期,客户端需要使用 refresh token 来获取新的访问 Token。正确处理令牌过期对于维护安全性和用户体验至关重要。

下面是一个示例,说明如何在客户端处理令牌过期(例如,在前端应用程序中使用 JavaScript/TypeScript):

async function fetchWithAuth(url, options = {}) {
let response = await fetch(url, options);

if (response.status === 401) {
// Token might be expired, try to refresh it
const refreshResponse = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
 },
body: JSON.stringify({
accessToken: localStorage.getItem('accessToken'),
refreshToken: localStorage.getItem('refreshToken')
 })
 });

if (refreshResponse.ok) {
const { accessToken, refreshToken } = await refreshResponse.json();
 localStorage.setItem('accessToken', accessToken);
 localStorage.setItem('refreshToken', refreshToken);

// Retry the original request
 options.headers = {
 ...options.headers,
'Authorization': `Bearer ${accessToken}`
 };
 response = await fetch(url, options);
 }
 }

return response;
}
           

管理刷新令牌的最佳实践

  1. 安全地存储刷新令牌:刷新令牌是敏感的,应安全存储。最好使用仅限 HTTP 的 cookie 来存储刷新令牌,因为它们不太容易受到 XSS 攻击。
  2. 轮换刷新令牌:每次使用刷新令牌时,都会生成一对新的访问和刷新令牌。这限制了被盗刷新令牌的生命周期。
  3. 已用令牌黑名单:保留无效令牌列表,以防止重复使用。这可以使用数据库或内存中存储来实现。
  4. 限制刷新尝试次数:实施一种机制来限制可用于防止滥用的刷新令牌的次数。

使用仅限 HTTP 的 Cookie 的示例

以下示例说明了如何在 HTTP 中使用仅限 HTTP 的 Cookie 发出和处理刷新令牌:AuthController

[HttpPost("login")]
public IActionResult Login([FromBody] LoginRequest request)
{
// Assuming the user is authenticated successfully
var claims = new[]
 {
new Claim(ClaimTypes.Name, request.Username),
// Add other claims as needed
 };

var accessToken = _jwtTokenService.GenerateAccessToken(claims);
var refreshToken = _jwtTokenService.GenerateRefreshToken();

// Save or update the refresh token in the database
SaveRefreshToken(new RefreshToken
 {
 Token = refreshToken,
 Username = request.Username,
 ExpirationDate = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays)
 });

// Set the refresh token as an HTTP-only cookie
 Response.Cookies.Append("refreshToken", refreshToken, new CookieOptions
 {
 HttpOnly = true,
 Secure = true, // Set to true in production
 Expires = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays)
 });

return Ok(new { AccessToken = accessToken });
}

[HttpPost("refresh")]
public IActionResult Refresh()
{
var refreshToken = Request.Cookies["refreshToken"];
if (string.IsOrEmpty(refreshToken))
 {
return Unauthorized("Refresh token is missing.");
 }

var storedRefreshToken = GetStoredRefreshToken(refreshToken);
if (storedRefreshToken ==  || storedRefreshToken.ExpirationDate < DateTime.UtcNow)
 {
return Unauthorized("Invalid or expired refresh token.");
 }

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

var newAccessToken = _jwtTokenService.GenerateAccessToken(claims);
var newRefreshToken = _jwtTokenService.GenerateRefreshToken
           

轮换刷新令牌

如前所述,在每次使用时轮换刷新令牌是一种很好的做法。这意味着每次使用刷新令牌获取新的访问令牌时,也应生成新的刷新令牌并将其发送到客户端。这限制了刷新令牌遭到破坏时的潜在损害。

在 中更新刷新令牌终结点以处理令牌轮换:AuthController

[HttpPost("refresh")]
public IActionResult Refresh()
{
var oldRefreshToken = Request.Cookies["refreshToken"];
if (string.IsOrEmpty(oldRefreshToken))
 {
return Unauthorized("Refresh token is missing.");
 }

var storedRefreshToken = GetStoredRefreshToken(oldRefreshToken);
if (storedRefreshToken ==  || storedRefreshToken.ExpirationDate < DateTime.UtcNow)
 {
return Unauthorized("Invalid or expired refresh token.");
 }

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

var newAccessToken = _jwtTokenService.GenerateAccessToken(claims);
var newRefreshToken = _jwtTokenService.GenerateRefreshToken();

// Update the stored refresh token
 storedRefreshToken.Token = newRefreshToken;
 storedRefreshToken.ExpirationDate = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays);
SaveRefreshToken(storedRefreshToken);

// Set the new refresh token as an HTTP-only cookie
 Response.Cookies.Append("refreshToken", newRefreshToken, new CookieOptions
 {
 HttpOnly = true,
 Secure = true, // Set to true in production
 Expires = DateTime.UtcNow.AddDays(_jwtSettings.RefreshTokenExpirationDays)
 });

return Ok(new { AccessToken = newAccessToken });
}
           

处理用户注销

当用户注销时,您应使其刷新令牌失效,以防止其用于生成新的访问令牌。您可以通过从数据库中删除刷新令牌并清除仅限 HTTP 的 Cookie 来实现此目的。

将注销端点添加到您的:AuthController

[HttpPost("logout")]
public IActionResult Logout()
{
var refreshToken = Request.Cookies["refreshToken"];
if (!string.IsOrEmpty(refreshToken))
 {
// Remove the refresh token from the database
RemoveRefreshToken(refreshToken);

// Clear the HTTP-only cookie
 Response.Cookies.Delete("refreshToken");
 }

return NoContent();
}
           

提高安全性

以下是一些需要牢记的其他安全注意事项:

  1. **使用 HTTPS:**始终使用 HTTPS 对客户端和服务器之间传输的数据(包括令牌)进行加密。这样可以防止中间人攻击。
  2. 短期访问令牌:保持访问令牌的短期访问令牌(例如,15-30 分钟)。这限制了攻击者设法窃取访问令牌的时间窗口。
  3. 更改密码时撤销令牌:如果用户更改了密码或执行了敏感操作,请撤销与用户关联的所有刷新令牌,以防止未经授权的访问。
  4. 速率限制:对身份验证终结点实施速率限制,以防止暴力攻击。

示例:Entity Framework Core 集成

对于实际应用程序,通常使用数据库来存储刷新令牌。下面是一个示例,说明如何集成 Entity Framework Core 来管理刷新令牌:

  1. 定义 RefreshToken 实体:
public class RefreshToken 
{ 
public int Id { get; set; } 
public string Token { get; set; } 
public string Username { get; set; } 
public DateTime ExpirationDate { get; set; } 
}
           

2. 将 DbSet 添加到 DbContext:

public class ApplicationDbContext : DbContext
{
public DbSet<RefreshToken> RefreshTokens { get; set; }

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

3. 实现管理刷新令牌的方法:

public class RefreshTokenService
{
private readonly ApplicationDbContext _context;

public RefreshTokenService(ApplicationDbContext context)
 {
 _context = context;
 }

public void SaveRefreshToken(RefreshToken refreshToken)
 {
 _context.RefreshTokens.Update(refreshToken);
 _context.SaveChanges();
 }

public RefreshToken GetStoredRefreshToken(string token)
 {
return _context.RefreshTokens.SingleOrDefault(rt => rt.Token == token);
 }

public void RemoveRefreshToken(string token)
 {
var refreshToken = GetStoredRefreshToken(token);
if (refreshToken != )
 {
 _context.RefreshTokens.Remove(refreshToken
           

如果你喜欢我的文章,请给我一个赞!谢谢