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

科技   2024-11-22 05:24   上海  


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

包含用户流的图表

什么是 JSON Web 令牌?

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


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

架构师老卢
资深软件架构师, 分享编程、软件设计经验, 教授前沿技术, 分享技术资源(每天发布电子书),每天进步一点点...
 最新文章