fix authorize

This commit is contained in:
2025-12-05 00:27:43 +08:00
parent bd95848972
commit 00dd4fa958
18 changed files with 300 additions and 168 deletions

View File

@@ -0,0 +1,90 @@
@inherits ComponentBase
<CascadingAuthenticationState>
<AuthorizeView Context="authContext">
<Authorized>
@if (_isAuthorized)
{
@ChildContent
}
else if (!string.IsNullOrEmpty(NotAuthorizedContent))
{
@NotAuthorizedContent
}
</Authorized>
<NotAuthorized>
@if (!string.IsNullOrEmpty(NotAuthenticatedContent))
{
@NotAuthenticatedContent
}
</NotAuthorized>
</AuthorizeView>
</CascadingAuthenticationState>
@code {
[CascadingParameter] private Task<AuthenticationState>? 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<string>();
}
// 检查单个权限
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;
}
}
}
}

View File

@@ -1,121 +0,0 @@
@using Microsoft.AspNetCore.Authorization
@using System.Security.Claims
@inject IPermissionService PermissionService
@inject IAuthorizationService AuthorizationService
<CascadingAuthenticationState>
<AuthorizeView Context="authContext">
<Authorized>
@if (_hasPermission)
{
@ChildContent
}
else if (!string.IsNullOrEmpty(NotAuthorizedContent))
{
@NotAuthorizedContent
}
</Authorized>
<NotAuthorized>
@if (!string.IsNullOrEmpty(NotAuthenticatedContent))
{
@NotAuthenticatedContent
}
</NotAuthorized>
</AuthorizeView>
</CascadingAuthenticationState>
@code {
[CascadingParameter] Task<AuthenticationState>? 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<bool> SafeHasPermissionAsync(string permission)
{
try
{
return await PermissionService.HasPermissionAsync(permission);
}
catch
{
// 出错时默认拒绝
return false;
}
}
}

View File

@@ -19,6 +19,9 @@
DefaultValue="umi ui" DefaultValue="umi ui"
Options="DefaultOptions" /> Options="DefaultOptions" />
</SpaceItem> *@ </SpaceItem> *@
<SpaceItem>
<Text Type="TextElementType.Warning">@handler</Text>
</SpaceItem>
<SpaceItem> <SpaceItem>
<AntDesign.Tooltip Title="@("Help")" Placement="@Placement.Bottom"> <AntDesign.Tooltip Title="@("Help")" Placement="@Placement.Bottom">
<Unbound> <Unbound>
@@ -62,6 +65,8 @@
@code { @code {
string handler = "Server";
private ErrorBoundary? _errorBoundary; private ErrorBoundary? _errorBoundary;
private void ResetError(Exception ex) private void ResetError(Exception ex)
@@ -105,6 +110,14 @@
protected async override Task OnInitializedAsync() protected async override Task OnInitializedAsync()
{ {
if (OperatingSystem.IsBrowser())
{
handler = "Wasm";
}
else
{
handler = "Server";
}
var url = "/api/menu/tree"; var url = "/api/menu/tree";
var apiResult = await HttpService.Get<ApiResult<List<MenuDataItem>>>(url); var apiResult = await HttpService.Get<ApiResult<List<MenuDataItem>>>(url);

View File

@@ -119,9 +119,21 @@ else
var token = dprop.TryGetProperty("token", out var t) ? t.GetString() ?? string.Empty : string.Empty; 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; 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。 // 浏览器已通过 fetch 收到 Set-Cookie强制重载使 Circuit 使用新 Cookie。
Logger.LogInformation("登录成功server 跳转: {ReturnUrl}", ReturnUrl); Logger.LogInformation($"登录成功server 跳转: {ReturnUrl}");
Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true); Navigation.NavigateTo(ReturnUrl ?? "/", forceLoad: true);
} }
else else

View File

@@ -24,7 +24,7 @@
} }
catch { /* 忽略网络错误,仍继续清理客户端状态 */ } catch { /* 忽略网络错误,仍继续清理客户端状态 */ }
if (AuthStateProvider is Atomx.Admin.Client.Utils.PersistentAuthenticationStateProvider provider) if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
{ {
await provider.MarkUserAsLoggedOut(); await provider.MarkUserAsLoggedOut();
} }
@@ -41,6 +41,14 @@
var success = jsResult.ValueKind == JsonValueKind.Object && jsResult.TryGetProperty("success", out var sp) && sp.GetBoolean(); var success = jsResult.ValueKind == JsonValueKind.Object && jsResult.TryGetProperty("success", out var sp) && sp.GetBoolean();
Logger.LogInformation("Server logout result: {Success}", success); Logger.LogInformation("Server logout result: {Success}", success);
try
{
// 清理 localStorage如果有的话
await localStorage.RemoveItemAsync(StorageKeys.AccessToken);
await localStorage.RemoveItemAsync(StorageKeys.RefreshToken);
}
catch { }
// 尽管我们可能已经处理了服务器态,强制重新加载确保 Circuit 更新 // 尽管我们可能已经处理了服务器态,强制重新加载确保 Circuit 更新
Navigation.NavigateTo("/account/login", forceLoad: true); Navigation.NavigateTo("/account/login", forceLoad: true);
} }

View File

@@ -32,17 +32,9 @@
<Flex Justify="FlexJustify.SpaceBetween"> <Flex Justify="FlexJustify.SpaceBetween">
帐号列表 帐号列表
<div> <div>
<AuthorizePermissionView Permission="@Permissions.User.Create"> <AuthorizeCheck Permission="@Permissions.Admin.Create">\
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button> <Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</AuthorizePermissionView> </AuthorizeCheck>
@* <AuthorizeView Policy="@Permissions.Admin.Edit">
<Authorized>
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
</Authorized>
<NotAuthorized>
没有权限
</NotAuthorized>
</AuthorizeView> *@
</div> </div>
</Flex> </Flex>
</TitleTemplate> </TitleTemplate>
@@ -182,7 +174,7 @@
{ {
loadQueryString(); loadQueryString();
LoadList(); _ = LoadList();
base.OnParametersSet(); base.OnParametersSet();
} }
@@ -213,28 +205,34 @@
} }
private async void LoadList() private async Task LoadList()
{ {
loading = true; loading = true;
var url = "/api/admin/search"; var url = "/api/admin/search";
var apiResult = await HttpService.GetPagingList<Admin>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20)); try
if (apiResult.Success)
{ {
if (apiResult.Data != null) var apiResult = await HttpService.GetPagingList<Admin>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20));
if (apiResult.Success)
{ {
PagingList = apiResult.Data; if (apiResult.Data != null)
{
PagingList = apiResult.Data;
}
} }
} }
loading = false; finally
StateHasChanged(); {
loading = false;
StateHasChanged();
}
} }
private void OnReset() private void OnReset()
{ {
search = new(); search = new();
LoadList(); _ = LoadList();
} }
void OnSearchReset() void OnSearchReset()
@@ -298,7 +296,7 @@
var apiResult = await HttpService.Post<ApiResult<string>>(url, new()); var apiResult = await HttpService.Post<ApiResult<string>>(url, new());
if (apiResult.Success) if (apiResult.Success)
{ {
LoadList(); _ = LoadList();
await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" }); await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" });
} }
else else

View File

@@ -1,8 +1,11 @@
using Atomx.Common.Models; using Atomx.Common.Models;
using Atomx.Utils.Json; using Atomx.Utils.Json;
using System.Net.Http.Json;
using System.Text;
using Microsoft.AspNetCore.Http; 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 namespace Atomx.Admin.Client.Services
{ {
@@ -10,11 +13,13 @@ namespace Atomx.Admin.Client.Services
{ {
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly IHttpContextAccessor? _httpContextAccessor; private readonly IHttpContextAccessor? _httpContextAccessor;
private readonly ILogger<HttpService> _logger;
public HttpService(HttpClient httpClient, IHttpContextAccessor? httpContextAccessor = null) public HttpService(HttpClient httpClient, IHttpContextAccessor? httpContextAccessor = null, ILogger<HttpService>? logger = null)
{ {
_httpClient = httpClient; _httpClient = httpClient;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<HttpService>.Instance;
} }
public async Task<T> Get<T>(string url) public async Task<T> Get<T>(string url)
@@ -29,6 +34,8 @@ namespace Atomx.Admin.Client.Services
} }
else else
{ {
await LogNonSuccessAsync(url, response);
ThrowForStatus(response.StatusCode, url);
throw new Exception($"Error: {response.StatusCode}"); throw new Exception($"Error: {response.StatusCode}");
} }
} }
@@ -50,6 +57,8 @@ namespace Atomx.Admin.Client.Services
} }
else else
{ {
await LogNonSuccessAsync(url, response);
ThrowForStatus(response.StatusCode, url);
throw new Exception($"Error: {response.StatusCode}"); throw new Exception($"Error: {response.StatusCode}");
} }
} }
@@ -78,13 +87,16 @@ namespace Atomx.Admin.Client.Services
} }
else else
{ {
// 明确抛 Unauthorized 以便上层按需处理 await LogNonSuccessAsync(url, response);
// 明确在 401/403 场景抛出授权异常以便上层 UI/组件做特殊处理
ThrowForStatus(response.StatusCode, url);
throw new Exception($"Error: {response.StatusCode}"); throw new Exception($"Error: {response.StatusCode}");
} }
} }
catch (HttpRequestException ex) 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"); 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}");
}
}
} }
} }

View File

@@ -29,7 +29,7 @@ namespace Atomx.Admin.Client.Utils
{ {
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(ClaimKeys.Id, userInfo.Id.ToString()), new(ClaimKeys.UId, userInfo.Id.ToString()),
new(ClaimKeys.Name, userInfo.Name), new(ClaimKeys.Name, userInfo.Name),
new(ClaimKeys.Email, userInfo.Email), new(ClaimKeys.Email, userInfo.Email),
new(ClaimKeys.Mobile, userInfo.MobilePhone), new(ClaimKeys.Mobile, userInfo.MobilePhone),
@@ -79,7 +79,7 @@ namespace Atomx.Admin.Client.Utils
{ {
var claims = new List<Claim> var claims = new List<Claim>
{ {
new(ClaimKeys.Id, customUserClaims.Id.ToString()), new(ClaimKeys.UId, customUserClaims.Id.ToString()),
new(ClaimKeys.Name, customUserClaims.Name), new(ClaimKeys.Name, customUserClaims.Name),
new(ClaimKeys.Email, customUserClaims.Email), new(ClaimKeys.Email, customUserClaims.Email),
new(ClaimKeys.Mobile, customUserClaims.MobilePhone), new(ClaimKeys.Mobile, customUserClaims.MobilePhone),
@@ -114,7 +114,7 @@ namespace Atomx.Admin.Client.Utils
var handler = new JwtSecurityTokenHandler(); var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(jwtToken); 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 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 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; var phone = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Mobile)?.Value ?? string.Empty;

View File

@@ -1,5 +1,6 @@
@using System.Net.Http @using System.Net.Http
@using System.Net.Http.Json @using System.Net.Http.Json
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms

View File

@@ -11,11 +11,11 @@
<link rel="stylesheet" href="@Assets["Atomx.Admin.styles.css"]" /> <link rel="stylesheet" href="@Assets["Atomx.Admin.styles.css"]" />
<ImportMap /> <ImportMap />
<link rel="icon" type="image/png" href="favicon.png" /> <link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveServer" /> <HeadOutlet @rendermode="InteractiveAuto" />
</head> </head>
<body> <body>
<Routes @rendermode="InteractiveServer" /> <Routes @rendermode="InteractiveAuto" />
<script src="_framework/blazor.web.js"></script> <script src="_framework/blazor.web.js"></script>
</body> </body>

View File

@@ -1,11 +1,13 @@
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.Common.Constants;
using Atomx.Common.Models; using Atomx.Common.Models;
using Atomx.Data; using Atomx.Data;
using Atomx.Data.Services; using Atomx.Data.Services;
using Atomx.Utils.Extension; using Atomx.Utils.Extension;
using MapsterMapper; using MapsterMapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -13,6 +15,7 @@ namespace Atomx.Admin.Controllers
{ {
[Route("api/[controller]")] [Route("api/[controller]")]
[ApiController] [ApiController]
[Authorize]
public class AdminController : ControllerBase public class AdminController : ControllerBase
{ {
readonly ILogger<AdminController> _logger; readonly ILogger<AdminController> _logger;
@@ -46,6 +49,7 @@ namespace Atomx.Admin.Controllers
/// <param name="size"></param> /// <param name="size"></param>
/// <returns></returns> /// <returns></returns>
[HttpPost("search")] [HttpPost("search")]
[Authorize(Policy =Permissions.Admin.View)]
public IActionResult Search(AdminSearch search, int page, int size = 20) public IActionResult Search(AdminSearch search, int page, int size = 20)
{ {
var startTime = search.RangeTime[0]; var startTime = search.RangeTime[0];

View File

@@ -182,6 +182,8 @@ namespace Atomx.Admin.Controllers
try try
{ {
int count = _dbContext.SaveChanges(); int count = _dbContext.SaveChanges();
//刷新缓存
await _cacheService.GetRoleById(data.Id,true);
result = result.IsSuccess(count.ToString()); result = result.IsSuccess(count.ToString());
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -120,7 +120,7 @@ namespace Atomx.Admin.Controllers
var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId); var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId);
var claims = new List<Claim> var claims = new List<Claim>
{ {
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.Email, user.Email ?? string.Empty),
new Claim(ClaimKeys.Name, user.Username ?? string.Empty), new Claim(ClaimKeys.Name, user.Username ?? string.Empty),
new Claim(ClaimKeys.Role, user.RoleId.ToString()), new Claim(ClaimKeys.Role, user.RoleId.ToString()),
@@ -201,8 +201,8 @@ namespace Atomx.Admin.Controllers
Path = "/" Path = "/"
}; };
Response.Cookies.Append("accessToken", accessToken, cookieOptions); Response.Cookies.Append(StorageKeys.AccessToken, accessToken, cookieOptions);
Response.Cookies.Append("refreshToken", refreshToken, cookieOptions); Response.Cookies.Append(StorageKeys.RefreshToken, refreshToken, cookieOptions);
return new JsonResult(new ApiResult<AuthResponse>().IsSuccess(authResponse)); return new JsonResult(new ApiResult<AuthResponse>().IsSuccess(authResponse));
} }
@@ -261,7 +261,7 @@ namespace Atomx.Admin.Controllers
var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId); var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId);
var claims = new List<Claim> var claims = new List<Claim>
{ {
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.Email, user.Email ?? string.Empty),
new Claim(ClaimKeys.Name, user.Username ?? string.Empty), new Claim(ClaimKeys.Name, user.Username ?? string.Empty),
new Claim(ClaimKeys.Role, user.RoleId.ToString()), new Claim(ClaimKeys.Role, user.RoleId.ToString()),
@@ -324,8 +324,8 @@ namespace Atomx.Admin.Controllers
Path = "/" Path = "/"
}; };
Response.Cookies.Append("accessToken", accessToken, cookieOptions); Response.Cookies.Append(StorageKeys.AccessToken, accessToken, cookieOptions);
Response.Cookies.Append("refreshToken", refreshToken, cookieOptions); Response.Cookies.Append(StorageKeys.RefreshToken, refreshToken, cookieOptions);
return new JsonResult(new ApiResult<AuthResponse>().IsSuccess(authResponse)); return new JsonResult(new ApiResult<AuthResponse>().IsSuccess(authResponse));
@@ -391,8 +391,8 @@ namespace Atomx.Admin.Controllers
Path = "/" Path = "/"
}; };
Response.Cookies.Append("accessToken", string.Empty, expiredOptions); Response.Cookies.Append(StorageKeys.AccessToken, string.Empty, expiredOptions);
Response.Cookies.Append("refreshToken", string.Empty, expiredOptions); Response.Cookies.Append(StorageKeys.RefreshToken, string.Empty, expiredOptions);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -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.Net;
using System.Text.Json; 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) 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; var response = context.Response;
response.ContentType = "application/json"; response.ContentType = "application/json";

View File

@@ -180,6 +180,7 @@ app.UseAuthorization();
app.UseAntiforgery(); app.UseAntiforgery();
app.MapStaticAssets(); app.MapStaticAssets();
app.UseMiddleware<MonitoringMiddleware>(); app.UseMiddleware<MonitoringMiddleware>();
app.UseMiddleware<ExceptionHandlingMiddleware>();
// SignalR endpoints<74><73><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD> Hub<75><62><EFBFBD><EFBFBD><EFBFBD>ڴ˴<DAB4>ӳ<EFBFBD> // SignalR endpoints<74><73><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD> Hub<75><62><EFBFBD><EFBFBD><EFBFBD>ڴ˴<DAB4>ӳ<EFBFBD>
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Hub <20><><EFBFBD><EFBFBD> ChatHub<75><62>NotificationHub<75><62><EFBFBD><EFBFBD><EFBFBD>ڴ<EFBFBD>ȡ<EFBFBD><C8A1>ע<EFBFBD>Ͳ<EFBFBD>ӳ<EFBFBD><D3B3> // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Hub <20><><EFBFBD><EFBFBD> ChatHub<75><62>NotificationHub<75><62><EFBFBD><EFBFBD><EFBFBD>ڴ<EFBFBD>ȡ<EFBFBD><C8A1>ע<EFBFBD>Ͳ<EFBFBD>ӳ<EFBFBD><D3B3>

View File

@@ -64,7 +64,7 @@ namespace Atomx.Admin.Services
//var userIdClaim = _httpContextAccessor.HttpContext?.User.FindFirst(ClaimKeys.Id); //var userIdClaim = _httpContextAccessor.HttpContext?.User.FindFirst(ClaimKeys.Id);
//return userIdClaim != null ? long.Parse(userIdClaim.Value) : 0; //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(); return id.ToLong();
} }

View File

@@ -96,7 +96,7 @@ namespace Atomx.Admin.Utils
if (principal.Identity?.IsAuthenticated == true) 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 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 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; var phone = principal.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Mobile)?.Value ?? string.Empty;

View File

@@ -2,7 +2,7 @@
{ {
public static class ClaimKeys public static class ClaimKeys
{ {
public const string Id = "id"; public const string UId = "sub";
public const string CorporationId = "cid"; public const string CorporationId = "cid";
public const string Name = "name"; public const string Name = "name";
public const string Email = "email"; public const string Email = "email";