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

984afcb3128f4d4764e0fb0eb07f91d6.jpeg

作为全栈 .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 == null || 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 null;
}

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.IsNullOrEmpty(refreshToken))
    {
        return Unauthorized("Refresh token is missing.");
    }

    var storedRefreshToken = GetStoredRefreshToken(refreshToken);
    if (storedRefreshToken == null || 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.IsNullOrEmpty(oldRefreshToken))
    {
        return Unauthorized("Refresh token is missing.");
    }

    var storedRefreshToken = GetStoredRefreshToken(oldRefreshToken);
    if (storedRefreshToken == null || 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.IsNullOrEmpty(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 != null)
        {
            _context.RefreshTokens.Remove(refreshToken

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值