添加项目文件。
This commit is contained in:
71
Atomx.Admin/Atomx.Admin.Client/Utils/AuthHeaderHandler.cs
Normal file
71
Atomx.Admin/Atomx.Admin.Client/Utils/AuthHeaderHandler.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace Atomx.Admin.Client.Utils
|
||||
{
|
||||
public class AuthHeaderHandler : DelegatingHandler
|
||||
{
|
||||
private readonly ITokenProvider _tokenProvider;
|
||||
private readonly NavigationManager _navigationManager;
|
||||
private readonly ILogger<AuthHeaderHandler> _logger;
|
||||
|
||||
public AuthHeaderHandler(
|
||||
ITokenProvider tokenProvider,
|
||||
NavigationManager navigationManager,
|
||||
ILogger<AuthHeaderHandler> logger)
|
||||
{
|
||||
_tokenProvider = tokenProvider;
|
||||
_navigationManager = navigationManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 获取token
|
||||
var token = await _tokenProvider.GetTokenAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
request.Headers.Authorization =
|
||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("No authentication token available for request: {Url}", request.RequestUri);
|
||||
}
|
||||
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
// 处理认证失败
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
await HandleUnauthorizedAsync();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending HTTP request to {Url}", request.RequestUri);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleUnauthorizedAsync()
|
||||
{
|
||||
// 在WASM模式下重定向到登录页
|
||||
if (OperatingSystem.IsBrowser())
|
||||
{
|
||||
_navigationManager.NavigateTo("/account/login", true);
|
||||
}
|
||||
// 在Server模式下可以执行其他操作
|
||||
else
|
||||
{
|
||||
// Server端的处理逻辑
|
||||
_logger.LogWarning("Unauthorized access detected in server mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Atomx.Admin/Atomx.Admin.Client/Utils/HttpClientExtension.cs
Normal file
20
Atomx.Admin/Atomx.Admin.Client/Utils/HttpClientExtension.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Atomx.Admin.Client.Services;
|
||||
|
||||
namespace Atomx.Admin.Client.Utils
|
||||
{
|
||||
public static class HttpClientExtension
|
||||
{
|
||||
public static void AddHttpClientApiService(this IServiceCollection services, string baseAddress)
|
||||
{
|
||||
services.AddHttpClient<HttpService>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(baseAddress);
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json");
|
||||
|
||||
}).AddHttpMessageHandler<AuthHeaderHandler>();
|
||||
|
||||
//services.AddHttpClient<AdminLoginLogService>(client => client.BaseAddress = new Uri(baseAddress));
|
||||
//services.AddHttpClient<AdminService>(client => client.BaseAddress = new Uri(baseAddress));
|
||||
}
|
||||
}
|
||||
}
|
||||
198
Atomx.Admin/Atomx.Admin.Client/Utils/IconsExtension.cs
Normal file
198
Atomx.Admin/Atomx.Admin.Client/Utils/IconsExtension.cs
Normal file
@@ -0,0 +1,198 @@
|
||||
using AntDesign;
|
||||
|
||||
namespace Atomx.Admin.Client.Utils
|
||||
{
|
||||
public class IconsExtension
|
||||
{
|
||||
public static List<string> IconList()
|
||||
{
|
||||
return new List<string>()
|
||||
{
|
||||
"step-backward",
|
||||
"step-forward",
|
||||
"fast-backward",
|
||||
"fast-forward",
|
||||
"shrink",
|
||||
"arrows-alt",
|
||||
"down",
|
||||
"up",
|
||||
"left",
|
||||
"right",
|
||||
"caret-up",
|
||||
"caret-down",
|
||||
"caret-left",
|
||||
"caret-right",
|
||||
"up-circle",
|
||||
"down-circle",
|
||||
"left-circle",
|
||||
"right-circle",
|
||||
"double-right",
|
||||
"double-left",
|
||||
"vertical-left",
|
||||
"vertical-right",
|
||||
"vertical-align-top",
|
||||
"vertical-align-middle",
|
||||
"vertical-align-bottom",
|
||||
"forward",
|
||||
"backward",
|
||||
"rollback",
|
||||
"enter",
|
||||
"retweet",
|
||||
"swap",
|
||||
"swap-left",
|
||||
"swap-right",
|
||||
"arrow-up",
|
||||
"arrow-down",
|
||||
"arrow-left",
|
||||
"arrow-right",
|
||||
"play-circle",
|
||||
"up-square",
|
||||
"down-square",
|
||||
"left-square",
|
||||
"right-square",
|
||||
"login",
|
||||
"logout",
|
||||
"menu-fold",
|
||||
"menu-unfold",
|
||||
"border-bottom",
|
||||
"border-horizontal",
|
||||
"border-inner",
|
||||
"border-outer",
|
||||
"border-left",
|
||||
"border-right",
|
||||
"border-top",
|
||||
"border-verticle",
|
||||
"pic-center",
|
||||
"pic-left",
|
||||
"pic-right",
|
||||
"radius-bottomleft",
|
||||
"radius-bottomright",
|
||||
"radius-upleft",
|
||||
"radius-upright",
|
||||
"fullscreen",
|
||||
"fullscreen-exit",
|
||||
"question",
|
||||
"question-circle",
|
||||
"plus",
|
||||
"plus-circle",
|
||||
"pause",
|
||||
"pause-circle",
|
||||
"minus",
|
||||
"minus-circle",
|
||||
"plus-square",
|
||||
"minus-square",
|
||||
"info",
|
||||
"info-circle",
|
||||
"exclamation",
|
||||
"exclamation-circle",
|
||||
"close",
|
||||
"close-circle",
|
||||
"close-square",
|
||||
"check",
|
||||
"check-circle",
|
||||
"check-square",
|
||||
"clock-circle",
|
||||
"warning",
|
||||
"issues-close",
|
||||
"stop",
|
||||
"edit",
|
||||
"form",
|
||||
"copy",
|
||||
"scissor",
|
||||
"delete",
|
||||
"snippets",
|
||||
"diff",
|
||||
"highlight",
|
||||
"align-center",
|
||||
"align-left",
|
||||
"align-right",
|
||||
"bg-colors",
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"strikethrough",
|
||||
"redo",
|
||||
"undo",
|
||||
"zoom-in",
|
||||
"zoom-out",
|
||||
"font-colors",
|
||||
"font-size",
|
||||
"line-height",
|
||||
"colum-height",
|
||||
"colum-width",
|
||||
"dash",
|
||||
"small-dash",
|
||||
"sort-ascending",
|
||||
"sort-descending",
|
||||
"drag",
|
||||
"ordered-list",
|
||||
"unordered-list",
|
||||
"radius-setting",
|
||||
"column-width",
|
||||
"area-chart",
|
||||
"pie-chart",
|
||||
"bar-chart",
|
||||
"dot-chart",
|
||||
"line-chart",
|
||||
"radar-chart",
|
||||
"heat-map",
|
||||
"fall",
|
||||
"rise",
|
||||
"stock",
|
||||
"box-plot",
|
||||
"fund",
|
||||
"sliders",
|
||||
"android",
|
||||
"apple",
|
||||
"windows",
|
||||
"ie",
|
||||
"chrome",
|
||||
"github",
|
||||
"aliwangwang",
|
||||
"dingding",
|
||||
"weibo-square",
|
||||
"weibo-circle",
|
||||
"taobao-circle",
|
||||
"html5",
|
||||
"weibo",
|
||||
"twitter",
|
||||
"wechat",
|
||||
"youtube",
|
||||
"alipay-circle",
|
||||
"taobao",
|
||||
"skype",
|
||||
"qq",
|
||||
"medium-workmark",
|
||||
"gitlab",
|
||||
"medium",
|
||||
"linkedin",
|
||||
"google-plus",
|
||||
"dropbox",
|
||||
"facebook",
|
||||
"codepen",
|
||||
"code-sandbox",
|
||||
"code-sandbox-circle",
|
||||
"amazon",
|
||||
"google",
|
||||
"codepen-circle",
|
||||
"alipay",
|
||||
"ant-design",
|
||||
"ant-cloud",
|
||||
"aliyun",
|
||||
"zhihu",
|
||||
"slack",
|
||||
"slack-square",
|
||||
"behance",
|
||||
"behance-square",
|
||||
"dribbble",
|
||||
"dribbble-square",
|
||||
"instagram",
|
||||
"yuque",
|
||||
"alibaba",
|
||||
"yahoo",
|
||||
"reddit",
|
||||
"sketch"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using Blazored.LocalStorage;
|
||||
using Atomx.Common.Configuration;
|
||||
using Atomx.Common.Utils;
|
||||
using Atomx.Utils.Extension;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Atomx.Admin.Client.Utils
|
||||
{
|
||||
public class PersistentAuthenticationStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
readonly ClaimsPrincipal anonymous = new(new ClaimsIdentity());
|
||||
|
||||
static readonly Task<AuthenticationState> defaultUnauthenticatedTask = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
|
||||
readonly Task<AuthenticationState> authenticationStateTask = defaultUnauthenticatedTask;
|
||||
|
||||
readonly ILocalStorageService _localStorage;
|
||||
|
||||
public PersistentAuthenticationStateProvider(PersistentComponentState state, ILocalStorageService localStorageService)
|
||||
{
|
||||
_localStorage = localStorageService;
|
||||
|
||||
if (!state.TryTakeFromJson<UserInfo>(nameof(UserInfo), out var userInfo) || userInfo is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimKeys.Id, userInfo.Id.ToString()),
|
||||
new(ClaimKeys.Name, userInfo.Name),
|
||||
new(ClaimKeys.Email, userInfo.Email),
|
||||
new(ClaimKeys.Mobile, userInfo.MobilePhone),
|
||||
new(ClaimKeys.Role, userInfo.Role),
|
||||
};
|
||||
foreach (var role in userInfo.Permissions)
|
||||
{
|
||||
claims.Add(new Claim(ClaimKeys.Permission, role));
|
||||
}
|
||||
|
||||
authenticationStateTask = Task.FromResult(
|
||||
new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(PersistentAuthenticationStateProvider)))));
|
||||
}
|
||||
|
||||
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var jwtToken = await _localStorage.GetItemAsStringAsync(StorageKeys.JWTTokenKeyName);
|
||||
if (string.IsNullOrEmpty(jwtToken))
|
||||
return await Task.FromResult(new AuthenticationState(anonymous));
|
||||
else
|
||||
{
|
||||
var getUserClaims = DecryptToken(jwtToken);
|
||||
if (getUserClaims == null)
|
||||
return await Task.FromResult(new AuthenticationState(anonymous));
|
||||
else
|
||||
{
|
||||
var claimsPrincipal = SetClaimPrincipal(getUserClaims);
|
||||
return await Task.FromResult(new AuthenticationState(claimsPrincipal));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return await Task.FromResult(new AuthenticationState(anonymous));
|
||||
}
|
||||
}
|
||||
|
||||
public ClaimsPrincipal SetClaimPrincipal(UserInfo customUserClaims)
|
||||
{
|
||||
if (string.IsNullOrEmpty(customUserClaims.Name))
|
||||
return new ClaimsPrincipal();
|
||||
else
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimKeys.Id, customUserClaims.Id.ToString()),
|
||||
new(ClaimKeys.Name, customUserClaims.Name),
|
||||
new(ClaimKeys.Email, customUserClaims.Email),
|
||||
new(ClaimKeys.Mobile, customUserClaims.MobilePhone),
|
||||
new(ClaimKeys.Role, customUserClaims.Role.ToString()),
|
||||
};
|
||||
foreach (var role in customUserClaims.Permissions)
|
||||
{
|
||||
claims.Add(new Claim(ClaimKeys.Permission, role));
|
||||
}
|
||||
return new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(PersistentAuthenticationStateProvider)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void UpdateAuthenticationState(string jwtToken = "")
|
||||
{
|
||||
var claimsPrincipal = new ClaimsPrincipal();
|
||||
if (!string.IsNullOrEmpty(jwtToken))
|
||||
{
|
||||
var getUserClaims = DecryptToken(jwtToken);
|
||||
claimsPrincipal = SetClaimPrincipal(getUserClaims);
|
||||
}
|
||||
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(claimsPrincipal)));
|
||||
}
|
||||
|
||||
private UserInfo DecryptToken(string jwtToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(jwtToken))
|
||||
return new UserInfo();
|
||||
else
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var token = handler.ReadJwtToken(jwtToken);
|
||||
|
||||
|
||||
|
||||
var id = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Id)?.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;
|
||||
var role = token.Claims.SingleOrDefault(x => x.Type == ClaimKeys.Role)?.Value ?? string.Empty;
|
||||
var permissions = token.Claims.Where(x => x.Type == ClaimKeys.Permission).Select(s => s.Value).ToArray();
|
||||
|
||||
return new UserInfo(id.ToLong(), name, email, phone, role, permissions);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task MarkUserAsLoggedOut()
|
||||
{
|
||||
await _localStorage.RemoveItemAsync(StorageKeys.JWTTokenKeyName);
|
||||
await _localStorage.RemoveItemAsync(StorageKeys.RefreshTokenKeyName);
|
||||
|
||||
var authState = Task.FromResult(new AuthenticationState(anonymous));
|
||||
|
||||
NotifyAuthenticationStateChanged(authState);
|
||||
}
|
||||
}
|
||||
|
||||
public record UserInfo(long Id = 0, string Name = null!, string Email = null!, string MobilePhone = null!, string Role = null!, string[] Permissions = null!);
|
||||
}
|
||||
40
Atomx.Admin/Atomx.Admin.Client/Utils/TokenProvider.cs
Normal file
40
Atomx.Admin/Atomx.Admin.Client/Utils/TokenProvider.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Atomx.Common.Configuration;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Atomx.Admin.Client.Utils
|
||||
{
|
||||
public interface ITokenProvider
|
||||
{
|
||||
Task<string?> GetTokenAsync();
|
||||
Task<bool> IsTokenValidAsync();
|
||||
}
|
||||
|
||||
public class ClientTokenProvider : ITokenProvider
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
|
||||
public ClientTokenProvider(IJSRuntime jsRuntime)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public async Task<string?> GetTokenAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从localStorage或sessionStorage获取token
|
||||
return await _jsRuntime.InvokeAsync<string>("localStorage.getItem", StorageKeys.JWTTokenKeyName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsTokenValidAsync()
|
||||
{
|
||||
var token = await GetTokenAsync();
|
||||
return !string.IsNullOrEmpty(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user