在 .NET 8 中使用 AppUser 进行 JWT 令牌身份验证

作者:微信公众号:【架构师老卢】
10-27 18:0
110

JWT 身份验证是保护 API 的标准方法之一。这允许无状态身份验证,因为签名令牌是在客户端和服务器之间传递的。在 .NET 8 中,使用 JWT 令牌的方式得到了改进。将它们与 AppUser 类集成将为您的应用程序提供无缝身份验证。本文介绍了在 .NET 8 Web 应用程序中通过 AppUser 类实现 JWT 令牌身份验证的过程。

包含用户流的图表

什么是 JSON Web 令牌?

JSON Web 令牌 (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于将信息作为 JSON 对象在各方之间安全地传输。此信息是经过数字签名的,因此可以验证和信任。可以使用密钥(使用 HMAC 算法)或使用 RSAECDSA 的公钥/私钥对对 JWT 进行签名。

尽管 JWT 可以加密以在各方之间提供机密性,但我们将重点介绍签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则对其他方隐藏这些声明。当使用公钥/私钥对令牌进行签名时,签名还会证明只有持有私钥的一方是签署私钥的一方。

什么是 JSON Web 令牌结构?

在其紧凑形式中,JSON Web 令牌由三个部分组成,由点 () 分隔,它们是:.

  • 页眉
  • 有效载荷
  • 签名

因此,JWT 通常如下所示。

xxxxx.yyyyy.zzzzz

更多详情请访问 https://jwt.io/introduction

设置 JWT 令牌身份验证

1. 创建新的 .NET 8 Web API 项目

dotnet new webapi -n JwtAuthApp

2. 安装所需的 NuGet 软件包

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

3. 创建 JWT 配置模型

using System.Globalization;

namespace JwtAuthApp.JWT;

public class JwtConfiguration
{
    public string Issuer { get; } = string.Empty;

    public string Secret { get; } = string.Empty;

    public string Audience { get; } = string.Empty;

    public int ExpireDays { get; }

    public JwtConfiguration(IConfiguration configuration)
    {
        var section = configuration.GetSection("JWT");

        Issuer = section[nameof(Issuer)];
        Secret = section[nameof(Secret)];
        Audience = section[nameof(Secret)];
        ExpireDays = Convert.ToInt32(section[nameof(ExpireDays)], CultureInfo.InvariantCulture);
    }
}

4. 将 JWT 配置添加到您的 app.settings 中

{  
  "Jwt": {  
    "Issuer": "JwtAuthApp",  
    "Audience": "https://localhost:7031/",  
    "Secret": "70FC177F-3667-453D-9DA1-AF223DF6C014",  
    "ExpireDays": 30  
  }  
}
  • ❗️颁发者:标识颁发令牌的委托人(通常是您的应用程序)。

受众:指定令牌的目标受众(通常是使用 API 的客户端或服务)。

  • ❗️密钥:密钥用于对 JWT 进行签名,以确保其真实性。它应该是一个长而随机的字符串,以防止篡改。
  • ExpireDays:定义令牌在过期前的有效期。

5. 为 Configuration 配置 DIProgram.cs

builder.Services.AddTransient<JwtConfiguration>();

6. 配置 JWT 身份验证扩展

using Microsoft.AspNetCore.Authentication;  
using Microsoft.AspNetCore.Authentication.JwtBearer;  
using Microsoft.AspNetCore.DataProtection;  
using Microsoft.IdentityModel.Tokens;  
using System.ComponentModel.DataAnnotations;  
using System.Diagnostics.Metrics;  
using System.Security.Claims;  
using System;  
using System.Text;

namespace JwtAuthApp.JWT;

public static class JwtAuthBuilderExtesnions  
{  
    public static AuthenticationBuilder AddJwtAuthentication(this IServiceCollection services, IConfiguration configuration)  
    {  
        var jwtConfiguration = new JwtConfiguration(configuration);

        services.AddAuthorization();

        return services.AddAuthentication(x =>  
        {  
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;  
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;  
        })  
        .AddJwtBearer(x =>  
        {  
            x.SaveToken = true;  
            x.TokenValidationParameters = new TokenValidationParameters  
            {  
                ValidateIssuer = true,  
                ValidateAudience = true,  
                ValidateLifetime = true,  
                ValidateIssuerSigningKey = true,  
                ValidIssuer = jwtConfiguration.Issuer,  
                ValidAudience = jwtConfiguration.Audience,  
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfiguration.Secret)),

                RequireExpirationTime = true,  
            };  
            x.Events = new JwtBearerEvents  
            {  
                OnMessageReceived = context =>  
                {  
                    string authorization = context.Request.Headers["Authorization"\];

                    if (string.IsNullOrEmpty(authorization))  
                    {  
                        context.NoResult();  
                    }  
                    else  
                    {  
                        context.Token = authorization.Replace("Bearer ", string.Empty);  
                    }

                    return Task.CompletedTask;  
                },  
            };  
        });  
    }  
}
  • SaveToken:定义在成功授权后是否应将不记名令牌存储在 AuthenticationProperties 中。
  • ❗️**ValidateIssuer:**确保令牌的 (issuer) 声明与预期的颁发者匹配。iss
  • ValidateAudience:验证令牌中的 (audience) 声明,以确保它与预期的受众匹配。aud
  • ValidateLifetime:检查令牌的(过期)时间是否有效,令牌是否未过期。exp
  • ❗️ValidateIssuerSigningKey:根据签名密钥验证令牌的签名,以确保其完整性。
  • ❗️ValidIssuer:指定从配置(或环境变量)中提取的令牌的预期颁发者。appsettings.json
  • ❗️IssuerSigningKey:使用对称安全密钥对 JWT 进行签名和验证,将配置中的密钥转换为字节数组进行加密。
  • RequireExpirationTime:确保 JWT 令牌包含 (expiration) 声明。exp
  • OnMessageReceived:在上下文中保存令牌

❗️ = 重要

7. 在Program.cs

builder.Services.AddJwtAuthentication(builder.Configuration);

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

8. 创建 Token 生成服务

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace JwtAuthApp.JWT;

public class TokenService
{
    private readonly JwtConfiguration _config;

    public TokenService(JwtConfiguration config)
    {
        _config = config;
    }

    public string GenerateToken(string id, string email)
    {
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, id),
            new Claim(JwtRegisteredClaimNames.Email, email),
            // Add more claims if needed
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config.Secret));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _config.Issuer,
            audience: _config.Audience,
            claims: claims,
            expires: DateTime.Now.AddDays(_config.ExpireDays),
            signingCredentials: creds
        );

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

我不建议在令牌中存储 First Name(名字)、Last Name(姓氏)、Role(角色)和其他数据等信息。

9. 注册 Token Service

builder.Services.AddTransient<TokenService>();

10. 添加登录端点或控制器

app.MapPost("/login", (LoginRequest request, TokenService tokenService) =>  
{  
    // In a real app, you would validate the user's credentials against a database.  
    // Authenticate user and generate token  
    // For demo purposes, we are using hardcoded values  
    var userIsAuthenticated = request.Username == "admin" && request.Password == "admin";

    if (!userIsAuthenticated)  
    {  
        return Results.Unauthorized();  
    }  
    var userId = "9999"; // Get user id from database  
    var email = "valentin.osidach@gmail.com"; // Get email from database  
    var token = tokenService.GenerateToken(userId, email);

    return Results.Ok(token);  
}).AllowAnonymous();

11. 新增 SwaggerConfiguration(方便测试)

using Microsoft.OpenApi.Models;  
using Swashbuckle.AspNetCore.SwaggerGen;

namespace JwtAuthApp.JWT;

public static class SwaggerConfiguration  
{  
    public static OpenApiSecurityScheme Scheme => new OpenApiSecurityScheme  
    {  
        In = ParameterLocation.Header,  
        Description = "Please enter a valid token",  
        Name = "Authorization",  
        Type = SecuritySchemeType.Http,  
        BearerFormat = "JWT",  
        Scheme = "Bearer",  
        Reference = new OpenApiReference  
        {  
            Id = "Bearer",  
            Type = ReferenceType.SecurityScheme,  
        },  
    };

    public static void Configure(SwaggerGenOptions option)  
    {  
        option.ResolveConflictingActions(apiDesc => apiDesc.First());  
        option.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" });  
        option.AddSecurityDefinition(Scheme.Reference.Id, Scheme);  
        option.AddSecurityRequirement(new OpenApiSecurityRequirement  
        {  
            { Scheme, Array.Empty<string>() },  
        });  
    }  
}builder.Services.AddSwaggerGen(SwaggerConfiguration.Configure);

12. 添加 AppUser

public class AppUser : ClaimsPrincipal  
{  
    public AppUser(IHttpContextAccessor contextAccessor) : base(contextAccessor.HttpContext.User) { }

    public string Id => FindFirst(ClaimTypes.NameIdentifier).Value;  
    public string Email => FindFirst(ClaimTypes.Email).Value;  
}builder.Services.AddTransient<AppUser>();  
builder.Services.AddHttpContextAccessor();

13. 为所有 Controller 或端点添加 Authorize 属性

app.MapGet("/weatherforecast", () =>  
{  
    var forecast = Enumerable.Range(1, 5).Select(index =>  
        new WeatherForecast  
        (  
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),  
            Random.Shared.Next(-20, 55),  
            summaries[Random.Shared.Next(summaries.Length)]  
        ))  
        .ToArray();  
    return forecast;  
})  
.RequireAuthorization()  
.WithName("GetWeatherForecast")  
.WithOpenApi();

//Inject AppUser and get user email from the token  
app.MapGet("/user", (AppUser user) =>  
{  
    return Results.Ok(user.Email);  
})  
.RequireAuthorization()  
.WithName("GetUserEmail")  
.WithOpenApi();

测试

  • 所有端点

  • 获取天气预报在登录前收到错误 401 (未授权)

  • 登录返回的 jwt 令牌

响应令牌 :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5OTk5IiwiZW1haWwiOiJ2YWxlbnRpbi5vc2lkYWNoQGdtYWlsLmNvbSIsImV4cCI6MTczMjIxMTM5OSwiaXNzIjoiSnd0QXV0aEFwcCIsImF1ZCI6IjcwRkMxNzdGLTM2NjctNDUzRC05REExLUFGMjIzREY2QzAxNCJ9.YCBGUFiGFKMZCJJL3sgsk-1lbruSnuY2lWpY71SLx3Y
  • 在 Swagger Auth 中使用 jwt 令牌

  • 获取天气预报返回结果

  • 获取用户电子邮件 返回用户电子邮件

在本文中,我们演示了如何在 .NET 8 中使用最小 API 结构实现 JWT 令牌身份验证。这种方法提供了一种简单而干净的方法来保护您的 API,而不会产生控制器的开销。关键步骤包括配置 JWT 身份验证、生成令牌以及使用最少的代码保护终端节点。

通过此设置,您可以通过添加更多功能(如用户注册、令牌刷新或基于角色的授权)来进一步扩展身份验证流程。

👥 觉得这个有趣吗?与朋友分享并引发讨论。有时,最好的见解来自一场精彩的辩论。

🚀 不断前进,保持灵感,永不停止学习。

感谢您的阅读!

程序文件:

using JwtAuthApp.JWT;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpContextAccessor();

builder.Services.AddTransient<JwtConfiguration>();
builder.Services.AddTransient<TokenService>();
builder.Services.AddTransient<AppUser>();

// Add JWT Authentication configuration
builder.Services.AddJwtAuthentication(builder.Configuration);
builder.Services.AddSwaggerGen(SwaggerConfiguration.Configure);

var app = builder.Build();

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

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "NY", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Lviv"
};


app.MapPost("/login", (LoginRequest request, TokenService tokenService) =>
{
    // In a real app, you would validate the user's credentials against a database.
    // Authenticate user and generate token 
    // For demo purposes, we are using hardcoded values
    var userIsAuthenticated = request.Username == "admin" && request.Password == "admin";

    if (!userIsAuthenticated)
    {
        return Results.Unauthorized();
    }
    var userId = "9999"; // Get user id from database
    var email = "valentin.osidach@gmail.com"; // Get email from database
    var token = tokenService.GenerateToken(userId, email);

    return Results.Ok(token);
}).AllowAnonymous();

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.RequireAuthorization()
.WithName("GetWeatherForecast")
.WithOpenApi();

//get user email from token 
app.MapGet("/user", [Authorize] (AppUser user) =>
{
    return Results.Ok(user.Email);
})
.RequireAuthorization()
.WithName("GetUserEmail")
.WithOpenApi();

app.Run();

public class LoginRequest
{
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
}

public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

public class AppUser : ClaimsPrincipal
{
    public AppUser(IHttpContextAccessor contextAccessor) : base(contextAccessor.HttpContext.User) { }

    public string Id => FindFirst(ClaimTypes.NameIdentifier).Value;
    public string Email => FindFirst(ClaimTypes.Email).Value;
}
相关留言评论
昵称:
邮箱:
阅读排行