作为全栈 .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 的身份验证提供可靠的设置。以下是我们所涵盖内容的简要概述:
- 项目设置:创建了新的 ASP.NET Core Web API 项目,并添加了必要的 NuGet 包。
- JWT 配置:在 和 中配置了 JWT 设置。appsettings.jsonProgram.cs
- 令牌服务:用于生成和验证令牌。JwtTokenService
- 身份验证终结点:使用登录和令牌刷新终结点创建。AuthController
- 模型和存储:定义了请求的模型,并实现了用于存储和检索刷新令牌的方法。
通过执行这些步骤,您可以确保您的 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;
}
管理刷新令牌的最佳实践
- 安全地存储刷新令牌:刷新令牌是敏感的,应安全存储。最好使用仅限 HTTP 的 cookie 来存储刷新令牌,因为它们不太容易受到 XSS 攻击。
- 轮换刷新令牌:每次使用刷新令牌时,都会生成一对新的访问和刷新令牌。这限制了被盗刷新令牌的生命周期。
- 已用令牌黑名单:保留无效令牌列表,以防止重复使用。这可以使用数据库或内存中存储来实现。
- 限制刷新尝试次数:实施一种机制来限制可用于防止滥用的刷新令牌的次数。
使用仅限 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();
}
提高安全性
以下是一些需要牢记的其他安全注意事项:
- **使用 HTTPS:**始终使用 HTTPS 对客户端和服务器之间传输的数据(包括令牌)进行加密。这样可以防止中间人攻击。
- 短期访问令牌:保持访问令牌的短期访问令牌(例如,15-30 分钟)。这限制了攻击者设法窃取访问令牌的时间窗口。
- 更改密码时撤销令牌:如果用户更改了密码或执行了敏感操作,请撤销与用户关联的所有刷新令牌,以防止未经授权的访问。
- 速率限制:对身份验证终结点实施速率限制,以防止暴力攻击。
示例:Entity Framework Core 集成
对于实际应用程序,通常使用数据库来存储刷新令牌。下面是一个示例,说明如何集成 Entity Framework Core 来管理刷新令牌:
- 定义 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
如果你喜欢我的文章,请给我一个赞!谢谢