chore
This commit is contained in:
@@ -20,6 +20,6 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 是否记住我
|
/// 是否记住我
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool SaveMe { get; set; }
|
public bool RememberMe { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
@layout EmptyLayout
|
@layout EmptyLayout
|
||||||
@inject ILogger<Login> Logger
|
@inject ILogger<Login> Logger
|
||||||
|
|
||||||
|
|
||||||
<PageTitle>登录</PageTitle>
|
<PageTitle>登录</PageTitle>
|
||||||
|
|
||||||
@if (!dataLoaded)
|
@if (!dataLoaded)
|
||||||
@@ -67,7 +66,6 @@ else
|
|||||||
|
|
||||||
private bool _isLoading = false;
|
private bool _isLoading = false;
|
||||||
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
if (OperatingSystem.IsBrowser())
|
if (OperatingSystem.IsBrowser())
|
||||||
@@ -85,12 +83,9 @@ else
|
|||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||||
if (authState.User.Identity != null)
|
if (authState.User.Identity != null && authState.User.Identity.IsAuthenticated)
|
||||||
{
|
{
|
||||||
if (authState.User.Identity.IsAuthenticated)
|
Navigation.NavigateTo(ReturnUrl ?? "/");
|
||||||
{
|
|
||||||
Navigation.NavigateTo(ReturnUrl ?? "/");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!dataLoaded)
|
if (!dataLoaded)
|
||||||
@@ -102,32 +97,49 @@ else
|
|||||||
|
|
||||||
private async Task LoginAsync()
|
private async Task LoginAsync()
|
||||||
{
|
{
|
||||||
if (form.Validate())
|
if (!form.Validate()) return;
|
||||||
|
|
||||||
|
_isLoading = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
|
// 请求后端登录接口,后端返回 ApiResult<AuthResponse>
|
||||||
var api = "/api/sign/in";
|
var api = "/api/sign/in";
|
||||||
var result = await HttpService.Post<ApiResult<string>>(api, login);
|
var result = await HttpService.Post<ApiResult<AuthResponse>>(api, login);
|
||||||
if (result.Success)
|
if (result.Success && result.Data != null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("请求api成功");
|
var auth = result.Data;
|
||||||
if (!string.IsNullOrEmpty(result.Data))
|
|
||||||
|
// 保存 access + refresh 到 localStorage(WASM 场景)
|
||||||
|
await localStorage.SetItemAsync("accessToken", auth.Token);
|
||||||
|
await localStorage.SetItemAsync("refreshToken", auth.RefreshToken);
|
||||||
|
|
||||||
|
// 更新客户端 AuthenticationState(调用自定义 Provider 更新方法)
|
||||||
|
if (AuthStateProvider is PersistentAuthenticationStateProvider provider)
|
||||||
{
|
{
|
||||||
await localStorage.SetItemAsStringAsync(StorageKeys.AccessToken, result.Data);
|
// provider 仅需要 access token 更新来触发 UI 更新
|
||||||
await localStorage.SetItemAsStringAsync("refreshToken", result.Data);
|
provider.UpdateAuthenticationState(auth.Token);
|
||||||
var authState = (AuthStateProvider as PersistentAuthenticationStateProvider);
|
|
||||||
if (authState != null)
|
|
||||||
{
|
|
||||||
authState.UpdateAuthenticationState(result.Data);
|
|
||||||
}
|
|
||||||
Logger.LogInformation($"登录成功跳转目标,{ReturnUrl}");
|
|
||||||
Navigation.NavigateTo(ReturnUrl ?? "/");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger.LogInformation("登录成功,跳转: {ReturnUrl}", ReturnUrl);
|
||||||
|
Navigation.NavigateTo(ReturnUrl ?? "/");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ModalService.Error(new ConfirmOptions() { Title = "提示", Content = result.Message });
|
ModalService.Error(new ConfirmOptions() { Title = "提示", Content = result.Message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "登录失败");
|
||||||
|
ModalService.Error(new ConfirmOptions() { Title = "错误", Content = "登录异常,请稍后重试" });
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isLoading = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnPasswordKeyDown(KeyboardEventArgs value)
|
private async Task OnPasswordKeyDown(KeyboardEventArgs value)
|
||||||
|
|||||||
@@ -6,26 +6,26 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
|||||||
|
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
// ע<>᱾<EFBFBD>ش洢<D8B4><E6B4A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
// ע<>᱾<EFBFBD>ش洢<D8B4><E6B4A2>WASM ʹ<><CAB9> localStorage <20><><EFBFBD><EFBFBD> tokens<EFBFBD><EFBFBD>
|
||||||
builder.Services.AddBlazoredLocalStorageAsSingleton();
|
builder.Services.AddBlazoredLocalStorageAsSingleton();
|
||||||
|
|
||||||
// <20><>Ȩ/<2F><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
// <20><>Ȩ/<2F><><EFBFBD>ݣ<EFBFBD>WASM ʹ<><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthenticationStateProvider<EFBFBD><EFBFBD>
|
||||||
builder.Services.AddAuthorizationCore();
|
builder.Services.AddAuthorizationCore();
|
||||||
builder.Services.AddCascadingAuthenticationState();
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();
|
builder.Services.AddSingleton<AuthenticationStateProvider, PersistentAuthenticationStateProvider>();
|
||||||
|
|
||||||
// Ȩ<><C8A8> & <20><><EFBFBD>ػ<EFBFBD>
|
// Ȩ<><C8A8> & <20><><EFBFBD>ػ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͻ<EFBFBD><EFBFBD><EFBFBD>ʵ<EFBFBD>֣<EFBFBD>
|
||||||
builder.Services.AddScoped<IPermissionService, ClientPermissionService>();
|
builder.Services.AddScoped<IPermissionService, ClientPermissionService>();
|
||||||
builder.Services.AddScoped<IconsExtension>();
|
builder.Services.AddScoped<IconsExtension>();
|
||||||
builder.Services.AddScoped<ILocalizationService, LocalizationClientService>();
|
builder.Services.AddScoped<ILocalizationService, LocalizationClientService>();
|
||||||
|
|
||||||
// Token provider<65><72>WASM<53><4D>
|
// Token provider<65><72>WASM<53><4D>: <20><> localStorage <20><>ȡ access token
|
||||||
builder.Services.AddScoped<ITokenProvider, ClientTokenProvider>();
|
builder.Services.AddScoped<ITokenProvider, ClientTokenProvider>();
|
||||||
|
|
||||||
// ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD> token & ˢ<>µ<EFBFBD> DelegatingHandler
|
// ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD> token & ˢ<>µ<EFBFBD> DelegatingHandler
|
||||||
builder.Services.AddScoped<AuthHeaderHandler>();
|
builder.Services.AddScoped<AuthHeaderHandler>();
|
||||||
|
|
||||||
// ע<EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HttpClient<6E><74><EFBFBD><EFBFBD>Ӧ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> API <20><><EFBFBD><EFBFBD>ʹ<EFBFBD>ã<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthHeaderHandler <20><><EFBFBD><EFBFBD><EFBFBD>ܵ<EFBFBD>
|
// <20><><EFBFBD><EFBFBD> HttpClient<6E><74>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>ͳһʹ<EFBFBD><EFBFBD> ApiClient<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthHeaderHandler <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܵ<EFBFBD>
|
||||||
var apiBase = builder.Configuration["WebApi:ServerUrl"] ?? builder.HostEnvironment.BaseAddress;
|
var apiBase = builder.Configuration["WebApi:ServerUrl"] ?? builder.HostEnvironment.BaseAddress;
|
||||||
builder.Services.AddHttpClient("ApiClient", client =>
|
builder.Services.AddHttpClient("ApiClient", client =>
|
||||||
{
|
{
|
||||||
@@ -33,8 +33,14 @@ builder.Services.AddHttpClient("ApiClient", client =>
|
|||||||
})
|
})
|
||||||
.AddHttpMessageHandler<AuthHeaderHandler>();
|
.AddHttpMessageHandler<AuthHeaderHandler>();
|
||||||
|
|
||||||
// Ϊ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֵ<EFBFBD> HttpClient<EFBFBD><EFBFBD>AuthHeaderHandler <20>ڲ<EFBFBD> CreateClient() ʹ<><CAB9>Ĭ<EFBFBD>Ϲ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
// ע<><D7A2>һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthHeaderHandler <20><> HttpClient<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ˢ<EFBFBD><EFBFBD> token<65><6E><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ã<EFBFBD>
|
||||||
// Ҳע<EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD> HttpClient <20><> BaseAddress
|
// <EFBFBD><EFBFBD> client <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AuthHeaderHandler <20><>ʹ<EFBFBD><CAB9> "RefreshClient"
|
||||||
|
builder.Services.AddHttpClient("RefreshClient", client =>
|
||||||
|
{
|
||||||
|
client.BaseAddress = new Uri(apiBase);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ĭ<><C4AC> HttpClient<6E><74><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2> HttpClient<6E><74>
|
||||||
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ApiClient"));
|
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ApiClient"));
|
||||||
|
|
||||||
// <20><> WASM DI <20><>ע<EFBFBD><D7A2> HttpService<63><65>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2><EFBFBD><EFBFBD> HttpClient ʵ<><CAB5>
|
// <20><> WASM DI <20><>ע<EFBFBD><D7A2> HttpService<63><65>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2><EFBFBD><EFBFBD> HttpClient ʵ<><CAB5>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace Atomx.Admin.Client.Utils
|
|||||||
/// - 在每次请求时将 access token 附带 Authorization header
|
/// - 在每次请求时将 access token 附带 Authorization header
|
||||||
/// - 在 401 或响应头包含 "Token-Expired" 时尝试刷新(调用 /api/sign/refresh)
|
/// - 在 401 或响应头包含 "Token-Expired" 时尝试刷新(调用 /api/sign/refresh)
|
||||||
/// - 防止并发刷新(SemaphoreSlim)
|
/// - 防止并发刷新(SemaphoreSlim)
|
||||||
|
/// - 注意:刷新请求必须使用一个不包含本 handler 的 HttpClient(避免循环)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AuthHeaderHandler : DelegatingHandler
|
public class AuthHeaderHandler : DelegatingHandler
|
||||||
{
|
{
|
||||||
@@ -45,6 +46,7 @@ namespace Atomx.Admin.Client.Utils
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// 从 ITokenProvider 获取当前 access token(WASM: ClientTokenProvider 从 localStorage 读取)
|
||||||
var token = await _tokenProvider.GetTokenAsync();
|
var token = await _tokenProvider.GetTokenAsync();
|
||||||
if (!string.IsNullOrEmpty(token))
|
if (!string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
@@ -57,6 +59,7 @@ namespace Atomx.Admin.Client.Utils
|
|||||||
|
|
||||||
var response = await base.SendAsync(request, cancellationToken);
|
var response = await base.SendAsync(request, cancellationToken);
|
||||||
|
|
||||||
|
// 当发现未授权或后端标记 token 过期时,尝试刷新
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
|
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
|
||||||
response.Headers.Contains("Token-Expired"))
|
response.Headers.Contains("Token-Expired"))
|
||||||
{
|
{
|
||||||
@@ -76,6 +79,7 @@ namespace Atomx.Admin.Client.Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新失败或无 token,跳转到登录页(强制刷新页面,清除 SPA 状态)
|
||||||
_navigationManager.NavigateTo("/account/login", true);
|
_navigationManager.NavigateTo("/account/login", true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -93,6 +97,13 @@ namespace Atomx.Admin.Client.Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试刷新 token
|
||||||
|
/// 关键点:
|
||||||
|
/// - 使用命名 HttpClient "RefreshClient"(在 WASM Program 中注册,不包含本 handler),避免递归
|
||||||
|
/// - 并发刷新时只有一个请求会实际触发刷新,其余等待结果
|
||||||
|
/// - 刷新成功后写入 localStorage(accessToken + refreshToken)
|
||||||
|
/// </summary>
|
||||||
private async Task<bool> TryRefreshTokenAsync(CancellationToken cancellationToken)
|
private async Task<bool> TryRefreshTokenAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _refreshLock.WaitAsync(cancellationToken);
|
await _refreshLock.WaitAsync(cancellationToken);
|
||||||
@@ -107,11 +118,13 @@ namespace Atomx.Admin.Client.Utils
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = _httpClientFactory.CreateClient();
|
// 使用不带本 handler 的 HttpClient 发送刷新请求,避免循环
|
||||||
var reqModel = new
|
var client = _httpClientFactory.CreateClient("RefreshClient");
|
||||||
|
|
||||||
|
var reqModel = new RefreshRequest
|
||||||
{
|
{
|
||||||
token = currentAccess,
|
Token = currentAccess,
|
||||||
refreshToken = currentRefresh
|
RefreshToken = currentRefresh
|
||||||
};
|
};
|
||||||
var reqJson = JsonSerializer.Serialize(reqModel);
|
var reqJson = JsonSerializer.Serialize(reqModel);
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Post, "api/sign/refresh")
|
using var req = new HttpRequestMessage(HttpMethod.Post, "api/sign/refresh")
|
||||||
@@ -125,6 +138,9 @@ namespace Atomx.Admin.Client.Utils
|
|||||||
if (!resp.IsSuccessStatusCode)
|
if (!resp.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Refresh request failed with status {Status}", resp.StatusCode);
|
_logger.LogWarning("Refresh request failed with status {Status}", resp.StatusCode);
|
||||||
|
// 如果刷新失败,移除本地 token(防止无限重试)
|
||||||
|
await _localStorage.RemoveItemAsync(AccessTokenKey);
|
||||||
|
await _localStorage.RemoveItemAsync(RefreshTokenKey);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,9 +153,12 @@ namespace Atomx.Admin.Client.Utils
|
|||||||
if (authResp == null || string.IsNullOrEmpty(authResp.Token) || string.IsNullOrEmpty(authResp.RefreshToken))
|
if (authResp == null || string.IsNullOrEmpty(authResp.Token) || string.IsNullOrEmpty(authResp.RefreshToken))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Invalid response from refresh endpoint");
|
_logger.LogWarning("Invalid response from refresh endpoint");
|
||||||
|
await _localStorage.RemoveItemAsync(AccessTokenKey);
|
||||||
|
await _localStorage.RemoveItemAsync(RefreshTokenKey);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存新的 tokens 到 localStorage
|
||||||
await _localStorage.SetItemAsync(AccessTokenKey, authResp.Token, cancellationToken);
|
await _localStorage.SetItemAsync(AccessTokenKey, authResp.Token, cancellationToken);
|
||||||
await _localStorage.SetItemAsync(RefreshTokenKey, authResp.RefreshToken, cancellationToken);
|
await _localStorage.SetItemAsync(RefreshTokenKey, authResp.RefreshToken, cancellationToken);
|
||||||
|
|
||||||
@@ -158,10 +177,14 @@ namespace Atomx.Admin.Client.Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 复制原始请求并用新的 token 替换 Authorization header
|
||||||
|
/// </summary>
|
||||||
private static async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage original, string newToken)
|
private static async Task<HttpRequestMessage> CloneHttpRequestMessageAsync(HttpRequestMessage original, string newToken)
|
||||||
{
|
{
|
||||||
var clone = new HttpRequestMessage(original.Method, original.RequestUri);
|
var clone = new HttpRequestMessage(original.Method, original.RequestUri);
|
||||||
|
|
||||||
|
// 复制内容(如果存在)
|
||||||
if (original.Content != null)
|
if (original.Content != null)
|
||||||
{
|
{
|
||||||
var ms = new MemoryStream();
|
var ms = new MemoryStream();
|
||||||
@@ -176,9 +199,11 @@ namespace Atomx.Admin.Client.Utils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 复制 headers
|
||||||
foreach (var header in original.Headers)
|
foreach (var header in original.Headers)
|
||||||
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||||
|
|
||||||
|
// 覆盖 Authorization
|
||||||
clone.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
|
clone.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
|
||||||
|
|
||||||
return clone;
|
return clone;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
|
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.Admin.Utils;
|
using Atomx.Admin.Utils;
|
||||||
@@ -16,27 +15,42 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Atomx.Admin.Controllers
|
namespace Atomx.Admin.Controllers
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 登录/刷新/登出控制器(重构)
|
||||||
|
/// 说明:
|
||||||
|
/// - 返回标准的 AuthResponse(access + refresh + expiry),便于 WASM 客户端保存到 localStorage
|
||||||
|
/// - 仍保留 Cookie 登录(SignInAsync)以兼容 Blazor Server 场景
|
||||||
|
/// - 使用注入的 ITokenService 负责 token 的生成、刷新与撤销(数据库保存 refresh token 哈希)
|
||||||
|
/// - 提供 /api/sign/in (POST), /api/sign/refresh (POST), /api/sign/out (POST)
|
||||||
|
/// </summary>
|
||||||
[Route("api/[controller]")]
|
[Route("api/[controller]")]
|
||||||
[ApiController]
|
[ApiController]
|
||||||
public class SignController : ControllerBase
|
public class SignController : ControllerBase
|
||||||
{
|
{
|
||||||
readonly ILogger<SignController> _logger;
|
private readonly ILogger<SignController> _logger;
|
||||||
readonly IIdentityService _identityService;
|
private readonly IIdentityService _identityService;
|
||||||
readonly IIdCreatorService _idCreator;
|
private readonly IIdCreatorService _idCreator;
|
||||||
readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
readonly DataContext _dbContext;
|
private readonly DataContext _dbContext;
|
||||||
readonly JwtSetting _jwtSetting;
|
private readonly JwtSetting _jwtSetting;
|
||||||
readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
readonly AuthenticationStateProvider _authenticationStateProvider;
|
private readonly AuthenticationStateProvider _authenticationStateProvider;
|
||||||
|
private readonly ITokenService _tokenService;
|
||||||
|
|
||||||
public SignController(ILogger<SignController> logger, IIdentityService identityService, IIdCreatorService idCreator, IMapper mapper, DataContext dbContext, JwtSetting jwtSetting, ICacheService cacheService, AuthenticationStateProvider authenticationStateProvider)
|
public SignController(
|
||||||
|
ILogger<SignController> logger,
|
||||||
|
IIdentityService identityService,
|
||||||
|
IIdCreatorService idCreator,
|
||||||
|
IMapper mapper,
|
||||||
|
DataContext dbContext,
|
||||||
|
JwtSetting jwtSetting,
|
||||||
|
ICacheService cacheService,
|
||||||
|
AuthenticationStateProvider authenticationStateProvider,
|
||||||
|
ITokenService tokenService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_identityService = identityService;
|
_identityService = identityService;
|
||||||
@@ -46,106 +60,201 @@ namespace Atomx.Admin.Controllers
|
|||||||
_jwtSetting = jwtSetting;
|
_jwtSetting = jwtSetting;
|
||||||
_cacheService = cacheService;
|
_cacheService = cacheService;
|
||||||
_authenticationStateProvider = authenticationStateProvider;
|
_authenticationStateProvider = authenticationStateProvider;
|
||||||
|
_tokenService = tokenService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户登录系统
|
/// 登录:支持邮箱或用户名登录
|
||||||
|
/// - 返回 AuthResponse 给 WASM(access + refresh)
|
||||||
|
/// - 在 Server 场景同时创建 Cookie(兼容 Blazor Server)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
|
||||||
[HttpPost("in")]
|
[HttpPost("in")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> Login(LoginModel model)
|
public async Task<IActionResult> Login([FromBody] LoginModel model)
|
||||||
{
|
{
|
||||||
var validator = new LoginModelValidator();
|
var validator = new LoginModelValidator();
|
||||||
var validation = validator.Validate(model);
|
var validation = validator.Validate(model);
|
||||||
|
|
||||||
if (!validation.IsValid)
|
if (!validation.IsValid)
|
||||||
{
|
{
|
||||||
var message = validation.Errors.FirstOrDefault()?.ErrorMessage;
|
var message = validation.Errors.FirstOrDefault()?.ErrorMessage ?? string.Empty;
|
||||||
var result = new ApiResult<string>().IsFail(message ?? string.Empty, null);
|
return new JsonResult(new ApiResult<AuthResponse>().IsFail(message, null));
|
||||||
return new JsonResult(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var tokenHandler = new JwtSecurityTokenHandler();
|
// 查询用户(支持 email / username)
|
||||||
var issuer = _jwtSetting.Issuer;
|
Atomx.Common.Entities.Admin? user = null;
|
||||||
var audience = _jwtSetting.Audience;
|
|
||||||
var securityKey = _jwtSetting.SecurityKey;
|
|
||||||
|
|
||||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
|
|
||||||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
|
||||||
|
|
||||||
Common.Entities.Admin? user = null;
|
|
||||||
if (model.Account.Contains("@"))
|
if (model.Account.Contains("@"))
|
||||||
{
|
{
|
||||||
user = _dbContext.Admins.Where(p => p.Email == model.Account).SingleOrDefault();
|
user = _dbContext.Admins.SingleOrDefault(p => p.Email == model.Account);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
user = _dbContext.Admins.Where(p => p.Username == model.Account).SingleOrDefault();
|
user = _dbContext.Admins.SingleOrDefault(p => p.Username == model.Account);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
var result = new ApiResult<string>().IsFail("用户不存在", null);
|
return new JsonResult(new ApiResult<AuthResponse>().IsFail("用户不存在", null));
|
||||||
return new JsonResult(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 简单密码校验(项目 uses MD5 存储示例)
|
||||||
if (user.Password != model.Password.ToMd5Password())
|
if (user.Password != model.Password.ToMd5Password())
|
||||||
{
|
{
|
||||||
var result = new ApiResult<string>().IsFail("账号密码不正确", null);
|
return new JsonResult(new ApiResult<AuthResponse>().IsFail("账号密码不正确", null));
|
||||||
return new JsonResult(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var role = _dbContext.Roles.Where(p => p.Id == user.RoleId).SingleOrDefault();
|
// 生成 access + refresh(TokenService 会把 refresh 的哈希保存到数据库)
|
||||||
|
var ip = _identityService.GetClientIp();
|
||||||
|
var userAgent = Request.Headers["User-Agent"].FirstOrDefault();
|
||||||
|
var authResponse = await _tokenService.GenerateTokenAsync(user, ip, userAgent);
|
||||||
|
|
||||||
var claims = new List<Claim>()
|
// 更新用户登录统计信息
|
||||||
{
|
user.LastLogin = DateTime.UtcNow;
|
||||||
new Claim(ClaimKeys.Id, user.Id.ToString()),
|
user.LastIp = ip;
|
||||||
new Claim(ClaimKeys.Email, user.Email),
|
user.LoginCount++;
|
||||||
new Claim(ClaimKeys.Name, user.Username),
|
_dbContext.Admins.Update(user);
|
||||||
new Claim(ClaimKeys.Role, user.RoleId.ToString()),
|
await _dbContext.SaveChangesAsync();
|
||||||
new Claim(ClaimKeys.Permission, role?.Permission??string.Empty)
|
|
||||||
};
|
// 为 Blazor Server 场景创建 Cookie(Claims 中包含必要角色/权限)
|
||||||
|
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.Email, user.Email ?? string.Empty),
|
||||||
|
new Claim(ClaimKeys.Name, user.Username ?? string.Empty),
|
||||||
|
new Claim(ClaimKeys.Role, user.RoleId.ToString()),
|
||||||
|
new Claim(ClaimKeys.Permission, role?.Permission ?? string.Empty)
|
||||||
|
};
|
||||||
|
|
||||||
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
|
// SignInAsync 创建 HttpOnly Cookie,便于 Server-side 认证
|
||||||
|
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
|
||||||
|
new ClaimsPrincipal(claimsIdentity),
|
||||||
|
new AuthenticationProperties
|
||||||
|
{
|
||||||
|
IsPersistent = model.RememberMe,
|
||||||
|
AllowRefresh = true,
|
||||||
|
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes)
|
||||||
|
});
|
||||||
|
|
||||||
var tokenDescriptor = new SecurityTokenDescriptor
|
// 另外将 tokens 写入 HttpOnly Cookie(增强与传统中间件的兼容性)
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Subject = claimsIdentity,
|
var cookieOptions = new CookieOptions
|
||||||
|
{
|
||||||
|
HttpOnly = true,
|
||||||
|
//Secure = !Request.IsLocal(), // 本地调试时允许 http
|
||||||
|
SameSite = SameSiteMode.Lax,
|
||||||
|
Expires = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes),
|
||||||
|
Path = "/"
|
||||||
|
};
|
||||||
|
Response.Cookies.Append("access_token", authResponse.Token, cookieOptions);
|
||||||
|
|
||||||
Expires = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes),
|
var refreshCookieOptions = new CookieOptions
|
||||||
SigningCredentials = credentials,
|
{
|
||||||
Issuer = issuer,
|
HttpOnly = true,
|
||||||
Audience = audience
|
//Secure = !Request.IsLocal(),
|
||||||
};
|
SameSite = SameSiteMode.Lax,
|
||||||
|
Expires = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.RefreshTokenExpirationMinutes),
|
||||||
var tokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
|
Path = "/"
|
||||||
|
};
|
||||||
var loginResult = new ApiResult<string>().IsSuccess(tokenString);
|
Response.Cookies.Append("refresh_token", authResponse.RefreshToken, refreshCookieOptions);
|
||||||
|
}
|
||||||
user.LastLogin = DateTime.UtcNow;
|
catch (Exception ex)
|
||||||
user.LastIp = _identityService.GetClientIp();
|
{
|
||||||
user.LoginCount++;
|
_logger.LogWarning(ex, "设置 token cookie 失败(非致命)");
|
||||||
_dbContext.Admins.Update(user);
|
}
|
||||||
_dbContext.SaveChanges();
|
|
||||||
|
|
||||||
|
|
||||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
|
|
||||||
|
|
||||||
return new JsonResult(loginResult);
|
|
||||||
|
|
||||||
|
return new JsonResult(new ApiResult<AuthResponse>().IsSuccess(authResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户退出系统
|
/// 刷新:客户端传入(可能已过期的)access token 与 refresh token(明文)
|
||||||
|
/// - TokenService.RefreshTokenAsync 会验证并一次性撤销旧的 refresh token,生成新的对
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
[HttpPost("refresh")]
|
||||||
[HttpGet("out")]
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public async Task<IActionResult> LogoutAsync()
|
public async Task<IActionResult> Refresh([FromBody] RefreshRequest request)
|
||||||
{
|
{
|
||||||
await HttpContext.SignOutAsync();
|
if (request == null || string.IsNullOrWhiteSpace(request.Token) || string.IsNullOrWhiteSpace(request.RefreshToken))
|
||||||
return new JsonResult(new ApiResult<string>());
|
{
|
||||||
|
return BadRequest(new ApiResult<AuthResponse>().IsFail("无效的刷新请求", null));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ip = _identityService.GetClientIp();
|
||||||
|
var newTokens = await _tokenService.RefreshTokenAsync(request.Token, request.RefreshToken, ip);
|
||||||
|
|
||||||
|
// 更新 cookie(如存在)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cookieOptions = new CookieOptions
|
||||||
|
{
|
||||||
|
HttpOnly = true,
|
||||||
|
//Secure = !Request.IsLocal(),
|
||||||
|
SameSite = SameSiteMode.Lax,
|
||||||
|
Expires = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes),
|
||||||
|
Path = "/"
|
||||||
|
};
|
||||||
|
Response.Cookies.Append("access_token", newTokens.Token, cookieOptions);
|
||||||
|
|
||||||
|
var refreshCookieOptions = new CookieOptions
|
||||||
|
{
|
||||||
|
HttpOnly = true,
|
||||||
|
//Secure = !Request.IsLocal(),
|
||||||
|
SameSite = SameSiteMode.Lax,
|
||||||
|
Expires = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.RefreshTokenExpirationMinutes),
|
||||||
|
Path = "/"
|
||||||
|
};
|
||||||
|
Response.Cookies.Append("refresh_token", newTokens.RefreshToken, refreshCookieOptions);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "刷新 token 时写 cookie 失败(允许)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JsonResult(new ApiResult<AuthResponse>().IsSuccess(newTokens));
|
||||||
|
}
|
||||||
|
catch (SecurityTokenException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "刷新失败:刷新令牌无效或 access token 无效");
|
||||||
|
return Unauthorized(new ApiResult<AuthResponse>().IsFail("刷新令牌无效或已过期", null));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "刷新令牌时发生内部错误");
|
||||||
|
return StatusCode(500, new ApiResult<AuthResponse>().IsFail("服务器内部错误", null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 登出:可传入 refresh token 以撤销(WASM 前端应同时清除 localStorage)
|
||||||
|
/// - 本接口会 SignOut Cookie(Server 场景)并尝试撤销 refresh token
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("out")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
public async Task<IActionResult> LogoutAsync([FromBody] RevokeRequest? revokeRequest = null)
|
||||||
|
{
|
||||||
|
if (revokeRequest != null && !string.IsNullOrWhiteSpace(revokeRequest.RefreshToken))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ip = _identityService.GetClientIp();
|
||||||
|
await _tokenService.RevokeTokenAsync(revokeRequest.RefreshToken, ip);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "撤销 refresh token 失败(允许)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 Cookie
|
||||||
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
Response.Cookies.Delete("access_token");
|
||||||
|
Response.Cookies.Delete("refresh_token");
|
||||||
|
|
||||||
|
return new JsonResult(new ApiResult<string>().IsSuccess("已退出"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,25 +9,24 @@ using System.Text.Json;
|
|||||||
|
|
||||||
namespace Atomx.Admin.Extensions
|
namespace Atomx.Admin.Extensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// AuthorizationExtension 重构说明:
|
||||||
|
/// - 配置 Cookie + JwtBearer 双方案,JwtBearer 的 Events 中添加对 SignalR / WebSocket 的 query string access_token 读取(OnMessageReceived)
|
||||||
|
/// - 保持 OnChallenge 的重定向行为(对于浏览器访问 API 时友好)
|
||||||
|
/// - 将 JwtSetting 注入为 Singleton 供 TokenService 使用
|
||||||
|
/// </summary>
|
||||||
public static class AuthorizationExtension
|
public static class AuthorizationExtension
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 添加身份验证服务
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="services"></param>
|
|
||||||
/// <param name="Configuration"></param>
|
|
||||||
/// <param name="environment"></param>
|
|
||||||
/// <exception cref="Exception"></exception>
|
|
||||||
public static void AddAuthorize(this IServiceCollection services, IConfiguration Configuration, IWebHostEnvironment environment)
|
public static void AddAuthorize(this IServiceCollection services, IConfiguration Configuration, IWebHostEnvironment environment)
|
||||||
{
|
{
|
||||||
var jwtSetting = Configuration.GetSection("Authentication:JwtBearer").Get<JwtSetting>();
|
var jwtSetting = Configuration.GetSection("Authentication:JwtBearer").Get<JwtSetting>();
|
||||||
if (jwtSetting == null)
|
if (jwtSetting == null)
|
||||||
{
|
{
|
||||||
throw new Exception("缺少配置信息");
|
throw new Exception("缺少配置信息 Authentication:JwtBearer");
|
||||||
}
|
}
|
||||||
services.AddSingleton(jwtSetting);
|
services.AddSingleton(jwtSetting);
|
||||||
|
|
||||||
// 从配置读取 Cookie 设置(可在 appsettings.json 的 Authentication:Cookie 节点配置)
|
// Cookie 配置读取
|
||||||
var cookieConf = Configuration.GetSection("Authentication:Cookie");
|
var cookieConf = Configuration.GetSection("Authentication:Cookie");
|
||||||
var cookieName = cookieConf.GetValue<string>("Name") ?? ".Atomx.Auth";
|
var cookieName = cookieConf.GetValue<string>("Name") ?? ".Atomx.Auth";
|
||||||
var cookiePath = cookieConf.GetValue<string>("Path") ?? "/";
|
var cookiePath = cookieConf.GetValue<string>("Path") ?? "/";
|
||||||
@@ -36,30 +35,26 @@ namespace Atomx.Admin.Extensions
|
|||||||
var securePolicyStr = cookieConf.GetValue<string>("SecurePolicy");
|
var securePolicyStr = cookieConf.GetValue<string>("SecurePolicy");
|
||||||
var expireMinutes = cookieConf.GetValue<int?>("ExpireMinutes") ?? 60;
|
var expireMinutes = cookieConf.GetValue<int?>("ExpireMinutes") ?? 60;
|
||||||
|
|
||||||
// 解析 SameSite(默认:开发环境 Strict,生产环境 None 用于跨站点场景比如前后端分离)
|
|
||||||
SameSiteMode sameSiteMode;
|
SameSiteMode sameSiteMode;
|
||||||
if (string.IsNullOrWhiteSpace(sameSiteStr) || !Enum.TryParse<SameSiteMode>(sameSiteStr, true, out sameSiteMode))
|
if (string.IsNullOrWhiteSpace(sameSiteStr) || !Enum.TryParse<SameSiteMode>(sameSiteStr, true, out sameSiteMode))
|
||||||
{
|
{
|
||||||
sameSiteMode = environment.IsDevelopment() ? SameSiteMode.Strict : SameSiteMode.None;
|
sameSiteMode = environment.IsDevelopment() ? SameSiteMode.Strict : SameSiteMode.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析 SecurePolicy(默认:开发 SameAsRequest,生产 Always)
|
|
||||||
CookieSecurePolicy securePolicy;
|
CookieSecurePolicy securePolicy;
|
||||||
if (string.IsNullOrWhiteSpace(securePolicyStr) || !Enum.TryParse<CookieSecurePolicy>(securePolicyStr, true, out securePolicy))
|
if (string.IsNullOrWhiteSpace(securePolicyStr) || !Enum.TryParse<CookieSecurePolicy>(securePolicyStr, true, out securePolicy))
|
||||||
{
|
{
|
||||||
securePolicy = environment.IsDevelopment() ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.Always;
|
securePolicy = environment.IsDevelopment() ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.Always;
|
||||||
}
|
}
|
||||||
|
|
||||||
//认证配置:注册 Cookie(用于 SignIn/SignOut)和 JwtBearer(用于 API 授权)
|
|
||||||
services.AddAuthentication(options =>
|
services.AddAuthentication(options =>
|
||||||
{
|
{
|
||||||
// 默认用于 API 的认证/挑战方案使用 JwtBearer
|
// 默认用于 API 的认证方案为 JwtBearer
|
||||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||||
})
|
})
|
||||||
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
|
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
|
||||||
{
|
{
|
||||||
// Cookie 配置,确保 SignInAsync 能找到处理器
|
|
||||||
options.Cookie.Name = cookieName;
|
options.Cookie.Name = cookieName;
|
||||||
options.Cookie.Path = cookiePath;
|
options.Cookie.Path = cookiePath;
|
||||||
if (!string.IsNullOrWhiteSpace(cookieDomain))
|
if (!string.IsNullOrWhiteSpace(cookieDomain))
|
||||||
@@ -88,25 +83,37 @@ namespace Atomx.Admin.Extensions
|
|||||||
ValidateAudience = true,
|
ValidateAudience = true,
|
||||||
ValidateLifetime = true,
|
ValidateLifetime = true,
|
||||||
ValidateIssuerSigningKey = true,
|
ValidateIssuerSigningKey = true,
|
||||||
ValidAudience = jwtSetting.Audience,//Audience
|
ValidAudience = jwtSetting.Audience,
|
||||||
ValidIssuer = jwtSetting.Issuer,
|
ValidIssuer = jwtSetting.Issuer,
|
||||||
ClockSkew = TimeSpan.FromMinutes(Convert.ToDouble(jwtSetting.ClockSkew)), //过期时钟偏差
|
ClockSkew = TimeSpan.FromMinutes(Convert.ToDouble(jwtSetting.ClockSkew)),
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.SecurityKey))
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.SecurityKey))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 允许从 query string 中获取 access_token(用于 SignalR / WebSocket)
|
||||||
options.Events = new JwtBearerEvents
|
options.Events = new JwtBearerEvents
|
||||||
{
|
{
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
// SignalR 客户端常把 token 放在 query string 参数 access_token
|
||||||
|
var accessToken = context.Request.Query["access_token"].FirstOrDefault();
|
||||||
|
var path = context.HttpContext.Request.Path;
|
||||||
|
if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/hubs") || path.StartsWithSegments("/hub")))
|
||||||
|
{
|
||||||
|
context.Token = accessToken;
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
OnChallenge = context =>
|
OnChallenge = context =>
|
||||||
{
|
{
|
||||||
|
// 浏览器端访问 API 并无 token 时重定向到登录页
|
||||||
var absoluteUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
|
var absoluteUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
|
||||||
context.HandleResponse();
|
context.HandleResponse();
|
||||||
context.Response.Redirect($"/account/login?returnUrl={Uri.EscapeDataString(absoluteUri)}");
|
context.Response.Redirect($"/account/login?returnUrl={Uri.EscapeDataString(absoluteUri)}");
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
},
|
},
|
||||||
|
|
||||||
OnAuthenticationFailed = context =>
|
OnAuthenticationFailed = context =>
|
||||||
{
|
{
|
||||||
//Token expired
|
if (context.Exception?.GetType() == typeof(SecurityTokenExpiredException))
|
||||||
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
|
|
||||||
{
|
{
|
||||||
context.Response.Headers.Append("Token-Expired", "true");
|
context.Response.Headers.Append("Token-Expired", "true");
|
||||||
}
|
}
|
||||||
@@ -115,13 +122,13 @@ namespace Atomx.Admin.Extensions
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 注册基于权限的策略
|
||||||
services.AddAuthorization(options =>
|
services.AddAuthorization(options =>
|
||||||
{
|
{
|
||||||
// 基于权限的策略
|
|
||||||
var allPermissions = Permissions.GetAllPermissions();
|
var allPermissions = Permissions.GetAllPermissions();
|
||||||
foreach (var permission in allPermissions)
|
foreach (var permission in allPermissions)
|
||||||
{
|
{
|
||||||
options.AddPolicy(permission, policy => { policy.Requirements.Add(new PermissionRequirement(permission)); });
|
options.AddPolicy(permission, policy => policy.Requirements.Add(new PermissionRequirement(permission)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,30 @@ using System.Text.RegularExpressions;
|
|||||||
|
|
||||||
namespace Atomx.Admin.Middlewares
|
namespace Atomx.Admin.Middlewares
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 请求监控中间件
|
||||||
|
/// 变更点:
|
||||||
|
/// - 不再在构造函数注入作用域(scoped)服务 IIdentityService,避免在应用启动时从根 provider 解析 scoped 服务导致异常。
|
||||||
|
/// - 在每次请求处理时通过 HttpContext.RequestServices 获取 IIdentityService(请求作用域内解析)。
|
||||||
|
/// </summary>
|
||||||
public class MonitoringMiddleware
|
public class MonitoringMiddleware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly ILogger<MonitoringMiddleware> _logger;
|
private readonly ILogger<MonitoringMiddleware> _logger;
|
||||||
private readonly MonitoringOptions _options;
|
private readonly MonitoringOptions _options;
|
||||||
private readonly IIdentityService _identityService;
|
|
||||||
|
|
||||||
public MonitoringMiddleware(RequestDelegate next, ILogger<MonitoringMiddleware> logger, IOptions<MonitoringOptions> options, IIdentityService identityService)
|
public MonitoringMiddleware(RequestDelegate next, ILogger<MonitoringMiddleware> logger, IOptions<MonitoringOptions> options)
|
||||||
{
|
{
|
||||||
_next = next;
|
_next = next;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
_identityService = identityService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task InvokeAsync(HttpContext context)
|
public async Task InvokeAsync(HttpContext context)
|
||||||
{
|
{
|
||||||
|
// 在请求作用域内解析 IIdentityService,避免在中间件构造时从根 provider 解析 scoped 服务
|
||||||
|
var identityService = context.RequestServices.GetService<IIdentityService>();
|
||||||
|
|
||||||
// 检查是否应该跳过监控
|
// 检查是否应该跳过监控
|
||||||
if (ShouldSkipMonitoring(context))
|
if (ShouldSkipMonitoring(context))
|
||||||
{
|
{
|
||||||
@@ -32,7 +39,7 @@ namespace Atomx.Admin.Middlewares
|
|||||||
|
|
||||||
var logInfo = new
|
var logInfo = new
|
||||||
{
|
{
|
||||||
UserId = _identityService.GetUserId(),
|
UserId = identityService?.GetUserId(),
|
||||||
Path = context.Request.Path,
|
Path = context.Request.Path,
|
||||||
Method = context.Request.Method,
|
Method = context.Request.Method,
|
||||||
StartTime = DateTime.UtcNow,
|
StartTime = DateTime.UtcNow,
|
||||||
@@ -50,7 +57,6 @@ namespace Atomx.Admin.Middlewares
|
|||||||
{
|
{
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
|
|
||||||
|
|
||||||
var logData = new
|
var logData = new
|
||||||
{
|
{
|
||||||
Path = context.Request.Path,
|
Path = context.Request.Path,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ using System.Text.Unicode;
|
|||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Configure Serilog
|
// Serilog <20><><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD>ԭ<EFBFBD><D4AD><EFBFBD><EFBFBD>
|
||||||
Log.Logger = new LoggerConfiguration()
|
Log.Logger = new LoggerConfiguration()
|
||||||
.ReadFrom.Configuration(builder.Configuration)
|
.ReadFrom.Configuration(builder.Configuration)
|
||||||
.Enrich.FromLogContext()
|
.Enrich.FromLogContext()
|
||||||
@@ -37,7 +37,7 @@ Log.Logger = new LoggerConfiguration()
|
|||||||
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||||
.CreateLogger();
|
.CreateLogger();
|
||||||
|
|
||||||
// Add services to the container.
|
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD>
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents()
|
.AddInteractiveServerComponents()
|
||||||
.AddInteractiveWebAssemblyComponents();
|
.AddInteractiveWebAssemblyComponents();
|
||||||
@@ -56,101 +56,93 @@ builder.Services.AddBlazoredLocalStorage();
|
|||||||
builder.Services.AddCascadingAuthenticationState();
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
|
|
||||||
// Ȩ<><EFBFBD><DEB7><EFBFBD>
|
// Ȩ<><EFBFBD><DEB7><EFBFBD> & <20><>Ȩ<EFBFBD><C8A8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
builder.Services.AddScoped<IPermissionService, PermissionService>();
|
builder.Services.AddScoped<IPermissionService, PermissionService>();
|
||||||
builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
builder.Services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
|
||||||
|
|
||||||
// AuthenticationStateProvider<65><72>Server <EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD>ÿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><EFBFBD>ʵ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ΪĬ<EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD>
|
// AuthenticationStateProvider<65><72>Server ʹ<>ÿ<EFBFBD><C3BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><D6A4>ʵ<EFBFBD><CAB5>
|
||||||
builder.Services.AddScoped<AuthenticationStateProvider, PersistingRevalidatingAuthenticationStateProvider>();
|
builder.Services.AddScoped<AuthenticationStateProvider, PersistingRevalidatingAuthenticationStateProvider>();
|
||||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ<EFBFBD><D2AA> Server <20><>ʹ<EFBFBD><CAB9> PersistentAuthenticationStateProvider <20>ľ<EFBFBD><C4BE>幦<EFBFBD>ܣ<EFBFBD><DCA3><EFBFBD><EFBFBD><EFBFBD><D4B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><D7A2>
|
|
||||||
builder.Services.AddScoped<PersistentAuthenticationStateProvider>();
|
builder.Services.AddScoped<PersistentAuthenticationStateProvider>();
|
||||||
|
|
||||||
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߷<EFBFBD><DFB7><EFBFBD>
|
||||||
builder.Services.AddScoped<IIdCreatorService, IdCreatorService>();
|
builder.Services.AddScoped<IIdCreatorService, IdCreatorService>();
|
||||||
builder.Services.AddScoped<IIdentityService, IdentityService>();
|
builder.Services.AddScoped<IIdentityService, IdentityService>();
|
||||||
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
|
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
|
||||||
builder.Services.AddScoped<LocalizationFile, LocalizationFile>();
|
builder.Services.AddScoped<LocalizationFile, LocalizationFile>();
|
||||||
|
|
||||||
// Server <20><>ͳһ ITokenProvider <20><>ʵ<EFBFBD>֣<EFBFBD>WASM <20><><EFBFBD><EFBFBD> Program.cs <20><>ע<EFBFBD><D7A2> ClientTokenProvider
|
// Token <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Provider<65><72>Server ʹ<><CAB9> ServerTokenProvider<EFBFBD><EFBFBD>
|
||||||
builder.Services.AddScoped<ITokenProvider, ServerTokenProvider>();
|
builder.Services.AddScoped<ITokenProvider, ServerTokenProvider>();
|
||||||
|
builder.Services.AddScoped<ITokenService, TokenService>(); // ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD> TokenService
|
||||||
builder.Services.AddScoped<AuthHeaderHandler>();
|
builder.Services.AddScoped<AuthHeaderHandler>();
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD> SignalR<EFBFBD><EFBFBD>֧<EFBFBD><EFBFBD>ͨ<EFBFBD><EFBFBD> query string <20><><EFBFBD><EFBFBD> access_token<65><6E><EFBFBD><EFBFBD><EFBFBD><EFBFBD> websocket/auth<EFBFBD><EFBFBD>
|
// ע<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>/ˢ<><CBA2>/<2F><><EFBFBD><EFBFBD> access & refresh token<EFBFBD><EFBFBD>
|
||||||
|
// - TokenService ʵ<><CAB5> ITokenService<63><65><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> DataContext<78><74>ICacheService<63><65>JwtSetting <20><>
|
||||||
|
// - <20><> Server <20><>ע<EFBFBD>룬SignController <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> DI <20><>ȡ<EFBFBD><C8A1>ʵ<EFBFBD><CAB5>
|
||||||
|
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||||
|
|
||||||
|
// SignalR<6C><52><EFBFBD><EFBFBD><EFBFBD>÷<EFBFBD><C3B7><EFBFBD><EFBFBD><EFBFBD> Hub ֧<>֣<EFBFBD>ע<EFBFBD>⣺JWT <20><> OnMessageReceived <20><><EFBFBD><EFBFBD> AuthorizationExtension <20>д<EFBFBD><D0B4><EFBFBD><EFBFBD><EFBFBD>
|
||||||
builder.Services.AddSignalR();
|
builder.Services.AddSignalR();
|
||||||
|
|
||||||
|
// HttpClient <20><><EFBFBD><EFBFBD><EFBFBD>ݷ<EFBFBD><DDB7><EFBFBD>
|
||||||
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost");
|
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost");
|
||||||
builder.Services.AddDataService();
|
builder.Services.AddDataService();
|
||||||
builder.Services.AddAuthorize(builder.Configuration, builder.Environment);
|
builder.Services.AddAuthorize(builder.Configuration, builder.Environment); // <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>úõ<C3BA><C3B5><EFBFBD>֤/<2F><>Ȩ
|
||||||
|
|
||||||
|
// EF Core DbContext
|
||||||
var connection = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
var connection = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
|
||||||
builder.Services.AddDbContext<DataContext>(options => options.UseNpgsql(connection, p => p.MigrationsHistoryTable("__DbMigrationsHistory")));
|
builder.Services.AddDbContext<DataContext>(options => options.UseNpgsql(connection, p => p.MigrationsHistoryTable("__DbMigrationsHistory")));
|
||||||
|
|
||||||
|
// Redis <20><><EFBFBD>棨<EFBFBD>Ѵ<EFBFBD><D1B4>ڣ<EFBFBD>
|
||||||
|
// ... <20><><EFBFBD><EFBFBD>ԭ<EFBFBD><D4AD> Redis ע<><D7A2>
|
||||||
|
|
||||||
var redisConnection = builder.Configuration.GetConnectionString("cache");
|
var redisConnection = builder.Configuration.GetConnectionString("cache");
|
||||||
|
|
||||||
builder.Services.AddStackExchangeRedisCache(options =>
|
builder.Services.AddStackExchangeRedisCache(options =>
|
||||||
{
|
{
|
||||||
#region
|
|
||||||
options.Configuration = redisConnection;
|
options.Configuration = redisConnection;
|
||||||
options.InstanceName = builder.Configuration["RedisCache:InstanceName"];
|
options.InstanceName = builder.Configuration["RedisCache:InstanceName"];
|
||||||
#endregion
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//// Redis<69>ֲ<EFBFBD>ʽ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
// <20><>Ӧѹ<D3A6><D1B9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
//builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
|
||||||
// ConnectionMultiplexer.Connect(redisConnection));
|
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧѹ<D3A6><D1B9>
|
|
||||||
// Ϊ<><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD> BrowserRefresh ע<><D7A2><EFBFBD>ű<EFBFBD><C5B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HTML <20><>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD>ѹ<EFBFBD><D1B9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Content-Encoding: br <20><><EFBFBD><EFBFBD>ע<EFBFBD><D7A2>ʧ<EFBFBD>ܣ<EFBFBD>
|
|
||||||
builder.Services.AddResponseCompression(options =>
|
builder.Services.AddResponseCompression(options =>
|
||||||
{
|
{
|
||||||
options.EnableForHttps = true;
|
options.EnableForHttps = true;
|
||||||
options.Providers.Add<BrotliCompressionProvider>();
|
options.Providers.Add<BrotliCompressionProvider>();
|
||||||
options.Providers.Add<GzipCompressionProvider>();
|
options.Providers.Add<GzipCompressionProvider>();
|
||||||
|
|
||||||
// <20>ų<EFBFBD> text/html<6D><6C>BrowserRefresh <20><>Ҫ<EFBFBD><D2AA>δѹ<CEB4><D1B9><EFBFBD><EFBFBD> HTML <20><>ע<EFBFBD><D7A2><EFBFBD>ű<EFBFBD>
|
|
||||||
options.MimeTypes = ResponseCompressionDefaults.MimeTypes
|
options.MimeTypes = ResponseCompressionDefaults.MimeTypes
|
||||||
.Where(m => !string.Equals(m, "text/html", StringComparison.OrdinalIgnoreCase))
|
.Where(m => !string.Equals(m, "text/html", StringComparison.OrdinalIgnoreCase))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
|
|
||||||
builder.Services.AddAntDesign();
|
builder.Services.AddAntDesign();
|
||||||
|
|
||||||
builder.Services.Configure<MonitoringOptions>(builder.Configuration.GetSection("Monitoring"));
|
builder.Services.Configure<MonitoringOptions>(builder.Configuration.GetSection("Monitoring"));
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.AddDataMigrate();
|
app.AddDataMigrate();
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD>HTTP<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܵ<EFBFBD>
|
// Forwarded headers<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||||
{
|
{
|
||||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||||
});
|
});
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseWebAssemblyDebugging();
|
app.UseWebAssemblyDebugging();
|
||||||
app.MapScalarApiReference(); // ӳ<><D3B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ο<EFBFBD>·<EFBFBD><C2B7>
|
app.MapScalarApiReference();
|
||||||
app.MapOpenApi(); // ӳ<><D3B3> OpenAPI <20>ĵ<EFBFBD>·<EFBFBD><C2B7>
|
app.MapOpenApi();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// <20><>ȫͷ
|
|
||||||
//app.UseSecurityHeaders();
|
|
||||||
|
|
||||||
// <20><>Ӧѹ<D3A6><D1B9>
|
|
||||||
app.UseResponseCompression();
|
app.UseResponseCompression();
|
||||||
|
|
||||||
app.UseCors(option =>
|
app.UseCors(option =>
|
||||||
{
|
{
|
||||||
|
// ע<>⣺<EFBFBD><E2A3BA><EFBFBD><EFBFBD>ʹ<EFBFBD><CAB9> Cookie <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҫ AllowCredentials <20><>ָ<EFBFBD><D6B8><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD><D4B4><EFBFBD>˴<EFBFBD>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD>ʾ<EFBFBD><CABE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD><D4B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||||
option.AllowAnyOrigin();
|
option.AllowAnyOrigin();
|
||||||
option.AllowAnyMethod();
|
option.AllowAnyMethod();
|
||||||
option.AllowAnyHeader();
|
option.AllowAnyHeader();
|
||||||
@@ -160,63 +152,30 @@ app.UseStaticFiles(new StaticFileOptions
|
|||||||
{
|
{
|
||||||
OnPrepareResponse = ctx =>
|
OnPrepareResponse = ctx =>
|
||||||
{
|
{
|
||||||
// <20><><EFBFBD>澲̬<E6BEB2>ļ<EFBFBD>
|
ctx.Context.Response.Headers.Append("Cache-Control", $"public, max-age={31536000}");
|
||||||
ctx.Context.Response.Headers.Append(
|
|
||||||
"Cache-Control", $"public, max-age={31536000}");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
// <20>м<EFBFBD><EFBFBD><EFBFBD>˳<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤ -> <20><>Ȩ
|
||||||
//app.UseRateLimiter();
|
|
||||||
|
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// Antiforgery & <20><><EFBFBD><EFBFBD><EFBFBD>м<EFBFBD><D0BC><EFBFBD>
|
||||||
app.UseAntiforgery();
|
app.UseAntiforgery();
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
|
|
||||||
app.UseMiddleware<MonitoringMiddleware>();
|
app.UseMiddleware<MonitoringMiddleware>();
|
||||||
|
|
||||||
|
// 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><EFBFBD>˵<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><EFBFBD>
|
||||||
//app.MapHealthChecks("/health", new HealthCheckOptions
|
// app.MapHub<ChatHub>("/hubs/chat");
|
||||||
//{
|
// app.MapHub<NotificationHub>("/hubs/notification");
|
||||||
// ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
|
|
||||||
// Predicate = _ => true,
|
|
||||||
// AllowCachingResponses = false
|
|
||||||
//});
|
|
||||||
|
|
||||||
//app.MapHealthChecks("/health/ready", new HealthCheckOptions
|
|
||||||
//{
|
|
||||||
// ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
|
|
||||||
// Predicate = check => check.Tags.Contains("ready")
|
|
||||||
//});
|
|
||||||
|
|
||||||
//app.MapHealthChecks("/health/live", new HealthCheckOptions
|
|
||||||
//{
|
|
||||||
// ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
|
|
||||||
// Predicate = _ => false
|
|
||||||
//});
|
|
||||||
|
|
||||||
// SignalR<6C>˵<EFBFBD>
|
|
||||||
//app.MapHub<ChatHub>("/hubs/chat");
|
|
||||||
//app.MapHub<NotificationHub>("/hubs/notification");
|
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
// Blazor <20><><EFBFBD>ã<EFBFBD>Server + WASM render modes<65><73>
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode()
|
.AddInteractiveServerRenderMode()
|
||||||
.AddInteractiveWebAssemblyRenderMode()
|
.AddInteractiveWebAssemblyRenderMode()
|
||||||
.AddAdditionalAssemblies(typeof(Atomx.Admin.Client._Imports).Assembly);
|
.AddAdditionalAssemblies(typeof(Atomx.Admin.Client._Imports).Assembly);
|
||||||
|
|
||||||
|
|
||||||
//// ȷ<><C8B7><EFBFBD><EFBFBD><EFBFBD>ݿⴴ<DDBF><E2B4B4><EFBFBD><EFBFBD>Ǩ<EFBFBD><C7A8>
|
|
||||||
//await using (var scope = app.Services.CreateAsyncScope())
|
|
||||||
//{
|
|
||||||
// var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
|
||||||
// await dbContext.Database.MigrateAsync();
|
|
||||||
|
|
||||||
// var seeder = scope.ServiceProvider.GetRequiredService<DatabaseSeeder>();
|
|
||||||
// await seeder.SeedAsync();
|
|
||||||
//}
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
Reference in New Issue
Block a user