fix chore

This commit is contained in:
yxw
2025-12-03 18:57:55 +08:00
parent 4f3eecabc4
commit 4702e73b5a
12 changed files with 458 additions and 15 deletions

View File

@@ -1,8 +1,10 @@
 
using AntDesign;
using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Models;
using Atomx.Admin.Client.Validators; using Atomx.Admin.Client.Validators;
using Atomx.Admin.Services; using Atomx.Admin.Services;
using Atomx.Admin.Utils; using Atomx.Admin.Utils;
using Atomx.Common.Entities;
using Atomx.Common.Models; using Atomx.Common.Models;
using Atomx.Common.Utils; using Atomx.Common.Utils;
using Atomx.Data; using Atomx.Data;
@@ -18,6 +20,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt; using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -56,13 +59,15 @@ namespace Atomx.Admin.Controllers
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> Login(LoginModel model) public async Task<IActionResult> Login(LoginModel model)
{ {
var result = new ApiResult<AuthResponse>();
var validator = new LoginModelValidator(); var validator = new LoginModelValidator();
var validation = validator.Validate(model); var validation = validator.Validate(model);
if (!validation.IsValid) if (!validation.IsValid)
{ {
var message = validation.Errors.FirstOrDefault()?.ErrorMessage; var message = validation.Errors.FirstOrDefault()?.ErrorMessage;
var result = new ApiResult<string>().IsFail(message ?? string.Empty, null); result = result.IsFail(message ?? string.Empty, null);
return new JsonResult(result); return new JsonResult(result);
} }
@@ -86,12 +91,12 @@ namespace Atomx.Admin.Controllers
if (user == null) if (user == null)
{ {
var result = new ApiResult<string>().IsFail("用户不存在", null); result = result.IsFail("用户不存在", null);
return new JsonResult(result); return new JsonResult(result);
} }
if (user.Password != model.Password.ToMd5Password()) if (user.Password != model.Password.ToMd5Password())
{ {
var result = new ApiResult<string>().IsFail("账号密码不正确", null); result = result.IsFail("账号密码不正确", null);
return new JsonResult(result); return new JsonResult(result);
} }
@@ -119,19 +124,36 @@ namespace Atomx.Admin.Controllers
Audience = audience Audience = audience
}; };
var tokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
var loginResult = new ApiResult<string>().IsSuccess(tokenString);
var tokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
var refreshToken = GenerateRefreshToken();
result.Data = new AuthResponse
{
Token = tokenString,
RefreshToken = refreshToken,
TokenExpiry = tokenDescriptor.Expires!.Value
};
var refreshTokenItem = new RefreshToken
{
Token = refreshToken,
UserId = user.Id,
ExpiresTime = tokenDescriptor.Expires.Value,
Ip = _identityService.GetClientIp(),
IssuedTime = DateTime.UtcNow,
IsRevoked = false,
UserAgent = _identityService.GetUserAgent()
};
user.LastLogin = DateTime.UtcNow; user.LastLogin = DateTime.UtcNow;
user.LastIp = _identityService.GetClientIp(); user.LastIp = _identityService.GetClientIp();
user.LoginCount++; user.LoginCount++;
_dbContext.Admins.Update(user);
_dbContext.RefreshTokens.Add(refreshTokenItem);
_dbContext.SaveChanges();
//((PersistingRevalidatingAuthenticationStateProvider) _authenticationStateProvider). return new JsonResult(result);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
return new JsonResult(loginResult);
} }
@@ -146,5 +168,13 @@ namespace Atomx.Admin.Controllers
await HttpContext.SignOutAsync(); await HttpContext.SignOutAsync();
return new JsonResult(new ApiResult<string>()); return new JsonResult(new ApiResult<string>());
} }
private string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
} }
} }

View File

@@ -2,8 +2,10 @@
using Atomx.Common.Constant; using Atomx.Common.Constant;
using Atomx.Common.Models; using Atomx.Common.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using System.Text; using System.Text;
using System.Text.Json;
namespace Atomx.Admin.Extensions namespace Atomx.Admin.Extensions
{ {
@@ -14,7 +16,7 @@ namespace Atomx.Admin.Extensions
/// </summary> /// </summary>
/// <param name="services"></param> /// <param name="services"></param>
/// <param name="Configuration"></param> /// <param name="Configuration"></param>
public static void AddAuthorize(this IServiceCollection services, IConfiguration Configuration) public static void AddAuthorize(this IServiceCollection services, IConfiguration Configuration, IWebHostEnvironment environment)
{ {
var jwtSetting = Configuration.GetSection("Authentication:JwtBearer").Get<JwtSetting>(); var jwtSetting = Configuration.GetSection("Authentication:JwtBearer").Get<JwtSetting>();
if (jwtSetting == null) if (jwtSetting == null)
@@ -28,6 +30,7 @@ namespace Atomx.Admin.Extensions
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{ {
options.ClaimsIssuer = jwtSetting.Issuer; options.ClaimsIssuer = jwtSetting.Issuer;
options.RequireHttpsMetadata = !environment.IsDevelopment(); //是否要求HTTPS,生产环境建议为true
options.TokenValidationParameters = new TokenValidationParameters options.TokenValidationParameters = new TokenValidationParameters
{ {
ValidateIssuer = true, ValidateIssuer = true,
@@ -36,17 +39,46 @@ namespace Atomx.Admin.Extensions
ValidateIssuerSigningKey = true, ValidateIssuerSigningKey = true,
ValidAudience = jwtSetting.Audience,//Audience ValidAudience = jwtSetting.Audience,//Audience
ValidIssuer = jwtSetting.Issuer, 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)) IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.SecurityKey))
}; };
options.Events = new JwtBearerEvents options.Events = new JwtBearerEvents
{ {
// SignalR JWT支持
OnMessageReceived = context =>
{
//从查询字符串中获取令牌(如果存在)
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 => OnChallenge = context =>
{ {
var absoluteUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}"; var absoluteUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
context.HandleResponse(); context.HandleResponse();
context.Response.Redirect($"/account/login?returnUrl={Uri.EscapeDataString(absoluteUri)}"); context.Response.Redirect($"/account/login?returnUrl={Uri.EscapeDataString(absoluteUri)}");
return Task.CompletedTask; 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 => OnAuthenticationFailed = context =>
@@ -58,6 +90,21 @@ namespace Atomx.Admin.Extensions
} }
return Task.CompletedTask; return Task.CompletedTask;
} }
//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("用户不存在或已被禁用");
// }
// }
//},
}; };
}); });
@@ -85,6 +132,34 @@ namespace Atomx.Admin.Extensions
// policy.Requirements.Add(new PermissionRequirement(PermissionConstants.Users.Edit)); // policy.Requirements.Add(new PermissionRequirement(PermissionConstants.Users.Edit));
//}); //});
}); });
//// HTTP客户端工厂
//services.AddHttpClient("ApiClient")
// .AddTransientHttpErrorPolicy(policy =>
// policy.WaitAndRetryAsync(3, retryAttempt =>
// TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))))
// .AddTransientHttpErrorPolicy(policy =>
// policy.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));
//// API版本控制
//services.AddApiVersioning(options =>
//{
// options.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0);
// options.AssumeDefaultVersionWhenUnspecified = true;
// options.ReportApiVersions = true;
//});
//// 添加响应压缩
//services.AddResponseCompression(options =>
//{
// options.EnableForHttps = true;
// options.Providers.Add<BrotliCompressionProvider>();
// options.Providers.Add<GzipCompressionProvider>();
//});
} }
} }
} }

View File

@@ -0,0 +1,45 @@
using System.Threading.RateLimiting;
namespace Atomx.Admin.Extensions
{
public static class RateLimiterExtension
{
/// <summary>
/// 添加限速
/// </summary>
/// <param name="services"></param>
/// <param name="Configuration"></param>
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
}));
});
}
}
}

View File

@@ -0,0 +1,17 @@
using Atomx.Admin.Middlewares;
namespace Atomx.Admin.Extensions
{
public static class SecurityHeadersExtension
{
/// <summary>
/// 安全头
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IApplicationBuilder UseSecurityHeaders(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SecurityHeadersMiddleware>();
}
}
}

View File

@@ -0,0 +1,33 @@
namespace Atomx.Admin.Extensions
{
public static class SignalRExtension
{
/// <summary>
/// 添加身份验证服务
/// </summary>
/// <param name="services"></param>
/// <param name="Configuration"></param>
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,83 @@
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System.Text.Json;
namespace Atomx.Admin.Middlewares
{
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,53 @@
namespace Atomx.Admin.Middlewares
{
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,9 +14,12 @@ using Blazored.LocalStorage;
using Mapster; using Mapster;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Scalar.AspNetCore; using Scalar.AspNetCore;
using Serilog; using Serilog;
using StackExchange.Redis;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using System.Text.Unicode; using System.Text.Unicode;
@@ -68,7 +71,10 @@ builder.Services.AddScoped<AuthHeaderHandler>();
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost"); builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost");
builder.Services.AddDataService(); builder.Services.AddDataService();
builder.Services.AddAuthorize(builder.Configuration); builder.Services.AddAuthorize(builder.Configuration,builder.Environment);
// <20><><EFBFBD><EFBFBD>SignalR
builder.Services.AddSignalR();
var connection = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); 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"))); builder.Services.AddDbContext<DataContext>(options => options.UseNpgsql(connection, p => p.MigrationsHistoryTable("__DbMigrationsHistory")));
@@ -82,6 +88,8 @@ builder.Services.AddStackExchangeRedisCache(options =>
#endregion #endregion
}); });
builder.Services.AddOpenApi(); builder.Services.AddOpenApi();
builder.Services.AddAntDesign(); builder.Services.AddAntDesign();
@@ -92,6 +100,12 @@ var app = builder.Build();
app.AddDataMigrate(); 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. // Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) if (app.Environment.IsDevelopment())
{ {
@@ -104,7 +118,11 @@ else
app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseExceptionHandler("/Error", createScopeForErrors: true);
} }
//<2F><>ȫͷ
app.UseSecurityHeaders();
// <20><>Ӧѹ<D3A6><D1B9>
app.UseResponseCompression();
app.UseCors(option => app.UseCors(option =>
{ {
@@ -113,7 +131,18 @@ app.UseCors(option =>
option.AllowAnyHeader(); 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.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
@@ -124,10 +153,46 @@ app.MapStaticAssets();
app.UseMiddleware<MonitoringMiddleware>(); 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.MapControllers();
app.MapRazorComponents<App>() app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode() .AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode() .AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(typeof(Atomx.Admin.Client._Imports).Assembly); .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(); app.Run();

View File

@@ -31,6 +31,12 @@ namespace Atomx.Admin.Services
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
int GetTimeZone(); int GetTimeZone();
/// <summary>
/// 获取用户代理信息
/// </summary>
/// <returns></returns>
string GetUserAgent();
} }
/// <summary> /// <summary>
@@ -102,5 +108,10 @@ namespace Atomx.Admin.Services
var timeZone = _httpContextAccessor.HttpContext?.User?.Claims?.SingleOrDefault(p => p.Type == "TimeZone")?.Value ?? "0"; var timeZone = _httpContextAccessor.HttpContext?.User?.Claims?.SingleOrDefault(p => p.Type == "TimeZone")?.Value ?? "0";
return timeZone.ToInt(); return timeZone.ToInt();
} }
public string GetUserAgent()
{
return _httpContextAccessor.HttpContext?.Request.Headers["User-Agent"].FirstOrDefault() ?? "";
}
} }
} }

View File

@@ -16,7 +16,7 @@
"Issuer": "http://api.sampleapi.com", "Issuer": "http://api.sampleapi.com",
"Audience": "SampleApi", "Audience": "SampleApi",
"SecurityKey": "SecurityKey23456SecurityKey23456", "SecurityKey": "SecurityKey23456SecurityKey23456",
"ClockSkew": "600", "ClockSkew": "10", // 10分钟时钟偏差
"AccessTokenExpirationMinutes": "60", "AccessTokenExpirationMinutes": "60",
"RefreshTokenExpirationMinutes": "60" "RefreshTokenExpirationMinutes": "60"
} }

View File

@@ -30,11 +30,13 @@ namespace Atomx.Common.Entities
/// <summary> /// <summary>
/// 发布时间 /// 发布时间
/// </summary> /// </summary>
[Column(TypeName = "timestamptz")]
public DateTime IssuedTime { get; set; } = DateTime.UtcNow; public DateTime IssuedTime { get; set; } = DateTime.UtcNow;
/// <summary> /// <summary>
/// 到期时间 /// 到期时间
/// </summary> /// </summary>
[Column(TypeName = "timestamptz")]
public DateTime ExpiresTime { get; set; } public DateTime ExpiresTime { get; set; }
/// <summary> /// <summary>
@@ -42,6 +44,12 @@ namespace Atomx.Common.Entities
/// </summary> /// </summary>
public bool IsRevoked { get; set; } public bool IsRevoked { get; set; }
/// <summary>
/// 回收时间
/// </summary>
[Column(TypeName = "timestamptz")]
public DateTime? RevokedTime { get; set; }
/// <summary> /// <summary>
/// 用户IP /// 用户IP
/// </summary> /// </summary>

View File

@@ -0,0 +1,23 @@
namespace Atomx.Common.Models
{
/// <summary>
/// 授权响应模型
/// </summary>
public class AuthResponse
{
/// <summary>
/// 令牌
/// </summary>
public string Token { get; set; } = string.Empty;
/// <summary>
/// 刷新令牌
/// </summary>
public string RefreshToken { get; set; } = string.Empty;
/// <summary>
/// 令牌过期时间
/// </summary>
public DateTime TokenExpiry { get; set; }
}
}