This commit is contained in:
2025-12-04 00:40:12 +08:00
parent 4f3eecabc4
commit c6a95fa28d
20 changed files with 874 additions and 9 deletions

View File

@@ -4,6 +4,7 @@ using Atomx.Common.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.Text.Json;
namespace Atomx.Admin.Extensions
{
@@ -14,7 +15,9 @@ namespace Atomx.Admin.Extensions
/// </summary>
/// <param name="services"></param>
/// <param name="Configuration"></param>
public static void AddAuthorize(this IServiceCollection services, IConfiguration Configuration)
/// <param name="environment"></param>
/// <exception cref="Exception"></exception>
public static void AddAuthorize(this IServiceCollection services, IConfiguration Configuration, IWebHostEnvironment environment)
{
var jwtSetting = Configuration.GetSection("Authentication:JwtBearer").Get<JwtSetting>();
if (jwtSetting == null)
@@ -27,6 +30,9 @@ namespace Atomx.Admin.Extensions
services.AddAuthentication()
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.RequireHttpsMetadata = !environment.IsDevelopment();
options.SaveToken = true;
options.ClaimsIssuer = jwtSetting.Issuer;
options.TokenValidationParameters = new TokenValidationParameters
{
@@ -36,17 +42,59 @@ namespace Atomx.Admin.Extensions
ValidateIssuerSigningKey = true,
ValidAudience = jwtSetting.Audience,//Audience
ValidIssuer = jwtSetting.Issuer,
ClockSkew = TimeSpan.FromMinutes(Convert.ToDouble(jwtSetting.ClockSkew)),
ClockSkew = TimeSpan.FromMinutes(Convert.ToDouble(jwtSetting.ClockSkew)), //过期时钟偏差
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.SecurityKey))
};
options.Events = new JwtBearerEvents
{
//OnTokenValidated = async context =>
//{
// var userService = context.HttpContext.RequestServices.GetRequiredService<IUserService>();
// var userId = context.Principal?.FindFirst("sub")?.Value;
// if (userId != null)
// {
// var user = await userService.GetUserByIdAsync(userId);
// if (user == null || !user.IsActive)
// {
// context.Fail("用户不存在或已被禁用");
// }
// }
//},
//OnMessageReceived = context =>
//{
// // SignalR JWT支持
// var accessToken = context.Request.Query["access_token"];
// var path = context.HttpContext.Request.Path;
// if (!string.IsNullOrEmpty(accessToken) &&
// (path.StartsWithSegments("/hubs") || path.StartsWithSegments("/notification")))
// {
// context.Token = accessToken;
// }
// return Task.CompletedTask;
//},
OnChallenge = context =>
{
var absoluteUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
context.HandleResponse();
context.Response.Redirect($"/account/login?returnUrl={Uri.EscapeDataString(absoluteUri)}");
return Task.CompletedTask;
//context.HandleResponse();
//context.Response.StatusCode = StatusCodes.Status401Unauthorized;
//context.Response.ContentType = "application/json";
//var result = JsonSerializer.Serialize(new
//{
// StatusCode = 401,
// Message = "未授权访问",
// Error = context.Error,
// ErrorDescription = context.ErrorDescription
//});
//return context.Response.WriteAsync(result);
},
OnAuthenticationFailed = context =>

View File

@@ -0,0 +1,56 @@
using System.Threading.RateLimiting;
namespace Atomx.Admin.Extensions
{
public static class RateLimiterExtension
{
/// <summary>
/// 添加速率限制
/// </summary>
/// <param name="services"></param>
/// <param name="Configuration"></param>
/// <param name="environment"></param>
/// <exception cref="Exception"></exception>
public static void AddRateLimiter(this IServiceCollection services, IConfiguration Configuration, IWebHostEnvironment environment)
{
// 速率限制
services.AddRateLimiter(limiterOptions =>
{
limiterOptions.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// 全局限制
limiterOptions.AddPolicy("global", context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 2
}));
// 登录限制
limiterOptions.AddPolicy("login", context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: $"{context.Connection.RemoteIpAddress}_{context.Request.Path}",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 5,
Window = TimeSpan.FromMinutes(15),
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
QueueLimit = 0
}));
});
//// HTTP客户端工厂
//services.AddHttpClient("ApiClient")
// .AddTransientHttpErrorPolicy(policy =>
// policy.WaitAndRetryAsync(3, retryAttempt =>
// TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
// .AddTransientHttpErrorPolicy(policy =>
// policy.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
}
}
}

View File

@@ -0,0 +1,12 @@
using Atomx.Admin.Middlewares;
namespace Atomx.Admin.Extensions
{
public static class SecurityHeadersExtensions
{
public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SecurityHeadersMiddleware>();
}
}
}

View File

@@ -0,0 +1,36 @@
namespace Atomx.Admin.Extensions
{
public static class SignalRExtension
{
/// <summary>
/// 添加SignalR
/// </summary>
/// <param name="services"></param>
/// <param name="Configuration"></param>
/// <param name="environment"></param>
/// <exception cref="Exception"></exception>
public static void AddSignalR(this IServiceCollection services, IConfiguration Configuration, IWebHostEnvironment environment)
{
//// SignalR
//services.AddSignalR(options =>
//{
// options.EnableDetailedErrors = environment.IsDevelopment();
// options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
// options.HandshakeTimeout = TimeSpan.FromSeconds(15);
// options.KeepAliveInterval = TimeSpan.FromSeconds(10);
// options.MaximumReceiveMessageSize = 1024 * 1024; // 1MB
// if (!environment.IsDevelopment())
// {
// options.StreamBufferCapacity = 10;
// }
//})
//.AddMessagePackProtocol()
//.AddStackExchangeRedis(redisConnectionString, options =>
//{
// options.Configuration.ChannelPrefix = "BlazorAuthApp:SignalR";
//});
}
}
}

View File

@@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System.Text.Json;
namespace Atomx.Admin.Middlewares
{
/// <summary>
/// 异常处理中间件
/// </summary>
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
private readonly IWebHostEnvironment _environment;
public ExceptionHandlingMiddleware(
RequestDelegate next,
ILogger<ExceptionHandlingMiddleware> logger,
IWebHostEnvironment environment)
{
_next = next;
_logger = logger;
_environment = environment;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
_logger.LogError(exception, "未处理的异常: {Message}", exception.Message);
var response = context.Response;
response.ContentType = "application/json";
var problemDetails = new ProblemDetails
{
Title = "发生错误",
Status = (int)HttpStatusCode.InternalServerError,
Instance = context.Request.Path
};
// 根据异常类型设置状态码
switch (exception)
{
case UnauthorizedAccessException:
problemDetails.Status = (int)HttpStatusCode.Unauthorized;
problemDetails.Title = "未授权访问";
break;
case KeyNotFoundException:
problemDetails.Status = (int)HttpStatusCode.NotFound;
problemDetails.Title = "资源未找到";
break;
case ArgumentException:
case InvalidOperationException:
problemDetails.Status = (int)HttpStatusCode.BadRequest;
problemDetails.Title = "无效请求";
break;
}
// 开发环境包含详细错误信息
if (_environment.IsDevelopment())
{
problemDetails.Extensions.Add("exception", exception.Message);
problemDetails.Extensions.Add("stackTrace", exception.StackTrace);
problemDetails.Extensions.Add("innerException", exception.InnerException?.Message);
}
else
{
problemDetails.Detail = "处理您的请求时发生错误。请稍后重试。";
}
response.StatusCode = problemDetails.Status.Value;
await response.WriteAsync(JsonSerializer.Serialize(problemDetails));
}
}
}

View File

@@ -0,0 +1,56 @@
namespace Atomx.Admin.Middlewares
{
/// <summary>
/// 安全中间件
/// </summary>
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
private readonly IWebHostEnvironment _environment;
public SecurityHeadersMiddleware(RequestDelegate next, IWebHostEnvironment environment)
{
_next = next;
_environment = environment;
}
public async Task InvokeAsync(HttpContext context)
{
// 添加安全头
if (!context.Response.HasStarted)
{
var headers = context.Response.Headers;
// CSP策略
if (!_environment.IsDevelopment())
{
headers.Append("Content-Security-Policy",
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self'; " +
"connect-src 'self' wss:; " +
"frame-ancestors 'none';");
}
// 其他安全头
headers.Append("X-Content-Type-Options", "nosniff");
headers.Append("X-Frame-Options", "DENY");
headers.Append("X-XSS-Protection", "1; mode=block");
headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
headers.Append("Permissions-Policy",
"camera=(), microphone=(), geolocation=(), interest-cohort=()");
// HSTS在生产环境中启用
if (!_environment.IsDevelopment())
{
headers.Append("Strict-Transport-Security",
"max-age=31536000; includeSubDomains; preload");
}
}
await _next(context);
}
}
}

View File

@@ -14,6 +14,9 @@ using Blazored.LocalStorage;
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore;
using Serilog;
@@ -68,22 +71,48 @@ builder.Services.AddScoped<AuthHeaderHandler>();
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost");
builder.Services.AddDataService();
builder.Services.AddAuthorize(builder.Configuration);
builder.Services.AddAuthorize(builder.Configuration,builder.Environment);
var connection = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<DataContext>(options => options.UseNpgsql(connection, p => p.MigrationsHistoryTable("__DbMigrationsHistory")));
var redisConnection = builder.Configuration.GetConnectionString("cache");
builder.Services.AddStackExchangeRedisCache(options =>
{
#region
options.Configuration = $"{builder.Configuration.GetConnectionString("cache")}";
options.Configuration = redisConnection;
options.InstanceName = builder.Configuration["RedisCache:InstanceName"];
#endregion
});
//// Redis<69>ֲ<EFBFBD>ʽ<EFBFBD><CABD><EFBFBD><EFBFBD>
//builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
// ConnectionMultiplexer.Connect(redisConnection));
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧѹ<D3A6><D1B9>
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
// <20><><EFBFBD><EFBFBD>Antiforgery
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
options.Cookie.Name = ".Atomx.Antiforgery";
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.HttpOnly = true;
});
builder.Services.AddOpenApi();
builder.Services.AddAntDesign();
builder.Services.Configure<MonitoringOptions>(builder.Configuration.GetSection("Monitoring"));
@@ -92,6 +121,12 @@ var app = builder.Build();
app.AddDataMigrate();
// <20><><EFBFBD><EFBFBD>HTTP<54><50><EFBFBD><EFBFBD><EFBFBD>ܵ<EFBFBD>
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
@@ -104,7 +139,11 @@ else
app.UseExceptionHandler("/Error", createScopeForErrors: true);
}
// <20><>ȫͷ
app.UseSecurityHeaders();
// <20><>Ӧѹ<D3A6><D1B9>
app.UseResponseCompression();
app.UseCors(option =>
{
@@ -113,7 +152,18 @@ app.UseCors(option =>
option.AllowAnyHeader();
});
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// <20><><EFBFBD>澲̬<E6BEB2>ļ<EFBFBD>
ctx.Context.Response.Headers.Append(
"Cache-Control", $"public, max-age={31536000}");
}
});
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
@@ -124,10 +174,45 @@ app.MapStaticAssets();
app.UseMiddleware<MonitoringMiddleware>();
//// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˵<EFBFBD>
//app.MapHealthChecks("/health", new HealthCheckOptions
//{
// ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
// Predicate = _ => true,
// AllowCachingResponses = false
//});
//app.MapHealthChecks("/health/ready", new HealthCheckOptions
//{
// ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
// Predicate = check => check.Tags.Contains("ready")
//});
//app.MapHealthChecks("/health/live", new HealthCheckOptions
//{
// ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
// Predicate = _ => false
//});
// SignalR<6C>˵<EFBFBD>
//app.MapHub<ChatHub>("/hubs/chat");
//app.MapHub<NotificationHub>("/hubs/notification");
app.MapControllers();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(Atomx.Admin.Client._Imports).Assembly);
//// ȷ<><C8B7><EFBFBD><EFBFBD><EFBFBD>ݿⴴ<DDBF><E2B4B4><EFBFBD><EFBFBD>Ǩ<EFBFBD><C7A8>
//await using (var scope = app.Services.CreateAsyncScope())
//{
// var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// await dbContext.Database.MigrateAsync();
// var seeder = scope.ServiceProvider.GetRequiredService<DatabaseSeeder>();
// await seeder.SeedAsync();
//}
app.Run();

View File

@@ -0,0 +1,383 @@
using Atomx.Common.Entities;
using Atomx.Common.Models;
using Atomx.Data;
using Atomx.Data.CacheServices;
using Atomx.Utils.Extension;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace Atomx.Admin.Services
{
public interface ITokenService
{
Task<AuthResponse> GenerateTokenAsync(User user, string? ipAddress = null, string? userAgent = null);
Task<AuthResponse> RefreshTokenAsync(string token, string refreshToken, string? ipAddress = null);
Task<bool> RevokeTokenAsync(string refreshToken, string? ipAddress = null);
Task<bool> ValidateTokenAsync(string token);
Task<User?> GetUserFromTokenAsync(string token);
}
public class TokenService : ITokenService
{
readonly DataContext _dbContext;
readonly ICacheService _cacheService;
readonly JwtSetting _jwtSetting;
private readonly ILogger<TokenService> _logger;
private readonly SecurityKey _securityKey;
private readonly SigningCredentials _signingCredentials;
public TokenService(
ILogger<TokenService> logger, DataContext dataContext, ICacheService cacheService, JwtSetting jwtSetting)
{
_logger = logger;
_dbContext = dataContext;
_cacheService = cacheService;
_jwtSetting = jwtSetting;
var key = Encoding.UTF8.GetBytes(_jwtSetting.SecurityKey);
_securityKey = new SymmetricSecurityKey(key);
_signingCredentials = new SigningCredentials(_securityKey, SecurityAlgorithms.HmacSha256);
}
public async Task<AuthResponse> GenerateTokenAsync(User user, string? ipAddress = null, string? userAgent = null)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
// 检查用户是否被锁定
if (user.LockoutEndTime.HasValue && user.LockoutEndTime > DateTime.UtcNow)
throw new InvalidOperationException("账户已被锁定");
// 生成访问令牌
var accessToken = GenerateAccessToken(user);
// 生成刷新令牌
var refreshToken = GenerateRefreshToken();
// 保存刷新令牌到数据库
var refreshTokenEntity = new RefreshToken
{
Token = HashRefreshToken(refreshToken),
UserId = user.Id,
IssuedTime = DateTime.UtcNow,
ExpiresTime = DateTime.UtcNow.AddDays(_jwtSetting.RefreshTokenExpirationMinutes),
Ip = ipAddress,
UserAgent = userAgent
};
// 移除旧的刷新令牌(安全策略)
await RemoveOldRefreshTokensAsync(user.Id);
_dbContext.RefreshTokens.Add(refreshTokenEntity);
await _dbContext.SaveChangesAsync();
// 记录审计日志
//await _auditService.LogAsync(new AuditLog
//{
// UserId = user.Id,
// Action = "GenerateToken",
// Timestamp = DateTime.UtcNow,
// IpAddress = ipAddress,
// UserAgent = userAgent,
// Details = $"Token generated for user {user.Username}"
//});
// 缓存令牌(防止重复使用)
await CacheTokenAsync(accessToken, user.Id);
return new AuthResponse
{
Token = accessToken,
RefreshToken = refreshToken,
TokenExpiry = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes)
};
}
public async Task<AuthResponse> RefreshTokenAsync(string token, string refreshToken, string? ipAddress = null)
{
var principal = GetPrincipalFromExpiredToken(token);
var userId = principal.FindFirst("sub")?.Value.ToLong();
if (userId == 0)
throw new SecurityTokenException("无效的令牌");
var user = await _dbContext.Users
.SingleOrDefaultAsync(u => u.Id == userId && u.IsActive);
if (user == null)
throw new SecurityTokenException("用户不存在或已被禁用");
// 验证刷新令牌
var hashedRefreshToken = HashRefreshToken(refreshToken);
var storedToken = await _dbContext.RefreshTokens
.FirstOrDefaultAsync(rt =>
rt.Token == hashedRefreshToken &&
rt.UserId == userId &&
rt.ExpiresTime > DateTime.UtcNow &&
!rt.IsRevoked);
if (storedToken == null)
throw new SecurityTokenException("无效的刷新令牌");
// 撤销旧的刷新令牌
storedToken.IsRevoked = true;
storedToken.RevokedTime = DateTime.UtcNow;
storedToken.Ip = ipAddress;
// 生成新的令牌对
var newTokenResponse = await GenerateTokenAsync(user, ipAddress, storedToken.UserAgent);
//// 记录审计日志
//await _auditService.LogAsync(new AuditLog
//{
// UserId = user.Id,
// Action = "RefreshToken",
// Timestamp = DateTime.UtcNow,
// IpAddress = ipAddress,
// Details = "Token refreshed successfully"
//});
return newTokenResponse;
}
public async Task<bool> RevokeTokenAsync(string refreshToken, string? ipAddress = null)
{
var hashedToken = HashRefreshToken(refreshToken);
var token = await _dbContext.RefreshTokens
.FirstOrDefaultAsync(rt => rt.Token == hashedToken);
if (token == null || token.IsRevoked)
return false;
token.IsRevoked = true;
token.RevokedTime = DateTime.UtcNow;
token.Ip = ipAddress;
await _dbContext.SaveChangesAsync();
// 清除缓存
await _cacheService.Remove($"user:{token.UserId}");
//await _auditService.LogAsync(new AuditLog
//{
// UserId = token.UserId,
// Action = "RevokeToken",
// Timestamp = DateTime.UtcNow,
// IpAddress = ipAddress,
// Details = "Token revoked"
//});
return true;
}
public async Task<bool> ValidateTokenAsync(string token)
{
try
{
// 检查令牌是否在缓存中(已被撤销)
var cacheKey = $"revoked_token:{HashToken(token)}";
var cached = await _cacheService.GetCacheString(cacheKey);
if (cached != null)
return false;
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = _securityKey,
ValidateIssuer = true,
ValidIssuer = _jwtSetting.Issuer,
ValidateAudience = true,
ValidAudience = _jwtSetting.Audience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
tokenHandler.ValidateToken(token, validationParameters, out _);
return true;
}
catch
{
return false;
}
}
public async Task<User?> GetUserFromTokenAsync(string token)
{
try
{
var principal = GetPrincipalFromToken(token);
var userId = principal.FindFirst("sub")?.Value.ToLong();
if (userId == 0)
return null;
// 尝试从缓存获取
var cacheKey = $"user:{userId}";
var cachedUser = await _cacheService.GetCacheString(cacheKey);
if (!string.IsNullOrEmpty(cachedUser))
{
return JsonSerializer.Deserialize<User>(cachedUser);
}
// 从数据库获取
var user = await _dbContext.Users
.FirstOrDefaultAsync(u => u.Id == userId && u.IsActive);
if (user != null)
{
// 缓存用户信息5分钟
await _cacheService.SetCacheAsync(cacheKey, JsonSerializer.Serialize(user), 5);
}
return user;
}
catch
{
return null;
}
}
private string GenerateAccessToken(User user)
{
var claims = new[]
{
new Claim("sub", user.Id.ToString()),
new Claim("jti", Guid.NewGuid().ToString()),
new Claim("name", user.Name),
new Claim("email", user.Email),
new Claim("email_confirmed", user.EmailConfirmed.ToString().ToLower()),
new Claim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
}.ToList();
// 添加角色声明
//foreach (var userRole in user.UserRoles)
//{
// claims.Add(new Claim("role", userRole.Role.ToString()));
//}
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes),
Issuer = _jwtSetting.Issuer,
Audience = _jwtSetting.Audience,
SigningCredentials = _signingCredentials,
NotBefore = DateTime.UtcNow
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
private string GenerateRefreshToken()
{
var randomNumber = new byte[64];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
private ClaimsPrincipal GetPrincipalFromToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = _securityKey,
ValidateIssuer = true,
ValidIssuer = _jwtSetting.Issuer,
ValidateAudience = true,
ValidAudience = _jwtSetting.Audience,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out _);
return principal;
}
catch (Exception ex)
{
_logger.LogError(ex, "令牌验证失败");
throw new SecurityTokenException("无效的令牌", ex);
}
}
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
try
{
var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = _securityKey,
ValidateIssuer = true,
ValidIssuer = _jwtSetting.Issuer,
ValidateAudience = true,
ValidAudience = _jwtSetting.Audience,
ValidateLifetime = false, // 注意:这里不验证过期时间
ClockSkew = TimeSpan.Zero
}, out _);
return principal;
}
catch (Exception ex)
{
_logger.LogError(ex, "过期令牌验证失败");
throw new SecurityTokenException("无效的令牌", ex);
}
}
private string HashRefreshToken(string refreshToken)
{
using var sha256 = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(refreshToken + _jwtSetting.SecurityKey);
var hash = sha256.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
private string HashToken(string token)
{
using var sha256 = SHA256.Create();
var bytes = Encoding.UTF8.GetBytes(token);
var hash = sha256.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
private async Task RemoveOldRefreshTokensAsync(long userId)
{
var tokens = await _dbContext.RefreshTokens
.Where(rt => rt.UserId == userId && !rt.IsRevoked)
.OrderByDescending(rt => rt.ExpiresTime)
.Skip(_jwtSetting.MaxRefreshTokensPerUser - 1)
.ToListAsync();
foreach (var token in tokens)
{
token.IsRevoked = true;
token.RevokedTime = DateTime.UtcNow;
}
}
private async Task CacheTokenAsync(string token, long userId)
{
var cacheKey = $"token:{HashToken(token)}";
await _cacheService.SetCacheAsync(cacheKey, userId, _jwtSetting.AccessTokenExpirationMinutes);
}
}
}

View File

@@ -16,9 +16,10 @@
"Issuer": "http://api.sampleapi.com",
"Audience": "SampleApi",
"SecurityKey": "SecurityKey23456SecurityKey23456",
"ClockSkew": "600",
"ClockSkew": "10",
"AccessTokenExpirationMinutes": "60",
"RefreshTokenExpirationMinutes": "60"
"RefreshTokenExpirationMinutes": "60",
"MaxRefreshTokensPerUser": "3"
}
},