diff --git a/Atomx.Admin/Atomx.Admin.Client/Components/AuthorizeCheck.razor b/Atomx.Admin/Atomx.Admin.Client/Components/AuthorizeCheck.razor new file mode 100644 index 0000000..d97aec6 --- /dev/null +++ b/Atomx.Admin/Atomx.Admin.Client/Components/AuthorizeCheck.razor @@ -0,0 +1,90 @@ +@inherits ComponentBase + + + + + @if (_isAuthorized) + { + @ChildContent + } + else if (!string.IsNullOrEmpty(NotAuthorizedContent)) + { + @NotAuthorizedContent + } + + + @if (!string.IsNullOrEmpty(NotAuthenticatedContent)) + { + @NotAuthenticatedContent + } + + + + +@code { + [CascadingParameter] private Task? AuthenticationStateTask { get; set; } + + [Parameter] public RenderFragment? ChildContent { get; set; } + [Parameter] public string? NotAuthorizedContent { get; set; } + [Parameter] public string? NotAuthenticatedContent { get; set; } + + [Parameter] public string? Permission { get; set; } // 单个权限 + [Parameter] public string[]? AnyPermissions { get; set; } // 多个权限 + [Parameter] public string[]? Roles { get; set; } // 多个角色 + [Parameter] public string? Policy { get; set; } // 策略名称 + + private bool _isAuthorized = false; + + protected override async Task OnInitializedAsync() + { + // 如果 Claims 中没有权限信息,使用 PermissionService 异步检查 + if (AuthenticationStateTask != null) + { + var authState = await AuthenticationStateTask; + var user = authState.User; + + if (user.Identity?.IsAuthenticated ?? false) + { + var userPermissions = user.Claims.Where(c => c.Type == ClaimKeys.Permission).Select(c => c.Value).SingleOrDefault()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); + if(userPermissions == null) + { + userPermissions = new List(); + } + // 检查单个权限 + if (Roles?.Length > 0) + { + var hasRole = Roles.Any(role => user.IsInRole(role)); + if (!hasRole) + { + _isAuthorized = true; + return; + } + } + + if (!string.IsNullOrEmpty(Permission)) + { + var hasAllPermissions = userPermissions.Contains(Permission); + if (hasAllPermissions) + { + _isAuthorized = true; + return; + } + } + + if (AnyPermissions?.Length > 0) + { + var hasAnyPermission = AnyPermissions.Any(p => userPermissions.Contains(p)); + if (!hasAnyPermission) + { + _isAuthorized = true; + return; + } + } + } + else + { + _isAuthorized = false; + } + } + } +} diff --git a/Atomx.Admin/Atomx.Admin.Client/Components/AuthorizePermissionView.razor b/Atomx.Admin/Atomx.Admin.Client/Components/AuthorizePermissionView.razor deleted file mode 100644 index 98e0397..0000000 --- a/Atomx.Admin/Atomx.Admin.Client/Components/AuthorizePermissionView.razor +++ /dev/null @@ -1,121 +0,0 @@ -@using Microsoft.AspNetCore.Authorization -@using System.Security.Claims -@inject IPermissionService PermissionService -@inject IAuthorizationService AuthorizationService - - - - - @if (_hasPermission) - { - @ChildContent - } - else if (!string.IsNullOrEmpty(NotAuthorizedContent)) - { - @NotAuthorizedContent - } - - - @if (!string.IsNullOrEmpty(NotAuthenticatedContent)) - { - @NotAuthenticatedContent - } - - - - -@code { - [CascadingParameter] Task? AuthenticationStateTask { get; set; } - - [Parameter] public RenderFragment? ChildContent { get; set; } - [Parameter] public string? Permission { get; set; } - [Parameter] public string[]? Permissions { get; set; } - [Parameter] public bool RequireAll { get; set; } - [Parameter] public string? Policy { get; set; } - [Parameter] public string? NotAuthorizedContent { get; set; } - [Parameter] public string? NotAuthenticatedContent { get; set; } - - private bool _hasPermission; - - protected override async Task OnParametersSetAsync() - { - _hasPermission = false; - - var authState = AuthenticationStateTask is null - ? await Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))) - : await AuthenticationStateTask; - - var user = authState.User; - - if (user?.Identity is null || !user.Identity.IsAuthenticated) - { - _hasPermission = false; - return; - } - - // 优先基于声明快速判断(适用于 Server 与 WASM) - if (!string.IsNullOrEmpty(Permission)) - { - if (user.Claims.Any(c => c.Type == ClaimKeys.Permission && c.Value == Permission)) - { - _hasPermission = true; - return; - } - - // 回退:调用后端权限服务(适用于 Server-side 权限来源于数据库) - _hasPermission = await SafeHasPermissionAsync(Permission); - return; - } - - if (Permissions != null && Permissions.Length > 0) - { - var userPermissions = user.Claims.Where(c => c.Type == ClaimKeys.Permission).Select(c => c.Value).ToHashSet(); - - if (RequireAll) - { - if (Permissions.All(p => userPermissions.Contains(p))) - { - _hasPermission = true; - return; - } - } - else - { - if (Permissions.Any(p => userPermissions.Contains(p))) - { - _hasPermission = true; - return; - } - } - - // 回退:调用后端权限服务 - if (RequireAll) - _hasPermission = await PermissionService.HasAllPermissionsAsync(Permissions); - else - _hasPermission = await PermissionService.HasAnyPermissionAsync(Permissions); - - return; - } - - if (!string.IsNullOrEmpty(Policy)) - { - // 使用 AuthorizationService 并传入当前用户 - var result = await AuthorizationService.AuthorizeAsync(user, Policy); - _hasPermission = result.Succeeded; - return; - } - } - - private async Task SafeHasPermissionAsync(string permission) - { - try - { - return await PermissionService.HasPermissionAsync(permission); - } - catch - { - // 出错时默认拒绝 - return false; - } - } -} \ No newline at end of file diff --git a/Atomx.Admin/Atomx.Admin.Client/Layout/MainLayout.razor b/Atomx.Admin/Atomx.Admin.Client/Layout/MainLayout.razor index e40de20..907969f 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Layout/MainLayout.razor +++ b/Atomx.Admin/Atomx.Admin.Client/Layout/MainLayout.razor @@ -19,6 +19,9 @@ DefaultValue="umi ui" Options="DefaultOptions" /> *@ + + @handler + @@ -62,6 +65,8 @@ @code { + string handler = "Server"; + private ErrorBoundary? _errorBoundary; private void ResetError(Exception ex) @@ -105,6 +110,14 @@ protected async override Task OnInitializedAsync() { + if (OperatingSystem.IsBrowser()) + { + handler = "Wasm"; + } + else + { + handler = "Server"; + } var url = "/api/menu/tree"; var apiResult = await HttpService.Get>>(url); diff --git a/Atomx.Admin/Atomx.Admin.Client/Pages/Login.razor b/Atomx.Admin/Atomx.Admin.Client/Pages/Login.razor index bc2c8ca..829b996 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Pages/Login.razor +++ b/Atomx.Admin/Atomx.Admin.Client/Pages/Login.razor @@ -119,9 +119,21 @@ else var token = dprop.TryGetProperty("token", out var t) ? t.GetString() ?? string.Empty : string.Empty; var refresh = dprop.TryGetProperty("refreshToken", out var r) ? r.GetString() ?? string.Empty : string.Empty; - // WASM 的 localStorage 在 Server Circuit 中无意义,这里不用写 localStorage。 + // WASM 的 localStorage 在 Server Circuit 中无意义,兼容auto模式写入 localStorage。 + try + { + await localStorage.SetItemAsync(StorageKeys.AccessToken, token); + await localStorage.SetItemAsync(StorageKeys.RefreshToken, refresh); + + if (AuthStateProvider is PersistentAuthenticationStateProvider provider) + { + provider.UpdateAuthenticationState(token); + } + } + catch { } + // 浏览器已通过 fetch 收到 Set-Cookie;强制重载使 Circuit 使用新 Cookie。 - Logger.LogInformation("登录成功,server 跳转: {ReturnUrl}", ReturnUrl); + Logger.LogInformation($"登录成功,server 跳转: {ReturnUrl}"); Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true); } else diff --git a/Atomx.Admin/Atomx.Admin.Client/Pages/Logout.razor b/Atomx.Admin/Atomx.Admin.Client/Pages/Logout.razor index 8f9e206..e441cdf 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Pages/Logout.razor +++ b/Atomx.Admin/Atomx.Admin.Client/Pages/Logout.razor @@ -24,7 +24,7 @@ } catch { /* 忽略网络错误,仍继续清理客户端状态 */ } - if (AuthStateProvider is Atomx.Admin.Client.Utils.PersistentAuthenticationStateProvider provider) + if (AuthStateProvider is PersistentAuthenticationStateProvider provider) { await provider.MarkUserAsLoggedOut(); } @@ -41,6 +41,14 @@ var success = jsResult.ValueKind == JsonValueKind.Object && jsResult.TryGetProperty("success", out var sp) && sp.GetBoolean(); Logger.LogInformation("Server logout result: {Success}", success); + try + { + // 清理 localStorage(如果有的话) + await localStorage.RemoveItemAsync(StorageKeys.AccessToken); + await localStorage.RemoveItemAsync(StorageKeys.RefreshToken); + } + catch { } + // 尽管我们可能已经处理了服务器态,强制重新加载确保 Circuit 更新 Navigation.NavigateTo("/account/login", forceLoad: true); } diff --git a/Atomx.Admin/Atomx.Admin.Client/Pages/Systems/AdminList.razor b/Atomx.Admin/Atomx.Admin.Client/Pages/Systems/AdminList.razor index 9323058..730469f 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Pages/Systems/AdminList.razor +++ b/Atomx.Admin/Atomx.Admin.Client/Pages/Systems/AdminList.razor @@ -32,17 +32,9 @@ 帐号列表
- + \ - - @* - - - - - 没有权限 - - *@ +
@@ -182,7 +174,7 @@ { loadQueryString(); - LoadList(); + _ = LoadList(); base.OnParametersSet(); } @@ -213,28 +205,34 @@ } - private async void LoadList() + private async Task LoadList() { loading = true; var url = "/api/admin/search"; - var apiResult = await HttpService.GetPagingList(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20)); - if (apiResult.Success) + try { - if (apiResult.Data != null) + var apiResult = await HttpService.GetPagingList(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20)); + if (apiResult.Success) { - PagingList = apiResult.Data; + if (apiResult.Data != null) + { + PagingList = apiResult.Data; + } } } - loading = false; - StateHasChanged(); + finally + { + loading = false; + StateHasChanged(); + } } private void OnReset() { search = new(); - LoadList(); + _ = LoadList(); } void OnSearchReset() @@ -298,7 +296,7 @@ var apiResult = await HttpService.Post>(url, new()); if (apiResult.Success) { - LoadList(); + _ = LoadList(); await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" }); } else diff --git a/Atomx.Admin/Atomx.Admin.Client/Services/HttpService.cs b/Atomx.Admin/Atomx.Admin.Client/Services/HttpService.cs index 7212b9e..b2c8ba3 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Services/HttpService.cs +++ b/Atomx.Admin/Atomx.Admin.Client/Services/HttpService.cs @@ -1,8 +1,11 @@ using Atomx.Common.Models; using Atomx.Utils.Json; -using System.Net.Http.Json; -using System.Text; using Microsoft.AspNetCore.Http; +using System.Net; +using System.Net.Http.Json; +using System.Security.Claims; +using System.Text; +using System.Text.Json; namespace Atomx.Admin.Client.Services { @@ -10,11 +13,13 @@ namespace Atomx.Admin.Client.Services { private readonly HttpClient _httpClient; private readonly IHttpContextAccessor? _httpContextAccessor; + private readonly ILogger _logger; - public HttpService(HttpClient httpClient, IHttpContextAccessor? httpContextAccessor = null) + public HttpService(HttpClient httpClient, IHttpContextAccessor? httpContextAccessor = null, ILogger? logger = null) { _httpClient = httpClient; _httpContextAccessor = httpContextAccessor; + _logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; } public async Task Get(string url) @@ -29,6 +34,8 @@ namespace Atomx.Admin.Client.Services } else { + await LogNonSuccessAsync(url, response); + ThrowForStatus(response.StatusCode, url); throw new Exception($"Error: {response.StatusCode}"); } } @@ -50,6 +57,8 @@ namespace Atomx.Admin.Client.Services } else { + await LogNonSuccessAsync(url, response); + ThrowForStatus(response.StatusCode, url); throw new Exception($"Error: {response.StatusCode}"); } } @@ -78,13 +87,16 @@ namespace Atomx.Admin.Client.Services } else { - // 明确抛 Unauthorized 以便上层按需处理 - throw new Exception($"Error: {response.StatusCode}"); + await LogNonSuccessAsync(url, response); + // 明确在 401/403 场景抛出授权异常以便上层 UI/组件做特殊处理 + ThrowForStatus(response.StatusCode, url); + throw new Exception($"Error: {response.StatusCode}"); } } catch (HttpRequestException ex) { - Console.WriteLine(ex.ToString()); + _logger.LogError(ex, "HttpRequestException while calling {Url}", url); + Console.Error.WriteLine($"[{DateTime.UtcNow:o}] HttpRequestException Url:{url} Error:{ex.Message}"); throw new Exception($"api {url} service failure"); } } @@ -115,5 +127,64 @@ namespace Atomx.Admin.Client.Services // 忽略任何转发异常,保持健壮性 } } + + private async Task LogNonSuccessAsync(string url, HttpResponseMessage response) + { + try + { + var status = response.StatusCode; + var reason = response.ReasonPhrase; + + string userId = "unknown"; + string ip = "unknown"; + + var ctx = _httpContextAccessor?.HttpContext; + if (ctx != null) + { + userId = ctx.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? ctx.User?.FindFirst("sub")?.Value + ?? "unknown"; + + if (ctx.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff)) + { + ip = xff.ToString().Split(',')[0].Trim(); + } + else + { + ip = ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + } + } + + // 结构化日志记录 + _logger.LogWarning("UserId:{UserId} Url:{Url} Ip:{Ip} HttpStatus:{StatusCode} Reason:{ReasonPhrase}", userId, url, ip, (int)status, reason); + + // 控制台输出一份,便于本地/容器查看 + var consoleMsg = new + { + Timestamp = DateTime.UtcNow.ToString("o"), + Level = "Warning", + UserId = userId, + Url = url, + Ip = ip, + Status = (int)status, + Reason = reason + }; + Console.WriteLine(JsonSerializer.Serialize(consoleMsg)); + } + catch (Exception ex) + { + // 日志失败不能影响主流程 + _logger.LogError(ex, "Failed to log non-success response for {Url}", url); + } + } + + private void ThrowForStatus(HttpStatusCode statusCode, string url) + { + if (statusCode == HttpStatusCode.Unauthorized || statusCode == HttpStatusCode.Forbidden) + { + // 抛出明确的授权异常,便于上层按需处理(例如提示登陆、重定向或显示权限不足) + throw new UnauthorizedAccessException($"Error: {statusCode} when calling {url}"); + } + } } } diff --git a/Atomx.Admin/Atomx.Admin.Client/Utils/PersistentAuthenticationStateProvider.cs b/Atomx.Admin/Atomx.Admin.Client/Utils/PersistentAuthenticationStateProvider.cs index 22f74b2..b539eb9 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Utils/PersistentAuthenticationStateProvider.cs +++ b/Atomx.Admin/Atomx.Admin.Client/Utils/PersistentAuthenticationStateProvider.cs @@ -29,7 +29,7 @@ namespace Atomx.Admin.Client.Utils { var claims = new List { - new(ClaimKeys.Id, userInfo.Id.ToString()), + new(ClaimKeys.UId, userInfo.Id.ToString()), new(ClaimKeys.Name, userInfo.Name), new(ClaimKeys.Email, userInfo.Email), new(ClaimKeys.Mobile, userInfo.MobilePhone), @@ -79,7 +79,7 @@ namespace Atomx.Admin.Client.Utils { var claims = new List { - new(ClaimKeys.Id, customUserClaims.Id.ToString()), + new(ClaimKeys.UId, customUserClaims.Id.ToString()), new(ClaimKeys.Name, customUserClaims.Name), new(ClaimKeys.Email, customUserClaims.Email), new(ClaimKeys.Mobile, customUserClaims.MobilePhone), @@ -114,7 +114,7 @@ namespace Atomx.Admin.Client.Utils var handler = new JwtSecurityTokenHandler(); var token = handler.ReadJwtToken(jwtToken); - var id = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Id)?.Value ?? string.Empty; + var id = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.UId)?.Value ?? string.Empty; var name = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Name)?.Value ?? string.Empty; var email = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Email)?.Value ?? string.Empty; var phone = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Mobile)?.Value ?? string.Empty; diff --git a/Atomx.Admin/Atomx.Admin.Client/_Imports.razor b/Atomx.Admin/Atomx.Admin.Client/_Imports.razor index 5c81061..6a784d7 100644 --- a/Atomx.Admin/Atomx.Admin.Client/_Imports.razor +++ b/Atomx.Admin/Atomx.Admin.Client/_Imports.razor @@ -1,5 +1,6 @@ @using System.Net.Http @using System.Net.Http.Json +@using System.Security.Claims @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms diff --git a/Atomx.Admin/Atomx.Admin/Components/App.razor b/Atomx.Admin/Atomx.Admin/Components/App.razor index bc8e9cc..531e327 100644 --- a/Atomx.Admin/Atomx.Admin/Components/App.razor +++ b/Atomx.Admin/Atomx.Admin/Components/App.razor @@ -11,11 +11,11 @@ - + - + diff --git a/Atomx.Admin/Atomx.Admin/Controllers/AdminController.cs b/Atomx.Admin/Atomx.Admin/Controllers/AdminController.cs index 137ca76..22892fc 100644 --- a/Atomx.Admin/Atomx.Admin/Controllers/AdminController.cs +++ b/Atomx.Admin/Atomx.Admin/Controllers/AdminController.cs @@ -1,11 +1,13 @@ using Atomx.Admin.Client.Models; using Atomx.Admin.Client.Validators; using Atomx.Admin.Services; +using Atomx.Common.Constants; using Atomx.Common.Models; using Atomx.Data; using Atomx.Data.Services; using Atomx.Utils.Extension; using MapsterMapper; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -13,6 +15,7 @@ namespace Atomx.Admin.Controllers { [Route("api/[controller]")] [ApiController] + [Authorize] public class AdminController : ControllerBase { readonly ILogger _logger; @@ -46,6 +49,7 @@ namespace Atomx.Admin.Controllers /// /// [HttpPost("search")] + [Authorize(Policy =Permissions.Admin.View)] public IActionResult Search(AdminSearch search, int page, int size = 20) { var startTime = search.RangeTime[0]; diff --git a/Atomx.Admin/Atomx.Admin/Controllers/RoleController.cs b/Atomx.Admin/Atomx.Admin/Controllers/RoleController.cs index 253d4ce..1599422 100644 --- a/Atomx.Admin/Atomx.Admin/Controllers/RoleController.cs +++ b/Atomx.Admin/Atomx.Admin/Controllers/RoleController.cs @@ -182,6 +182,8 @@ namespace Atomx.Admin.Controllers try { int count = _dbContext.SaveChanges(); + //刷新缓存 + await _cacheService.GetRoleById(data.Id,true); result = result.IsSuccess(count.ToString()); } catch (Exception ex) diff --git a/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs b/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs index d710cb9..184965d 100644 --- a/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs +++ b/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs @@ -120,7 +120,7 @@ namespace Atomx.Admin.Controllers var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId); var claims = new List { - new Claim(ClaimKeys.Id, user.Id.ToString()), + new Claim(ClaimKeys.UId, user.Id.ToString()), new Claim(ClaimKeys.Email, user.Email ?? string.Empty), new Claim(ClaimKeys.Name, user.Username ?? string.Empty), new Claim(ClaimKeys.Role, user.RoleId.ToString()), @@ -201,8 +201,8 @@ namespace Atomx.Admin.Controllers Path = "/" }; - Response.Cookies.Append("accessToken", accessToken, cookieOptions); - Response.Cookies.Append("refreshToken", refreshToken, cookieOptions); + Response.Cookies.Append(StorageKeys.AccessToken, accessToken, cookieOptions); + Response.Cookies.Append(StorageKeys.RefreshToken, refreshToken, cookieOptions); return new JsonResult(new ApiResult().IsSuccess(authResponse)); } @@ -261,7 +261,7 @@ namespace Atomx.Admin.Controllers var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId); var claims = new List { - new Claim(ClaimKeys.Id, user.Id.ToString()), + new Claim(ClaimKeys.UId, user.Id.ToString()), new Claim(ClaimKeys.Email, user.Email ?? string.Empty), new Claim(ClaimKeys.Name, user.Username ?? string.Empty), new Claim(ClaimKeys.Role, user.RoleId.ToString()), @@ -324,8 +324,8 @@ namespace Atomx.Admin.Controllers Path = "/" }; - Response.Cookies.Append("accessToken", accessToken, cookieOptions); - Response.Cookies.Append("refreshToken", refreshToken, cookieOptions); + Response.Cookies.Append(StorageKeys.AccessToken, accessToken, cookieOptions); + Response.Cookies.Append(StorageKeys.RefreshToken, refreshToken, cookieOptions); return new JsonResult(new ApiResult().IsSuccess(authResponse)); @@ -391,8 +391,8 @@ namespace Atomx.Admin.Controllers Path = "/" }; - Response.Cookies.Append("accessToken", string.Empty, expiredOptions); - Response.Cookies.Append("refreshToken", string.Empty, expiredOptions); + Response.Cookies.Append(StorageKeys.AccessToken, string.Empty, expiredOptions); + Response.Cookies.Append(StorageKeys.RefreshToken, string.Empty, expiredOptions); } catch (Exception ex) { diff --git a/Atomx.Admin/Atomx.Admin/Middlewares/ExceptionHandlingMiddleware.cs b/Atomx.Admin/Atomx.Admin/Middlewares/ExceptionHandlingMiddleware.cs index e95cfdd..3e68e20 100644 --- a/Atomx.Admin/Atomx.Admin/Middlewares/ExceptionHandlingMiddleware.cs +++ b/Atomx.Admin/Atomx.Admin/Middlewares/ExceptionHandlingMiddleware.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Mvc; +using Atomx.Common.Constants; +using Atomx.Data.Services; +using Microsoft.AspNetCore.Mvc; using System.Net; using System.Text.Json; @@ -35,9 +37,60 @@ namespace Atomx.Admin.Middlewares } } + private static string GetRequestUserId(HttpContext ctx) + { + var user = ctx.User; + return user?.FindFirst(ClaimKeys.UId)?.Value + ?? user?.FindFirst("sub")?.Value + ?? "unknown"; + } + + private static string GetRequestUrl(HttpContext ctx) + { + return $"{ctx.Request.Method} {ctx.Request.Path}{ctx.Request.QueryString}"; + } + + private static string GetClientIp(HttpContext ctx) + { + if (ctx.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff)) + { + return xff.ToString().Split(',')[0].Trim(); + } + return ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + } + private async Task HandleExceptionAsync(HttpContext context, Exception exception) { - _logger.LogError(exception, "未处理的异常: {Message}", exception.Message); + // 额外收集请求上下文信息 + var userId = GetRequestUserId(context); + var url = GetRequestUrl(context); + var ip = GetClientIp(context); + + // 结构化日志(ILogger) + _logger.LogError(exception, "UserId:{UserId} Url:{Url} Ip:{Ip} Error:{Message}", userId, url, ip, exception.Message); + + // 同时输出到控制台(便于本地/容器日志查看) + try + { + var consoleMsg = new + { + Timestamp = DateTime.UtcNow.ToString("o"), + Level = "Error", + UserId = userId, + Url = url, + Ip = ip, + Message = exception.Message, + Exception = exception.GetType().FullName, + StackTrace = exception.StackTrace, + InnerException = exception.InnerException?.Message + }; + // 简单 JSON 输出,便于日志聚合和搜索 + Console.Error.WriteLine(JsonSerializer.Serialize(consoleMsg)); + } + catch + { + // 忽略控制台写入异常,确保原始异常处理继续 + } var response = context.Response; response.ContentType = "application/json"; diff --git a/Atomx.Admin/Atomx.Admin/Program.cs b/Atomx.Admin/Atomx.Admin/Program.cs index 119efa6..f3da71c 100644 --- a/Atomx.Admin/Atomx.Admin/Program.cs +++ b/Atomx.Admin/Atomx.Admin/Program.cs @@ -180,6 +180,7 @@ app.UseAuthorization(); app.UseAntiforgery(); app.MapStaticAssets(); app.UseMiddleware(); +app.UseMiddleware(); // SignalR endpointsĿ Hubڴ˴ӳ䣩 // Hub ChatHubNotificationHubڴȡעͲӳ diff --git a/Atomx.Admin/Atomx.Admin/Services/IdentityService.cs b/Atomx.Admin/Atomx.Admin/Services/IdentityService.cs index c6706ad..86f23c7 100644 --- a/Atomx.Admin/Atomx.Admin/Services/IdentityService.cs +++ b/Atomx.Admin/Atomx.Admin/Services/IdentityService.cs @@ -64,7 +64,7 @@ namespace Atomx.Admin.Services //var userIdClaim = _httpContextAccessor.HttpContext?.User.FindFirst(ClaimKeys.Id); //return userIdClaim != null ? long.Parse(userIdClaim.Value) : 0; - var id = _httpContextAccessor.HttpContext?.User?.Claims?.SingleOrDefault(p => p.Type == ClaimKeys.Id)?.Value ?? "0"; + var id = _httpContextAccessor.HttpContext?.User?.Claims?.SingleOrDefault(p => p.Type == ClaimKeys.UId)?.Value ?? "0"; return id.ToLong(); } diff --git a/Atomx.Admin/Atomx.Admin/Utils/PersistingRevalidatingAuthenticationStateProvider.cs b/Atomx.Admin/Atomx.Admin/Utils/PersistingRevalidatingAuthenticationStateProvider.cs index 5700e6e..711434f 100644 --- a/Atomx.Admin/Atomx.Admin/Utils/PersistingRevalidatingAuthenticationStateProvider.cs +++ b/Atomx.Admin/Atomx.Admin/Utils/PersistingRevalidatingAuthenticationStateProvider.cs @@ -96,7 +96,7 @@ namespace Atomx.Admin.Utils if (principal.Identity?.IsAuthenticated == true) { - var id = principal.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Id)?.Value ?? string.Empty; + var id = principal.Claims.SingleOrDefault(x => x.Type == ClaimKeys.UId)?.Value ?? string.Empty; var name = principal.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Name)?.Value ?? string.Empty; var email = principal.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Email)?.Value ?? string.Empty; var phone = principal.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Mobile)?.Value ?? string.Empty; diff --git a/Atomx.Common/Constants/ClaimKeys.cs b/Atomx.Common/Constants/ClaimKeys.cs index a3c8eea..2371df9 100644 --- a/Atomx.Common/Constants/ClaimKeys.cs +++ b/Atomx.Common/Constants/ClaimKeys.cs @@ -2,7 +2,7 @@ { public static class ClaimKeys { - public const string Id = "id"; + public const string UId = "sub"; public const string CorporationId = "cid"; public const string Name = "name"; public const string Email = "email";