From 00dd4fa9587250ce200dfb2a0d0dc8f8842f36d2 Mon Sep 17 00:00:00 2001
From: Seany <17074267@qq.com>
Date: Fri, 5 Dec 2025 00:27:43 +0800
Subject: [PATCH] fix authorize
---
.../Components/AuthorizeCheck.razor | 90 +++++++++++++
.../Components/AuthorizePermissionView.razor | 121 ------------------
.../Layout/MainLayout.razor | 13 ++
.../Atomx.Admin.Client/Pages/Login.razor | 16 ++-
.../Atomx.Admin.Client/Pages/Logout.razor | 10 +-
.../Pages/Systems/AdminList.razor | 38 +++---
.../Services/HttpService.cs | 83 +++++++++++-
.../PersistentAuthenticationStateProvider.cs | 6 +-
Atomx.Admin/Atomx.Admin.Client/_Imports.razor | 1 +
Atomx.Admin/Atomx.Admin/Components/App.razor | 4 +-
.../Controllers/AdminController.cs | 4 +
.../Atomx.Admin/Controllers/RoleController.cs | 2 +
.../Atomx.Admin/Controllers/SignController.cs | 16 +--
.../ExceptionHandlingMiddleware.cs | 57 ++++++++-
Atomx.Admin/Atomx.Admin/Program.cs | 1 +
.../Atomx.Admin/Services/IdentityService.cs | 2 +-
...RevalidatingAuthenticationStateProvider.cs | 2 +-
Atomx.Common/Constants/ClaimKeys.cs | 2 +-
18 files changed, 300 insertions(+), 168 deletions(-)
create mode 100644 Atomx.Admin/Atomx.Admin.Client/Components/AuthorizeCheck.razor
delete mode 100644 Atomx.Admin/Atomx.Admin.Client/Components/AuthorizePermissionView.razor
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";