fix chore
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
//});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
Atomx.Admin/Atomx.Admin/Extensions/RateLimiterExtension.cs
Normal file
45
Atomx.Admin/Atomx.Admin/Extensions/RateLimiterExtension.cs
Normal 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
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Atomx.Admin/Atomx.Admin/Extensions/SignalRExtension.cs
Normal file
33
Atomx.Admin/Atomx.Admin/Extensions/SignalRExtension.cs
Normal 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";
|
||||||
|
//});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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() ?? "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
23
Atomx.Common/Models/AuthResponse.cs
Normal file
23
Atomx.Common/Models/AuthResponse.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user