chore
This commit is contained in:
@@ -20,6 +20,6 @@
|
||||
/// <summary>
|
||||
/// 是否记住我
|
||||
/// </summary>
|
||||
public bool SaveMe { get; set; }
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
@layout EmptyLayout
|
||||
@inject ILogger<Login> Logger
|
||||
|
||||
|
||||
<PageTitle>登录</PageTitle>
|
||||
|
||||
@if (!dataLoaded)
|
||||
@@ -67,7 +66,6 @@ else
|
||||
|
||||
private bool _isLoading = false;
|
||||
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (OperatingSystem.IsBrowser())
|
||||
@@ -85,14 +83,11 @@ else
|
||||
if (firstRender)
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (authState.User.Identity != null)
|
||||
{
|
||||
if (authState.User.Identity.IsAuthenticated)
|
||||
if (authState.User.Identity != null && authState.User.Identity.IsAuthenticated)
|
||||
{
|
||||
Navigation.NavigateTo(ReturnUrl ?? "/");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!dataLoaded)
|
||||
{
|
||||
dataLoaded = true;
|
||||
@@ -102,32 +97,49 @@ else
|
||||
|
||||
private async Task LoginAsync()
|
||||
{
|
||||
if (form.Validate())
|
||||
if (!form.Validate()) return;
|
||||
|
||||
_isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
// 请求后端登录接口,后端返回 ApiResult<AuthResponse>
|
||||
var api = "/api/sign/in";
|
||||
var result = await HttpService.Post<ApiResult<string>>(api, login);
|
||||
if (result.Success)
|
||||
var result = await HttpService.Post<ApiResult<AuthResponse>>(api, login);
|
||||
if (result.Success && result.Data != null)
|
||||
{
|
||||
Console.WriteLine("请求api成功");
|
||||
if (!string.IsNullOrEmpty(result.Data))
|
||||
var auth = 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);
|
||||
await localStorage.SetItemAsStringAsync("refreshToken", result.Data);
|
||||
var authState = (AuthStateProvider as PersistentAuthenticationStateProvider);
|
||||
if (authState != null)
|
||||
{
|
||||
authState.UpdateAuthenticationState(result.Data);
|
||||
// provider 仅需要 access token 更新来触发 UI 更新
|
||||
provider.UpdateAuthenticationState(auth.Token);
|
||||
}
|
||||
Logger.LogInformation($"登录成功跳转目标,{ReturnUrl}");
|
||||
|
||||
Logger.LogInformation("登录成功,跳转: {ReturnUrl}", ReturnUrl);
|
||||
Navigation.NavigateTo(ReturnUrl ?? "/");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -6,26 +6,26 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
|
||||
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();
|
||||
|
||||
// <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.AddCascadingAuthenticationState();
|
||||
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<IconsExtension>();
|
||||
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>();
|
||||
|
||||
// ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD> token & ˢ<>µ<EFBFBD> DelegatingHandler
|
||||
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;
|
||||
builder.Services.AddHttpClient("ApiClient", client =>
|
||||
{
|
||||
@@ -33,8 +33,14 @@ builder.Services.AddHttpClient("ApiClient", client =>
|
||||
})
|
||||
.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>
|
||||
// Ҳע<EFBFBD><EFBFBD>Ĭ<EFBFBD><EFBFBD> HttpClient <20><> BaseAddress
|
||||
// ע<><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> 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"));
|
||||
|
||||
// <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
|
||||
/// - 在 401 或响应头包含 "Token-Expired" 时尝试刷新(调用 /api/sign/refresh)
|
||||
/// - 防止并发刷新(SemaphoreSlim)
|
||||
/// - 注意:刷新请求必须使用一个不包含本 handler 的 HttpClient(避免循环)
|
||||
/// </summary>
|
||||
public class AuthHeaderHandler : DelegatingHandler
|
||||
{
|
||||
@@ -45,6 +46,7 @@ namespace Atomx.Admin.Client.Utils
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从 ITokenProvider 获取当前 access token(WASM: ClientTokenProvider 从 localStorage 读取)
|
||||
var token = await _tokenProvider.GetTokenAsync();
|
||||
if (!string.IsNullOrEmpty(token))
|
||||
{
|
||||
@@ -57,6 +59,7 @@ namespace Atomx.Admin.Client.Utils
|
||||
|
||||
var response = await base.SendAsync(request, cancellationToken);
|
||||
|
||||
// 当发现未授权或后端标记 token 过期时,尝试刷新
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized ||
|
||||
response.Headers.Contains("Token-Expired"))
|
||||
{
|
||||
@@ -76,6 +79,7 @@ namespace Atomx.Admin.Client.Utils
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新失败或无 token,跳转到登录页(强制刷新页面,清除 SPA 状态)
|
||||
_navigationManager.NavigateTo("/account/login", true);
|
||||
}
|
||||
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)
|
||||
{
|
||||
await _refreshLock.WaitAsync(cancellationToken);
|
||||
@@ -107,11 +118,13 @@ namespace Atomx.Admin.Client.Utils
|
||||
return false;
|
||||
}
|
||||
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
var reqModel = new
|
||||
// 使用不带本 handler 的 HttpClient 发送刷新请求,避免循环
|
||||
var client = _httpClientFactory.CreateClient("RefreshClient");
|
||||
|
||||
var reqModel = new RefreshRequest
|
||||
{
|
||||
token = currentAccess,
|
||||
refreshToken = currentRefresh
|
||||
Token = currentAccess,
|
||||
RefreshToken = currentRefresh
|
||||
};
|
||||
var reqJson = JsonSerializer.Serialize(reqModel);
|
||||
using var req = new HttpRequestMessage(HttpMethod.Post, "api/sign/refresh")
|
||||
@@ -125,6 +138,9 @@ namespace Atomx.Admin.Client.Utils
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Refresh request failed with status {Status}", resp.StatusCode);
|
||||
// 如果刷新失败,移除本地 token(防止无限重试)
|
||||
await _localStorage.RemoveItemAsync(AccessTokenKey);
|
||||
await _localStorage.RemoveItemAsync(RefreshTokenKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -137,9 +153,12 @@ namespace Atomx.Admin.Client.Utils
|
||||
if (authResp == null || string.IsNullOrEmpty(authResp.Token) || string.IsNullOrEmpty(authResp.RefreshToken))
|
||||
{
|
||||
_logger.LogWarning("Invalid response from refresh endpoint");
|
||||
await _localStorage.RemoveItemAsync(AccessTokenKey);
|
||||
await _localStorage.RemoveItemAsync(RefreshTokenKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 保存新的 tokens 到 localStorage
|
||||
await _localStorage.SetItemAsync(AccessTokenKey, authResp.Token, 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)
|
||||
{
|
||||
var clone = new HttpRequestMessage(original.Method, original.RequestUri);
|
||||
|
||||
// 复制内容(如果存在)
|
||||
if (original.Content != null)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
@@ -176,9 +199,11 @@ namespace Atomx.Admin.Client.Utils
|
||||
}
|
||||
}
|
||||
|
||||
// 复制 headers
|
||||
foreach (var header in original.Headers)
|
||||
clone.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
|
||||
// 覆盖 Authorization
|
||||
clone.Headers.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
|
||||
|
||||
return clone;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
|
||||
using Atomx.Admin.Client.Models;
|
||||
using Atomx.Admin.Client.Models;
|
||||
using Atomx.Admin.Client.Validators;
|
||||
using Atomx.Admin.Services;
|
||||
using Atomx.Admin.Utils;
|
||||
@@ -16,27 +15,42 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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]")]
|
||||
[ApiController]
|
||||
public class SignController : ControllerBase
|
||||
{
|
||||
readonly ILogger<SignController> _logger;
|
||||
readonly IIdentityService _identityService;
|
||||
readonly IIdCreatorService _idCreator;
|
||||
readonly IMapper _mapper;
|
||||
readonly DataContext _dbContext;
|
||||
readonly JwtSetting _jwtSetting;
|
||||
readonly ICacheService _cacheService;
|
||||
readonly AuthenticationStateProvider _authenticationStateProvider;
|
||||
private readonly ILogger<SignController> _logger;
|
||||
private readonly IIdentityService _identityService;
|
||||
private readonly IIdCreatorService _idCreator;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly DataContext _dbContext;
|
||||
private readonly JwtSetting _jwtSetting;
|
||||
private readonly ICacheService _cacheService;
|
||||
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;
|
||||
_identityService = identityService;
|
||||
@@ -46,106 +60,201 @@ namespace Atomx.Admin.Controllers
|
||||
_jwtSetting = jwtSetting;
|
||||
_cacheService = cacheService;
|
||||
_authenticationStateProvider = authenticationStateProvider;
|
||||
_tokenService = tokenService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户登录系统
|
||||
/// 登录:支持邮箱或用户名登录
|
||||
/// - 返回 AuthResponse 给 WASM(access + refresh)
|
||||
/// - 在 Server 场景同时创建 Cookie(兼容 Blazor Server)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("in")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Login(LoginModel model)
|
||||
public async Task<IActionResult> Login([FromBody] LoginModel model)
|
||||
{
|
||||
var validator = new LoginModelValidator();
|
||||
var validation = validator.Validate(model);
|
||||
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
var message = validation.Errors.FirstOrDefault()?.ErrorMessage;
|
||||
var result = new ApiResult<string>().IsFail(message ?? string.Empty, null);
|
||||
return new JsonResult(result);
|
||||
var message = validation.Errors.FirstOrDefault()?.ErrorMessage ?? string.Empty;
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail(message, null));
|
||||
}
|
||||
|
||||
var tokenHandler = new JwtSecurityTokenHandler();
|
||||
var issuer = _jwtSetting.Issuer;
|
||||
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;
|
||||
// 查询用户(支持 email / username)
|
||||
Atomx.Common.Entities.Admin? user = null;
|
||||
if (model.Account.Contains("@"))
|
||||
{
|
||||
user = _dbContext.Admins.Where(p => p.Email == model.Account).SingleOrDefault();
|
||||
user = _dbContext.Admins.SingleOrDefault(p => p.Email == model.Account);
|
||||
}
|
||||
else
|
||||
{
|
||||
user = _dbContext.Admins.Where(p => p.Username == model.Account).SingleOrDefault();
|
||||
user = _dbContext.Admins.SingleOrDefault(p => p.Username == model.Account);
|
||||
}
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
var result = new ApiResult<string>().IsFail("用户不存在", null);
|
||||
return new JsonResult(result);
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail("用户不存在", null));
|
||||
}
|
||||
|
||||
// 简单密码校验(项目 uses MD5 存储示例)
|
||||
if (user.Password != model.Password.ToMd5Password())
|
||||
{
|
||||
var result = new ApiResult<string>().IsFail("账号密码不正确", null);
|
||||
return new JsonResult(result);
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsFail("账号密码不正确", null));
|
||||
}
|
||||
|
||||
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;
|
||||
user.LastIp = ip;
|
||||
user.LoginCount++;
|
||||
_dbContext.Admins.Update(user);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
|
||||
// 为 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),
|
||||
new Claim(ClaimKeys.Name, user.Username),
|
||||
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 tokenDescriptor = new SecurityTokenDescriptor
|
||||
// SignInAsync 创建 HttpOnly Cookie,便于 Server-side 认证
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
|
||||
new ClaimsPrincipal(claimsIdentity),
|
||||
new AuthenticationProperties
|
||||
{
|
||||
Subject = claimsIdentity,
|
||||
IsPersistent = model.RememberMe,
|
||||
AllowRefresh = true,
|
||||
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes)
|
||||
});
|
||||
|
||||
Expires = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes),
|
||||
SigningCredentials = credentials,
|
||||
Issuer = issuer,
|
||||
Audience = audience
|
||||
// 另外将 tokens 写入 HttpOnly Cookie(增强与传统中间件的兼容性)
|
||||
try
|
||||
{
|
||||
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);
|
||||
|
||||
var tokenString = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));
|
||||
|
||||
var loginResult = new ApiResult<string>().IsSuccess(tokenString);
|
||||
|
||||
user.LastLogin = DateTime.UtcNow;
|
||||
user.LastIp = _identityService.GetClientIp();
|
||||
user.LoginCount++;
|
||||
_dbContext.Admins.Update(user);
|
||||
_dbContext.SaveChanges();
|
||||
|
||||
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
|
||||
|
||||
return new JsonResult(loginResult);
|
||||
var refreshCookieOptions = new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
//Secure = !Request.IsLocal(),
|
||||
SameSite = SameSiteMode.Lax,
|
||||
Expires = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.RefreshTokenExpirationMinutes),
|
||||
Path = "/"
|
||||
};
|
||||
Response.Cookies.Append("refresh_token", authResponse.RefreshToken, refreshCookieOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "设置 token cookie 失败(非致命)");
|
||||
}
|
||||
|
||||
return new JsonResult(new ApiResult<AuthResponse>().IsSuccess(authResponse));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户退出系统
|
||||
/// 刷新:客户端传入(可能已过期的)access token 与 refresh token(明文)
|
||||
/// - TokenService.RefreshTokenAsync 会验证并一次性撤销旧的 refresh token,生成新的对
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("out")]
|
||||
[HttpPost("refresh")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LogoutAsync()
|
||||
public async Task<IActionResult> Refresh([FromBody] RefreshRequest request)
|
||||
{
|
||||
await HttpContext.SignOutAsync();
|
||||
return new JsonResult(new ApiResult<string>());
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.Token) || string.IsNullOrWhiteSpace(request.RefreshToken))
|
||||
{
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// AuthorizationExtension 重构说明:
|
||||
/// - 配置 Cookie + JwtBearer 双方案,JwtBearer 的 Events 中添加对 SignalR / WebSocket 的 query string access_token 读取(OnMessageReceived)
|
||||
/// - 保持 OnChallenge 的重定向行为(对于浏览器访问 API 时友好)
|
||||
/// - 将 JwtSetting 注入为 Singleton 供 TokenService 使用
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
var jwtSetting = Configuration.GetSection("Authentication:JwtBearer").Get<JwtSetting>();
|
||||
if (jwtSetting == null)
|
||||
{
|
||||
throw new Exception("缺少配置信息");
|
||||
throw new Exception("缺少配置信息 Authentication:JwtBearer");
|
||||
}
|
||||
services.AddSingleton(jwtSetting);
|
||||
|
||||
// 从配置读取 Cookie 设置(可在 appsettings.json 的 Authentication:Cookie 节点配置)
|
||||
// Cookie 配置读取
|
||||
var cookieConf = Configuration.GetSection("Authentication:Cookie");
|
||||
var cookieName = cookieConf.GetValue<string>("Name") ?? ".Atomx.Auth";
|
||||
var cookiePath = cookieConf.GetValue<string>("Path") ?? "/";
|
||||
@@ -36,30 +35,26 @@ namespace Atomx.Admin.Extensions
|
||||
var securePolicyStr = cookieConf.GetValue<string>("SecurePolicy");
|
||||
var expireMinutes = cookieConf.GetValue<int?>("ExpireMinutes") ?? 60;
|
||||
|
||||
// 解析 SameSite(默认:开发环境 Strict,生产环境 None 用于跨站点场景比如前后端分离)
|
||||
SameSiteMode sameSiteMode;
|
||||
if (string.IsNullOrWhiteSpace(sameSiteStr) || !Enum.TryParse<SameSiteMode>(sameSiteStr, true, out sameSiteMode))
|
||||
{
|
||||
sameSiteMode = environment.IsDevelopment() ? SameSiteMode.Strict : SameSiteMode.None;
|
||||
}
|
||||
|
||||
// 解析 SecurePolicy(默认:开发 SameAsRequest,生产 Always)
|
||||
CookieSecurePolicy securePolicy;
|
||||
if (string.IsNullOrWhiteSpace(securePolicyStr) || !Enum.TryParse<CookieSecurePolicy>(securePolicyStr, true, out securePolicy))
|
||||
{
|
||||
securePolicy = environment.IsDevelopment() ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.Always;
|
||||
}
|
||||
|
||||
//认证配置:注册 Cookie(用于 SignIn/SignOut)和 JwtBearer(用于 API 授权)
|
||||
services.AddAuthentication(options =>
|
||||
{
|
||||
// 默认用于 API 的认证/挑战方案使用 JwtBearer
|
||||
// 默认用于 API 的认证方案为 JwtBearer
|
||||
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
|
||||
{
|
||||
// Cookie 配置,确保 SignInAsync 能找到处理器
|
||||
options.Cookie.Name = cookieName;
|
||||
options.Cookie.Path = cookiePath;
|
||||
if (!string.IsNullOrWhiteSpace(cookieDomain))
|
||||
@@ -88,25 +83,37 @@ namespace Atomx.Admin.Extensions
|
||||
ValidateAudience = true,
|
||||
ValidateLifetime = true,
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidAudience = jwtSetting.Audience,//Audience
|
||||
ValidAudience = jwtSetting.Audience,
|
||||
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))
|
||||
};
|
||||
|
||||
// 允许从 query string 中获取 access_token(用于 SignalR / WebSocket)
|
||||
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 =>
|
||||
{
|
||||
// 浏览器端访问 API 并无 token 时重定向到登录页
|
||||
var absoluteUri = $"{context.Request.Scheme}://{context.Request.Host}{context.Request.Path}{context.Request.QueryString}";
|
||||
context.HandleResponse();
|
||||
context.Response.Redirect($"/account/login?returnUrl={Uri.EscapeDataString(absoluteUri)}");
|
||||
return Task.CompletedTask;
|
||||
},
|
||||
|
||||
OnAuthenticationFailed = context =>
|
||||
{
|
||||
//Token expired
|
||||
if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
|
||||
if (context.Exception?.GetType() == typeof(SecurityTokenExpiredException))
|
||||
{
|
||||
context.Response.Headers.Append("Token-Expired", "true");
|
||||
}
|
||||
@@ -115,13 +122,13 @@ namespace Atomx.Admin.Extensions
|
||||
};
|
||||
});
|
||||
|
||||
// 注册基于权限的策略
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
// 基于权限的策略
|
||||
var allPermissions = Permissions.GetAllPermissions();
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求监控中间件
|
||||
/// 变更点:
|
||||
/// - 不再在构造函数注入作用域(scoped)服务 IIdentityService,避免在应用启动时从根 provider 解析 scoped 服务导致异常。
|
||||
/// - 在每次请求处理时通过 HttpContext.RequestServices 获取 IIdentityService(请求作用域内解析)。
|
||||
/// </summary>
|
||||
public class MonitoringMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<MonitoringMiddleware> _logger;
|
||||
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;
|
||||
_logger = logger;
|
||||
_options = options.Value;
|
||||
_identityService = identityService;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
// 在请求作用域内解析 IIdentityService,避免在中间件构造时从根 provider 解析 scoped 服务
|
||||
var identityService = context.RequestServices.GetService<IIdentityService>();
|
||||
|
||||
// 检查是否应该跳过监控
|
||||
if (ShouldSkipMonitoring(context))
|
||||
{
|
||||
@@ -32,7 +39,7 @@ namespace Atomx.Admin.Middlewares
|
||||
|
||||
var logInfo = new
|
||||
{
|
||||
UserId = _identityService.GetUserId(),
|
||||
UserId = identityService?.GetUserId(),
|
||||
Path = context.Request.Path,
|
||||
Method = context.Request.Method,
|
||||
StartTime = DateTime.UtcNow,
|
||||
@@ -50,7 +57,6 @@ namespace Atomx.Admin.Middlewares
|
||||
{
|
||||
stopwatch.Stop();
|
||||
|
||||
|
||||
var logData = new
|
||||
{
|
||||
Path = context.Request.Path,
|
||||
|
||||
@@ -28,7 +28,7 @@ using System.Text.Unicode;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Configure Serilog
|
||||
// Serilog <20><><EFBFBD>ã<EFBFBD><C3A3><EFBFBD><EFBFBD><EFBFBD>ԭ<EFBFBD><D4AD><EFBFBD><EFBFBD>
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
.Enrich.FromLogContext()
|
||||
@@ -37,7 +37,7 @@ Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
|
||||
.CreateLogger();
|
||||
|
||||
// Add services to the container.
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ע<EFBFBD><EFBFBD>
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents()
|
||||
.AddInteractiveWebAssemblyComponents();
|
||||
@@ -56,101 +56,93 @@ builder.Services.AddBlazoredLocalStorage();
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
// Ȩ<><EFBFBD><DEB7><EFBFBD>
|
||||
// Ȩ<><EFBFBD><DEB7><EFBFBD> & <20><>Ȩ<EFBFBD><C8A8><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
builder.Services.AddScoped<IPermissionService, PermissionService>();
|
||||
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>();
|
||||
// <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>();
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߷<EFBFBD><DFB7><EFBFBD>
|
||||
builder.Services.AddScoped<IIdCreatorService, IdCreatorService>();
|
||||
builder.Services.AddScoped<IIdentityService, IdentityService>();
|
||||
builder.Services.AddScoped<ILocalizationService, LocalizationService>();
|
||||
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<ITokenService, TokenService>(); // ע<><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD> TokenService
|
||||
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();
|
||||
|
||||
// HttpClient <20><><EFBFBD><EFBFBD><EFBFBD>ݷ<EFBFBD><DDB7><EFBFBD>
|
||||
builder.Services.AddHttpClientApiService(builder.Configuration["WebApi:ServerUrl"] ?? "http://localhost");
|
||||
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.");
|
||||
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");
|
||||
|
||||
builder.Services.AddStackExchangeRedisCache(options =>
|
||||
{
|
||||
#region
|
||||
options.Configuration = redisConnection;
|
||||
options.InstanceName = builder.Configuration["RedisCache:InstanceName"];
|
||||
#endregion
|
||||
});
|
||||
|
||||
//// Redis<69>ֲ<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>
|
||||
// <20><>Ӧѹ<D3A6><D1B9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
builder.Services.AddResponseCompression(options =>
|
||||
{
|
||||
options.EnableForHttps = true;
|
||||
options.Providers.Add<BrotliCompressionProvider>();
|
||||
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
|
||||
.Where(m => !string.Equals(m, "text/html", StringComparison.OrdinalIgnoreCase))
|
||||
.ToArray();
|
||||
});
|
||||
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
|
||||
builder.Services.AddAntDesign();
|
||||
|
||||
builder.Services.Configure<MonitoringOptions>(builder.Configuration.GetSection("Monitoring"));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
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
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||
});
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseWebAssemblyDebugging();
|
||||
app.MapScalarApiReference(); // ӳ<><D3B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ο<EFBFBD>·<EFBFBD><C2B7>
|
||||
app.MapOpenApi(); // ӳ<><D3B3> OpenAPI <20>ĵ<EFBFBD>·<EFBFBD><C2B7>
|
||||
app.MapScalarApiReference();
|
||||
app.MapOpenApi();
|
||||
}
|
||||
else
|
||||
{
|
||||
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
||||
}
|
||||
|
||||
// <20><>ȫͷ
|
||||
//app.UseSecurityHeaders();
|
||||
|
||||
// <20><>Ӧѹ<D3A6><D1B9>
|
||||
app.UseResponseCompression();
|
||||
|
||||
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.AllowAnyMethod();
|
||||
option.AllowAnyHeader();
|
||||
@@ -160,63 +152,30 @@ app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
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>
|
||||
//app.UseRateLimiter();
|
||||
|
||||
// <20>м<EFBFBD><EFBFBD><EFBFBD>˳<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤ -> <20><>Ȩ
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
// Antiforgery & <20><><EFBFBD><EFBFBD><EFBFBD>м<EFBFBD><D0BC><EFBFBD>
|
||||
app.UseAntiforgery();
|
||||
app.MapStaticAssets();
|
||||
|
||||
app.UseMiddleware<MonitoringMiddleware>();
|
||||
|
||||
|
||||
//// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˵<EFBFBD>
|
||||
//app.MapHealthChecks("/health", new HealthCheckOptions
|
||||
//{
|
||||
// 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>
|
||||
// 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><EFBFBD>
|
||||
// app.MapHub<ChatHub>("/hubs/chat");
|
||||
// app.MapHub<NotificationHub>("/hubs/notification");
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
// Blazor <20><><EFBFBD>ã<EFBFBD>Server + WASM render modes<65><73>
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
.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();
|
||||
|
||||
Reference in New Issue
Block a user