fix authorize
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
throw new Exception($"Error: {response.StatusCode}");
|
// 明确在 401/403 场景抛出授权异常以便上层 UI/组件做特殊处理
|
||||||
|
ThrowForStatus(response.StatusCode, url);
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user