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"
|
||||
Options="DefaultOptions" />
|
||||
</SpaceItem> *@
|
||||
<SpaceItem>
|
||||
<Text Type="TextElementType.Warning">@handler</Text>
|
||||
</SpaceItem>
|
||||
<SpaceItem>
|
||||
<AntDesign.Tooltip Title="@("Help")" Placement="@Placement.Bottom">
|
||||
<Unbound>
|
||||
@@ -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<ApiResult<List<MenuDataItem>>>(url);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -32,17 +32,9 @@
|
||||
<Flex Justify="FlexJustify.SpaceBetween">
|
||||
帐号列表
|
||||
<div>
|
||||
<AuthorizePermissionView Permission="@Permissions.User.Create">
|
||||
<AuthorizeCheck Permission="@Permissions.Admin.Create">\
|
||||
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
|
||||
</AuthorizePermissionView>
|
||||
@* <AuthorizeView Policy="@Permissions.Admin.Edit">
|
||||
<Authorized>
|
||||
<Button Class="me-3" OnClick="OnCreateClick" Type="ButtonType.Primary">新增</Button>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
没有权限
|
||||
</NotAuthorized>
|
||||
</AuthorizeView> *@
|
||||
</AuthorizeCheck>
|
||||
</div>
|
||||
</Flex>
|
||||
</TitleTemplate>
|
||||
@@ -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<Admin>(url, search, Page.GetValueOrDefault(1), PageSize.GetValueOrDefault(20));
|
||||
if (apiResult.Success)
|
||||
try
|
||||
{
|
||||
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;
|
||||
StateHasChanged();
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnReset()
|
||||
{
|
||||
search = new();
|
||||
LoadList();
|
||||
_ = LoadList();
|
||||
}
|
||||
|
||||
void OnSearchReset()
|
||||
@@ -298,7 +296,7 @@
|
||||
var apiResult = await HttpService.Post<ApiResult<string>>(url, new());
|
||||
if (apiResult.Success)
|
||||
{
|
||||
LoadList();
|
||||
_ = LoadList();
|
||||
await ModalService.InfoAsync(new ConfirmOptions() { Title = "操作提示", Content = "删除数据成功" });
|
||||
}
|
||||
else
|
||||
|
||||
@@ -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<HttpService> _logger;
|
||||
|
||||
public HttpService(HttpClient httpClient, IHttpContextAccessor? httpContextAccessor = null)
|
||||
public HttpService(HttpClient httpClient, IHttpContextAccessor? httpContextAccessor = null, ILogger<HttpService>? logger = null)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger<HttpService>.Instance;
|
||||
}
|
||||
|
||||
public async Task<T> Get<T>(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 以便上层按需处理
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Atomx.Admin.Client.Utils
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
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<Claim>
|
||||
{
|
||||
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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
<link rel="stylesheet" href="@Assets["Atomx.Admin.styles.css"]" />
|
||||
<ImportMap />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet @rendermode="InteractiveServer" />
|
||||
<HeadOutlet @rendermode="InteractiveAuto" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes @rendermode="InteractiveServer" />
|
||||
<Routes @rendermode="InteractiveAuto" />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -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<AdminController> _logger;
|
||||
@@ -46,6 +49,7 @@ namespace Atomx.Admin.Controllers
|
||||
/// <param name="size"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost("search")]
|
||||
[Authorize(Policy =Permissions.Admin.View)]
|
||||
public IActionResult Search(AdminSearch search, int page, int size = 20)
|
||||
{
|
||||
var startTime = search.RangeTime[0];
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -120,7 +120,7 @@ namespace Atomx.Admin.Controllers
|
||||
var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId);
|
||||
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.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<AuthResponse>().IsSuccess(authResponse));
|
||||
}
|
||||
@@ -261,7 +261,7 @@ namespace Atomx.Admin.Controllers
|
||||
var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId);
|
||||
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.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<AuthResponse>().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)
|
||||
{
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -180,6 +180,7 @@ app.UseAuthorization();
|
||||
app.UseAntiforgery();
|
||||
app.MapStaticAssets();
|
||||
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>䣩
|
||||
// <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);
|
||||
//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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user