作為全棧 .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
如果你喜歡我的文章,請給我一個贊!謝謝