From 6217a8ca556b3266a2dadb857e7813b9f94f1095 Mon Sep 17 00:00:00 2001 From: yxw <17074267@qq.com> Date: Thu, 4 Dec 2025 17:14:46 +0800 Subject: [PATCH] chore --- Atomx.Admin/Atomx.Admin.Client/Program.cs | 3 - .../Services/ITokenProvider.cs | 24 -- .../Utils/AuthHeaderHandler.cs | 25 +- .../Utils/ClientTokenProvider.cs | 38 -- Atomx.Admin/Atomx.Admin/Components/App.razor | 4 +- .../Atomx.Admin/Controllers/SignController.cs | 281 +++++++++++- .../Extensions/AuthorizationExtension.cs | 43 +- Atomx.Admin/Atomx.Admin/Program.cs | 8 - .../Atomx.Admin/Services/ITokenService.cs | 20 - .../Atomx.Admin/Services/TokenService.cs | 400 ------------------ .../Atomx.Admin/Utils/ServerTokenProvider.cs | 108 ----- 11 files changed, 283 insertions(+), 671 deletions(-) delete mode 100644 Atomx.Admin/Atomx.Admin.Client/Services/ITokenProvider.cs delete mode 100644 Atomx.Admin/Atomx.Admin.Client/Utils/ClientTokenProvider.cs delete mode 100644 Atomx.Admin/Atomx.Admin/Services/ITokenService.cs delete mode 100644 Atomx.Admin/Atomx.Admin/Services/TokenService.cs delete mode 100644 Atomx.Admin/Atomx.Admin/Utils/ServerTokenProvider.cs diff --git a/Atomx.Admin/Atomx.Admin.Client/Program.cs b/Atomx.Admin/Atomx.Admin.Client/Program.cs index a399d07..8234f94 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Program.cs +++ b/Atomx.Admin/Atomx.Admin.Client/Program.cs @@ -19,9 +19,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -// Token provider(WASM): 从 localStorage 读取 access token -builder.Services.AddScoped(); - // 注册用于自动附带 token & 刷新的 DelegatingHandler builder.Services.AddScoped(); diff --git a/Atomx.Admin/Atomx.Admin.Client/Services/ITokenProvider.cs b/Atomx.Admin/Atomx.Admin.Client/Services/ITokenProvider.cs deleted file mode 100644 index c2b049a..0000000 --- a/Atomx.Admin/Atomx.Admin.Client/Services/ITokenProvider.cs +++ /dev/null @@ -1,24 +0,0 @@ -锘縰sing System.Threading.Tasks; - -namespace Atomx.Admin.Client.Services -{ - /// - /// 缁熶竴鐨 Token 鎻愪緵鍣ㄦ帴鍙o紙鏀惧湪鍏变韩椤圭洰锛 - /// 鐩爣锛 - /// - Server 涓 WASM 浣跨敤鐩稿悓鐨勬帴鍙g被鍨嬩互閬垮厤 DI 娉ㄥ叆绫诲瀷涓嶄竴鑷 - /// - 浠呰礋璐b滄彁渚涒濆綋鍓嶅彲鐢ㄧ殑 access token锛堜笉鎵挎媴鍒锋柊绛栫暐锛 - /// - public interface ITokenProvider - { - /// - /// 杩斿洖褰撳墠鍙敤鐨 access token锛堝鏋滄病鏈夊垯杩斿洖 null锛 - /// - Task GetTokenAsync(); - - /// - /// 蹇熷垽鏂綋鍓 token 鏄惁瀛樺湪涓旓紙濡傛灉鍙互瑙f瀽涓 JWT锛夋湭杩囨湡銆 - /// 娉ㄦ剰锛氭鏂规硶涓哄揩閫熸鏌ワ紝涓嶈兘鏇夸唬鏈嶅姟绔殑瀹屾暣楠岃瘉銆 - /// - Task IsTokenValidAsync(); - } -} diff --git a/Atomx.Admin/Atomx.Admin.Client/Utils/AuthHeaderHandler.cs b/Atomx.Admin/Atomx.Admin.Client/Utils/AuthHeaderHandler.cs index cca5568..7c62092 100644 --- a/Atomx.Admin/Atomx.Admin.Client/Utils/AuthHeaderHandler.cs +++ b/Atomx.Admin/Atomx.Admin.Client/Utils/AuthHeaderHandler.cs @@ -18,7 +18,6 @@ namespace Atomx.Admin.Client.Utils /// public class AuthHeaderHandler : DelegatingHandler { - private readonly ITokenProvider _tokenProvider; private readonly NavigationManager _navigationManager; private readonly ILogger _logger; private readonly ILocalStorageService _localStorage; @@ -26,13 +25,11 @@ namespace Atomx.Admin.Client.Utils private static readonly SemaphoreSlim _refreshLock = new(1, 1); public AuthHeaderHandler( - ITokenProvider tokenProvider, NavigationManager navigationManager, ILogger logger, ILocalStorageService localStorage, IHttpClientFactory httpClientFactory) { - _tokenProvider = tokenProvider; _navigationManager = navigationManager; _logger = logger; _localStorage = localStorage; @@ -45,7 +42,12 @@ namespace Atomx.Admin.Client.Utils try { // 浠 ITokenProvider 鑾峰彇褰撳墠 access token锛圵ASM: ClientTokenProvider 浠 localStorage 璇诲彇锛 - var token = await _tokenProvider.GetTokenAsync(); + var token = string.Empty; + try + { + token = await _localStorage.GetItemAsync(StorageKeys.AccessToken); + } + catch { } if (!string.IsNullOrEmpty(token)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); @@ -206,5 +208,20 @@ namespace Atomx.Admin.Client.Utils return clone; } + + private async Task HandleUnauthorizedAsync() + { + // 鍦╓ASM妯″紡涓嬮噸瀹氬悜鍒扮櫥褰曢〉 + if (OperatingSystem.IsBrowser()) + { + _navigationManager.NavigateTo("/account/login", true); + } + // 鍦⊿erver妯″紡涓嬪彲浠ユ墽琛屽叾浠栨搷浣 + else + { + // Server绔殑澶勭悊閫昏緫 + _logger.LogWarning("Unauthorized access detected in server mode"); + } + } } } \ No newline at end of file diff --git a/Atomx.Admin/Atomx.Admin.Client/Utils/ClientTokenProvider.cs b/Atomx.Admin/Atomx.Admin.Client/Utils/ClientTokenProvider.cs deleted file mode 100644 index 4a5a334..0000000 --- a/Atomx.Admin/Atomx.Admin.Client/Utils/ClientTokenProvider.cs +++ /dev/null @@ -1,38 +0,0 @@ -锘縰sing Atomx.Admin.Client.Services; -using Microsoft.JSInterop; - -namespace Atomx.Admin.Client.Utils -{ - /// - /// WASM 瀹㈡埛绔笅鐨 Token 鎻愪緵鍣紙瀹炵幇鍏变韩鐨 ITokenProvider锛 - /// - 鐩存帴浠庢祻瑙堝櫒 storage锛坙ocalStorage/sessionStorage锛夎鍙 access token - /// - 璁捐涓鸿交閲忥紝浠呰礋璐h鍙 token锛涘埛鏂伴昏緫鏀惧湪 AuthHeaderHandler / 鍚庣 Refresh 鎺ュ彛 - /// - public class ClientTokenProvider : ITokenProvider - { - private readonly IJSRuntime _jsRuntime; - - public ClientTokenProvider(IJSRuntime jsRuntime) - { - _jsRuntime = jsRuntime; - } - - public async Task GetTokenAsync() - { - try - { - return await _jsRuntime.InvokeAsync("localStorage.getItem", "accessToken"); - } - catch - { - return null; - } - } - - public async Task IsTokenValidAsync() - { - var token = await GetTokenAsync(); - return !string.IsNullOrEmpty(token); - } - } -} diff --git a/Atomx.Admin/Atomx.Admin/Components/App.razor b/Atomx.Admin/Atomx.Admin/Components/App.razor index 531e327..bc8e9cc 100644 --- a/Atomx.Admin/Atomx.Admin/Components/App.razor +++ b/Atomx.Admin/Atomx.Admin/Components/App.razor @@ -11,11 +11,11 @@ - + - + diff --git a/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs b/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs index 20cfd86..c614ab5 100644 --- a/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs +++ b/Atomx.Admin/Atomx.Admin/Controllers/SignController.cs @@ -2,6 +2,7 @@ using Atomx.Admin.Client.Validators; using Atomx.Admin.Services; using Atomx.Common.Constants; +using Atomx.Common.Entities; using Atomx.Common.Models; using Atomx.Data; using Atomx.Data.CacheServices; @@ -13,8 +14,12 @@ using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; namespace Atomx.Admin.Controllers { @@ -38,7 +43,6 @@ namespace Atomx.Admin.Controllers private readonly JwtSetting _jwtSetting; private readonly ICacheService _cacheService; private readonly AuthenticationStateProvider _authenticationStateProvider; - private readonly ITokenService _tokenService; public SignController( ILogger logger, @@ -48,8 +52,7 @@ namespace Atomx.Admin.Controllers DataContext dbContext, JwtSetting jwtSetting, ICacheService cacheService, - AuthenticationStateProvider authenticationStateProvider, - ITokenService tokenService) + AuthenticationStateProvider authenticationStateProvider) { _logger = logger; _identityService = identityService; @@ -59,7 +62,6 @@ namespace Atomx.Admin.Controllers _jwtSetting = jwtSetting; _cacheService = cacheService; _authenticationStateProvider = authenticationStateProvider; - _tokenService = tokenService; } /// @@ -101,19 +103,20 @@ namespace Atomx.Admin.Controllers return new JsonResult(new ApiResult().IsFail("璐﹀彿瀵嗙爜涓嶆纭", null)); } - // 鐢熸垚 access + refresh锛圱okenService 浼氭妸 refresh 鐨勫搱甯屼繚瀛樺埌鏁版嵁搴擄級 - var ip = _identityService.GetClientIp(); - var userAgent = Request.Headers["User-Agent"].FirstOrDefault(); - var authResponse = await _tokenService.GenerateTokenAsync(user, ip, userAgent); + if (user.LockoutEndTime.HasValue && user.LockoutEndTime.Value > DateTime.UtcNow) + { + return new JsonResult(new ApiResult().IsFail($"璐﹀彿宸查攣瀹氾紝瑙i攣鏃堕棿锛歿user.LockoutEndTime.Value.ToLocalTime()}", null)); + } - // 鏇存柊鐢ㄦ埛鐧诲綍缁熻淇℃伅 - user.LastLogin = DateTime.UtcNow; - user.LastIp = ip; - user.LoginCount++; - _dbContext.Admins.Update(user); - await _dbContext.SaveChangesAsync(); + var tokenHandler = new JwtSecurityTokenHandler(); + var issuer = _jwtSetting.Issuer; + var audience = _jwtSetting.Audience; + var securityKey = _jwtSetting.SecurityKey; - // 涓 Blazor Server 鍦烘櫙鍒涘缓 Cookie锛圕laims 涓寘鍚繀瑕佽鑹/鏉冮檺锛 + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey)); + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + // Claims 涓寘鍚繀瑕佽鑹/鏉冮檺 var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId); var claims = new List { @@ -124,9 +127,61 @@ namespace Atomx.Admin.Controllers new Claim(ClaimKeys.Permission, role?.Permission ?? string.Empty) }; + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + + Expires = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes), + SigningCredentials = credentials, + Issuer = issuer, + Audience = audience + }; + + // 鐢熸垚 access token + var accessToken = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); + + // 鐢熸垚 refresh token锛堟槑鏂囷級 + var refreshToken = GenerateRefreshToken(); + var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); - // SignInAsync 鍒涘缓 HttpOnly Cookie锛屼究浜 Server-side 璁よ瘉 + // 淇濆瓨 refresh token 鐨勫搱甯屽埌鏁版嵁搴擄紙涓嶅彲閫嗭級 + var refreshTokenEntity = new RefreshToken + { + Token = HashRefreshToken(refreshToken), + UserId = user.Id, + IssuedTime = DateTime.UtcNow, + ExpiresTime = DateTime.UtcNow.AddMinutes(_jwtSetting.RefreshTokenExpirationMinutes), + Ip = _identityService.GetClientIp(), + UserAgent = _identityService.GetUserAgent() + }; + + // 淇濈暀鏈鏂 N 涓湭鎾ら攢鐨勫埛鏂颁护鐗岋紝鍏朵綑鏍囪涓烘挙閿 + await RemoveOldRefreshTokensAsync(user.Id); + + // 鏇存柊鐢ㄦ埛鐧诲綍缁熻淇℃伅 + user.LastLogin = DateTime.UtcNow; + user.LastIp = _identityService.GetClientIp(); + user.LoginCount++; + _dbContext.Admins.Update(user); + _dbContext.RefreshTokens.Add(refreshTokenEntity); + await _dbContext.SaveChangesAsync(); + + //灏 access token 鍝堝笇鍐欏叆缂撳瓨锛堥槻姝㈤噸澶嶄娇鐢ㄦ垨鍙敤浜庡揩閫熸牎楠,鐢ㄤ簬蹇熸嫆缁濈瓑锛夛紝杩囨湡鏃堕棿涓 access token 淇濇寔涓鑷达紙鍒嗛挓锛 + var cacheKey = $"token:{HashToken(accessToken)}"; + await _cacheService.SetCacheAsync(cacheKey, user.Id, _jwtSetting.AccessTokenExpirationMinutes); + + + + + var authResponse = new AuthResponse + { + Token = accessToken, + RefreshToken = refreshToken, + TokenExpiry = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes) + }; + + //SignInAsync 鍒涘缓 HttpOnly Cookie锛屼究浜 Server-side 璁よ瘉 await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity), new AuthenticationProperties @@ -136,6 +191,17 @@ namespace Atomx.Admin.Controllers ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes) }); + // 璁剧疆Cookie锛堢敤浜嶴erver妯″紡锛 + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Expires = DateTime.UtcNow.AddDays(7), + SameSite = SameSiteMode.Strict, + Secure = Request.IsHttps + }; + + Response.Cookies.Append("accessToken", accessToken, cookieOptions); + Response.Cookies.Append("refreshToken", refreshToken, cookieOptions); return new JsonResult(new ApiResult().IsSuccess(authResponse)); } @@ -148,17 +214,119 @@ namespace Atomx.Admin.Controllers [AllowAnonymous] public async Task Refresh([FromBody] RefreshRequest request) { + var uid = _identityService.GetUserId(); + if (uid == 0) + { + return BadRequest(new ApiResult().IsFail("鏃犳晥鐨勪护鐗岃姹", null)); + } + if (request == null || string.IsNullOrWhiteSpace(request.Token) || string.IsNullOrWhiteSpace(request.RefreshToken)) { return BadRequest(new ApiResult().IsFail("鏃犳晥鐨勫埛鏂拌姹", null)); } + var user = await _dbContext.Admins.FirstOrDefaultAsync(a => a.Id == uid); + if (user == null) + throw new SecurityTokenException("鐢ㄦ埛涓嶅瓨鍦ㄦ垨宸茶绂佺敤"); + try { - var ip = _identityService.GetClientIp(); - var newTokens = await _tokenService.RefreshTokenAsync(request.Token, request.RefreshToken, ip); + // 楠岃瘉 refresh token锛堟暟鎹簱涓瓨鍌ㄤ负鍝堝笇锛 + var hashedRefreshToken = HashRefreshToken(request.RefreshToken); + var storedToken = await _dbContext.RefreshTokens + .FirstOrDefaultAsync(rt => + rt.Token == hashedRefreshToken && + rt.UserId == uid && + rt.ExpiresTime > DateTime.UtcNow && + !rt.IsRevoked); - return new JsonResult(new ApiResult().IsSuccess(newTokens)); + if (storedToken == null) + throw new SecurityTokenException("鏃犳晥鐨勫埛鏂颁护鐗"); + + // 鏍囪璇 refresh token 涓哄凡鎾ら攢锛堜竴娆℃э級 + storedToken.IsRevoked = true; + storedToken.RevokedTime = DateTime.UtcNow; + storedToken.Ip = _identityService.GetClientIp(); + + 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); + + // Claims 涓寘鍚繀瑕佽鑹/鏉冮檺 + var role = _dbContext.Roles.SingleOrDefault(p => p.Id == user.RoleId); + var claims = new List + { + 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 tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + + Expires = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes), + SigningCredentials = credentials, + Issuer = issuer, + Audience = audience + }; + + // 鐢熸垚 access token + var accessToken = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor)); + + // 鐢熸垚 refresh token锛堟槑鏂囷級 + var refreshToken = GenerateRefreshToken(); + + var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + + // 淇濆瓨 refresh token 鐨勫搱甯屽埌鏁版嵁搴擄紙涓嶅彲閫嗭級 + var refreshTokenEntity = new RefreshToken + { + Token = HashRefreshToken(refreshToken), + UserId = user.Id, + IssuedTime = DateTime.UtcNow, + ExpiresTime = DateTime.UtcNow.AddMinutes(_jwtSetting.RefreshTokenExpirationMinutes), + Ip = _identityService.GetClientIp(), + UserAgent = _identityService.GetUserAgent() + }; + + // 淇濈暀鏈鏂 N 涓湭鎾ら攢鐨勫埛鏂颁护鐗岋紝鍏朵綑鏍囪涓烘挙閿 + await RemoveOldRefreshTokensAsync(user.Id); + + _dbContext.RefreshTokens.Add(refreshTokenEntity); + await _dbContext.SaveChangesAsync(); + + //灏 access token 鍝堝笇鍐欏叆缂撳瓨锛堥槻姝㈤噸澶嶄娇鐢ㄦ垨鍙敤浜庡揩閫熸牎楠,鐢ㄤ簬蹇熸嫆缁濈瓑锛夛紝杩囨湡鏃堕棿涓 access token 淇濇寔涓鑷达紙鍒嗛挓锛 + var cacheKey = $"token:{HashToken(accessToken)}"; + await _cacheService.SetCacheAsync(cacheKey, user.Id, _jwtSetting.AccessTokenExpirationMinutes); + + var authResponse = new AuthResponse + { + Token = accessToken, + RefreshToken = refreshToken, + TokenExpiry = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes) + }; + + // 璁剧疆Cookie锛堢敤浜嶴erver妯″紡锛 + var cookieOptions = new CookieOptions + { + HttpOnly = true, + Expires = DateTime.UtcNow.AddDays(7), + SameSite = SameSiteMode.Strict, + Secure = Request.IsHttps + }; + + Response.Cookies.Append("accessToken", accessToken, cookieOptions); + Response.Cookies.Append("refreshToken", refreshToken, cookieOptions); + + + return new JsonResult(new ApiResult().IsSuccess(authResponse)); } catch (SecurityTokenException ex) { @@ -184,8 +352,21 @@ namespace Atomx.Admin.Controllers { try { - var ip = _identityService.GetClientIp(); - await _tokenService.RevokeTokenAsync(revokeRequest.RefreshToken, ip); + var hashedToken = HashRefreshToken(revokeRequest.RefreshToken); + var token = await _dbContext.RefreshTokens + .FirstOrDefaultAsync(rt => rt.Token == hashedToken); + + if (token == null || token.IsRevoked) + return new JsonResult(new ApiResult().IsSuccess("宸查鍑")); + + token.IsRevoked = true; + token.RevokedTime = DateTime.UtcNow; + token.Ip = _identityService.GetClientIp(); + + await _dbContext.SaveChangesAsync(); + + // 娓呴櫎涓庣敤鎴风浉鍏崇殑缂撳瓨锛堜緥濡 user info锛 + await _cacheService.Remove($"user:{token.UserId}"); } catch (Exception ex) { @@ -195,10 +376,64 @@ namespace Atomx.Admin.Controllers // 娓呯悊 Cookie await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - Response.Cookies.Delete("access_token"); - Response.Cookies.Delete("refresh_token"); + + Response.Cookies.Delete("accessToken"); + Response.Cookies.Delete("refreshToken"); return new JsonResult(new ApiResult().IsSuccess("宸查鍑")); } + + + /// + /// 鐢熸垚闅忔満 refresh token锛堟槑鏂囷紝鐢辨湇鍔¤繑鍥炲埌瀹㈡埛绔紝鏁版嵁搴撲粎瀛樺搱甯岋級 + /// + private string GenerateRefreshToken() + { + var randomNumber = new byte[64]; + using var rng = RandomNumberGenerator.Create(); + rng.GetBytes(randomNumber); + return Convert.ToBase64String(randomNumber); + } + + /// + /// 鍝堝笇鍒锋柊浠ょ墝锛堜笉鍙嗭級锛歋HA256( refreshToken + secret ) + /// 鏁版嵁搴撲粎淇濆瓨璇ュ硷紝瀹㈡埛绔繚瀛樻槑鏂 refreshToken + /// + private string HashRefreshToken(string refreshToken) + { + using var sha256 = SHA256.Create(); + var bytes = Encoding.UTF8.GetBytes(refreshToken + _jwtSetting.SecurityKey); + var hash = sha256.ComputeHash(bytes); + return Convert.ToBase64String(hash); + } + + /// + /// 鎾ら攢骞朵繚鐣欐渶杩 N 涓湭鎾ら攢鐨勫埛鏂颁护鐗岋紙瓒呭嚭閮ㄥ垎鏍囪涓哄凡鎾ら攢锛 + /// + private async Task RemoveOldRefreshTokensAsync(long userId) + { + var tokens = await _dbContext.RefreshTokens + .Where(rt => rt.UserId == userId && !rt.IsRevoked) + .OrderByDescending(rt => rt.IssuedTime) + .Skip(Math.Max(0, _jwtSetting.MaxRefreshTokensPerUser - 1)) + .ToListAsync(); + + foreach (var token in tokens) + { + token.IsRevoked = true; + token.RevokedTime = DateTime.UtcNow; + } + } + + /// + /// 鍝堝笇 access token锛堢敤浜庢挙閿缂撳瓨 key锛 + /// + private string HashToken(string token) + { + using var sha256 = SHA256.Create(); + var bytes = Encoding.UTF8.GetBytes(token); + var hash = sha256.ComputeHash(bytes); + return Convert.ToBase64String(hash); + } } } diff --git a/Atomx.Admin/Atomx.Admin/Extensions/AuthorizationExtension.cs b/Atomx.Admin/Atomx.Admin/Extensions/AuthorizationExtension.cs index 27240be..1daf239 100644 --- a/Atomx.Admin/Atomx.Admin/Extensions/AuthorizationExtension.cs +++ b/Atomx.Admin/Atomx.Admin/Extensions/AuthorizationExtension.cs @@ -26,51 +26,12 @@ namespace Atomx.Admin.Extensions } services.AddSingleton(jwtSetting); - // Cookie 閰嶇疆璇诲彇 - var cookieConf = Configuration.GetSection("Authentication:Cookie"); - var cookieName = cookieConf.GetValue("Name") ?? ".Atomx.Auth"; - var cookiePath = cookieConf.GetValue("Path") ?? "/"; - var cookieDomain = cookieConf.GetValue("Domain"); - var sameSiteStr = cookieConf.GetValue("SameSite"); - var securePolicyStr = cookieConf.GetValue("SecurePolicy"); - var expireMinutes = cookieConf.GetValue("ExpireMinutes") ?? 60; - - SameSiteMode sameSiteMode; - if (string.IsNullOrWhiteSpace(sameSiteStr) || !Enum.TryParse(sameSiteStr, true, out sameSiteMode)) - { - sameSiteMode = environment.IsDevelopment() ? SameSiteMode.Strict : SameSiteMode.None; - } - - CookieSecurePolicy securePolicy; - if (string.IsNullOrWhiteSpace(securePolicyStr) || !Enum.TryParse(securePolicyStr, true, out securePolicy)) - { - securePolicy = environment.IsDevelopment() ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.Always; - } - services.AddAuthentication(options => { // 榛樿鐢ㄤ簬 API 鐨勮璇佹柟妗堜负 JwtBearer options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) - .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => - { - options.Cookie.Name = cookieName; - options.Cookie.Path = cookiePath; - if (!string.IsNullOrWhiteSpace(cookieDomain)) - { - options.Cookie.Domain = cookieDomain; - } - options.Cookie.HttpOnly = true; - options.Cookie.SameSite = sameSiteMode; - options.Cookie.SecurePolicy = securePolicy; - - options.ExpireTimeSpan = TimeSpan.FromMinutes(expireMinutes); - options.SlidingExpiration = true; - - options.LoginPath = "/account/login"; - options.LogoutPath = "/api/sign/out"; - }) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.RequireHttpsMetadata = !environment.IsDevelopment(); @@ -97,7 +58,7 @@ namespace Atomx.Admin.Extensions // 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"))) + if (!string.IsNullOrEmpty(accessToken) && (path.StartsWithSegments("/hubs") || path.StartsWithSegments("/hub") || path.StartsWithSegments("/api"))) { context.Token = accessToken; } @@ -120,7 +81,7 @@ namespace Atomx.Admin.Extensions return Task.CompletedTask; } }; - }); + }).AddCookie(); // 娉ㄥ唽鍩轰簬鏉冮檺鐨勭瓥鐣 services.AddAuthorization(options => diff --git a/Atomx.Admin/Atomx.Admin/Program.cs b/Atomx.Admin/Atomx.Admin/Program.cs index 1ae1bbf..2035f9d 100644 --- a/Atomx.Admin/Atomx.Admin/Program.cs +++ b/Atomx.Admin/Atomx.Admin/Program.cs @@ -70,16 +70,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -// Token 服务与 Provider(Server 使用 ServerTokenProvider) -builder.Services.AddScoped(); -builder.Services.AddScoped(); // 注册后端 TokenService builder.Services.AddScoped(); -// 注册后端 Token 服务(生成/刷新/撤销 access & refresh token) -// - TokenService 实现 ITokenService,依赖于 DataContext、ICacheService、JwtSetting 等 -// - 在 Server 端注入,SignController 与其他服务将从 DI 获取该实现 -builder.Services.AddScoped(); - // SignalR:启用服务端 Hub 支持(注意:JWT 的 OnMessageReceived 已在 AuthorizationExtension 中处理) builder.Services.AddSignalR(); diff --git a/Atomx.Admin/Atomx.Admin/Services/ITokenService.cs b/Atomx.Admin/Atomx.Admin/Services/ITokenService.cs deleted file mode 100644 index c8d6f68..0000000 --- a/Atomx.Admin/Atomx.Admin/Services/ITokenService.cs +++ /dev/null @@ -1,20 +0,0 @@ -锘縰sing Atomx.Common.Entities; -using Atomx.Common.Models; - -namespace Atomx.Admin.Services -{ - /// - /// Token 鏈嶅姟鎺ュ彛锛圓dmin 涓撶敤锛夈 - /// - 鐢熸垚 / 鍒锋柊 / 鎾ら攢 鍒锋柊浠ょ墝 - /// - 楠岃瘉 access token - /// - 鏍规嵁 token 鑾峰彇 Admin 瀹炰綋 - /// - public interface ITokenService - { - Task GenerateTokenAsync(Atomx.Common.Entities.Admin admin, string? ipAddress = null, string? userAgent = null); - Task RefreshTokenAsync(string token, string refreshToken, string? ipAddress = null); - Task RevokeTokenAsync(string refreshToken, string? ipAddress = null); - Task ValidateTokenAsync(string token); - Task GetAdminFromTokenAsync(string token); - } -} diff --git a/Atomx.Admin/Atomx.Admin/Services/TokenService.cs b/Atomx.Admin/Atomx.Admin/Services/TokenService.cs deleted file mode 100644 index 839e170..0000000 --- a/Atomx.Admin/Atomx.Admin/Services/TokenService.cs +++ /dev/null @@ -1,400 +0,0 @@ -锘縰sing Atomx.Common.Constants; -using Atomx.Common.Entities; -using Atomx.Common.Models; -using Atomx.Data; -using Atomx.Data.CacheServices; -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; - -namespace Atomx.Admin.Services -{ - /// - /// 璐熻矗锛氱敓鎴 access token / refresh token銆佸埛鏂般佹挙閿銆侀獙璇侊紙Admin 涓撶敤锛 - /// 瑕佺偣锛 - /// - RefreshToken 鍦ㄦ暟鎹簱涓互 SHA256(token + secret) 淇濆瓨锛堜笉鍙嗭級 - /// - 鍙煡 Admin 琛紙鐢ㄦ埛绔笉鑰冭檻 User锛 - /// - 淇濈暀姣忎釜 Admin 鏈杩 N 涓湭鎾ら攢鐨勫埛鏂颁护鐗岋紙閰嶇疆椤癸級 - /// - 涓嶅湪鏃ュ織涓啓鍏ユ槑鏂 token - /// - public class TokenService : ITokenService - { - readonly DataContext _dbContext; - readonly ICacheService _cacheService; - readonly JwtSetting _jwtSetting; - private readonly ILogger _logger; - private readonly SecurityKey _securityKey; - private readonly SigningCredentials _signingCredentials; - - public TokenService( - ILogger logger, DataContext dataContext, ICacheService cacheService, JwtSetting jwtSetting) - { - _logger = logger; - _dbContext = dataContext; - _cacheService = cacheService; - _jwtSetting = jwtSetting; - - // 闃插尽鎬ч粯璁ゅ硷紙閰嶇疆缂哄け鏃讹級 - if (_jwtSetting.AccessTokenExpirationMinutes <= 0) _jwtSetting.AccessTokenExpirationMinutes = 15; - if (_jwtSetting.RefreshTokenExpirationMinutes <= 0) _jwtSetting.RefreshTokenExpirationMinutes = 60 * 24 * 30; // 30 澶 - if (_jwtSetting.MaxRefreshTokensPerUser <= 0) _jwtSetting.MaxRefreshTokensPerUser = 7; - - var key = Encoding.UTF8.GetBytes(_jwtSetting.SecurityKey); - _securityKey = new SymmetricSecurityKey(key); - _signingCredentials = new SigningCredentials(_securityKey, SecurityAlgorithms.HmacSha256); - } - - // helper to avoid analyzer complaining about direct field use in ctor defaulting - private int _jwt_setting_max() => _jwtSetting.MaxRefreshTokensPerUser; - - /// - /// 鐢熸垚涓瀵 token锛坅ccess + refresh锛夊苟灏 Refresh 鐨勫搱甯屽瓨搴撱 - /// 杩斿洖鐨 RefreshToken 涓烘槑鏂囷紙浠呯敤浜庡鎴风瀛樺偍锛夛紝鏁版嵁搴撳彧瀛 Hash銆 - /// - public async Task GenerateTokenAsync(Atomx.Common.Entities.Admin admin, string? ipAddress = null, string? userAgent = null) - { - if (admin == null) - throw new ArgumentNullException(nameof(admin)); - - // 妫鏌ユ槸鍚﹁閿佸畾锛圓dmin 鏈 LockoutEndTime锛 - if (admin.LockoutEndTime.HasValue && admin.LockoutEndTime > DateTime.UtcNow) - throw new InvalidOperationException("璐︽埛宸茶閿佸畾"); - - // 鐢熸垚 access token - var accessToken = GenerateAccessToken(admin); - - // 鐢熸垚 refresh token锛堟槑鏂囷級 - var refreshToken = GenerateRefreshToken(); - - // 淇濆瓨 refresh token 鐨勫搱甯屽埌鏁版嵁搴擄紙涓嶅彲閫嗭級 - var refreshTokenEntity = new RefreshToken - { - Token = HashRefreshToken(refreshToken), - UserId = admin.Id, // 铏界劧鍙 UserId锛屼絾鍦 Admin 鍦烘櫙涓〃绀 Admin.Id - IssuedTime = DateTime.UtcNow, - ExpiresTime = DateTime.UtcNow.AddMinutes(_jwtSetting.RefreshTokenExpirationMinutes), - Ip = ipAddress, - UserAgent = userAgent - }; - - // 淇濈暀鏈鏂 N 涓湭鎾ら攢鐨勫埛鏂颁护鐗岋紝鍏朵綑鏍囪涓烘挙閿 - await RemoveOldRefreshTokensAsync(admin.Id); - - _dbContext.RefreshTokens.Add(refreshTokenEntity); - await _dbContext.SaveChangesAsync(); - - // 缂撳瓨 access token锛堥槻姝㈤噸澶嶄娇鐢ㄦ垨鍙敤浜庡揩閫熸牎楠岋級 - await CacheTokenAsync(accessToken, admin.Id); - - return new AuthResponse - { - Token = accessToken, - RefreshToken = refreshToken, - TokenExpiry = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes) - }; - } - - /// - /// 浣跨敤宸茶繃鏈熺殑 access token锛堝彧鐢ㄤ簬璇诲彇韬唤淇℃伅锛+ 鏄庢枃 refreshToken 鏉ュ埛鏂般 - /// 涓氬姟锛 - /// - 楠岃瘉 access token 绛惧悕涓 issuer/audience锛堝厑璁歌繃鏈燂級 - /// - 鏍规嵁 Claim 涓殑 admin id 鍦 Admins 琛ㄦ煡鎵 - /// - 楠岃瘉 refresh token 鐨勫搱甯屾槸鍚﹀湪鏁版嵁搴撲笖鏈挙閿鏈繃鏈 - /// - 灏嗚 refresh token 鏍囪涓烘挙閿骞剁敓鎴愭柊鐨勫 token 杩斿洖 - /// - public async Task RefreshTokenAsync(string token, string refreshToken, string? ipAddress = null) - { - var principal = GetPrincipalFromExpiredToken(token); - var idClaim = principal.FindFirst(ClaimKeys.Id)?.Value; - - if (!long.TryParse(idClaim, out var adminId) || adminId == 0) - throw new SecurityTokenException("鏃犳晥鐨勪护鐗"); - - var admin = await _dbContext.Admins.FirstOrDefaultAsync(a => a.Id == adminId); - if (admin == null) - throw new SecurityTokenException("鐢ㄦ埛涓嶅瓨鍦ㄦ垨宸茶绂佺敤"); - - // 楠岃瘉 refresh token锛堟暟鎹簱涓瓨鍌ㄤ负鍝堝笇锛 - var hashedRefreshToken = HashRefreshToken(refreshToken); - var storedToken = await _dbContext.RefreshTokens - .FirstOrDefaultAsync(rt => - rt.Token == hashedRefreshToken && - rt.UserId == adminId && - rt.ExpiresTime > DateTime.UtcNow && - !rt.IsRevoked); - - if (storedToken == null) - throw new SecurityTokenException("鏃犳晥鐨勫埛鏂颁护鐗"); - - // 鏍囪璇 refresh token 涓哄凡鎾ら攢锛堜竴娆℃э級 - storedToken.IsRevoked = true; - storedToken.RevokedTime = DateTime.UtcNow; - storedToken.Ip = ipAddress; - - // 鐢熸垚鏂扮殑 access/refresh 瀵 - var newTokens = await GenerateTokenAsync(admin, ipAddress, storedToken.UserAgent); - - // SaveChanges 宸插湪 GenerateTokenAsync 璋冪敤涓墽琛岋紙浣嗘垜浠慨鏀逛簡 storedToken锛岄渶瑕佺‘淇濅繚瀛橈級 - await _dbContext.SaveChangesAsync(); - - return newTokens; - } - - /// - /// 鎾ら攢鏌愪釜鏄庢枃 refresh token锛堢敤浜庣櫥鍑猴級 - /// - public async Task RevokeTokenAsync(string refreshToken, string? ipAddress = null) - { - var hashedToken = HashRefreshToken(refreshToken); - var token = await _dbContext.RefreshTokens - .FirstOrDefaultAsync(rt => rt.Token == hashedToken); - - if (token == null || token.IsRevoked) - return false; - - token.IsRevoked = true; - token.RevokedTime = DateTime.UtcNow; - token.Ip = ipAddress; - - await _dbContext.SaveChangesAsync(); - - // 娓呴櫎涓庣敤鎴风浉鍏崇殑缂撳瓨锛堜緥濡 user info锛 - await _cacheService.Remove($"user:{token.UserId}"); - - return true; - } - - /// - /// 楠岃瘉 access token锛堝畬鏁撮獙璇侊細绛惧悕銆乮ssuer銆乤udience銆佽繃鏈燂級 - /// 棰濆锛氭鏌 token 鏄惁鍦ㄦ挙閿缂撳瓨涓 - /// - public async Task ValidateTokenAsync(string token) - { - try - { - // 妫鏌ヤ护鐗屾槸鍚﹀湪缂撳瓨涓紙宸茶鎾ら攢锛 - var cacheKey = $"revoked_token:{HashToken(token)}"; - var cached = await _cacheService.GetCacheString(cacheKey); - if (cached != null) - return false; - - var tokenHandler = new JwtSecurityTokenHandler(); - var validationParameters = new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = _securityKey, - ValidateIssuer = true, - ValidIssuer = _jwtSetting.Issuer, - ValidateAudience = true, - ValidAudience = _jwtSetting.Audience, - ValidateLifetime = true, - ClockSkew = TimeSpan.Zero - }; - - tokenHandler.ValidateToken(token, validationParameters, out _); - return true; - } - catch - { - return false; - } - } - - /// - /// 鏍规嵁 access token 鑾峰彇 Admin锛堝鏋 token 鍚堟硶锛夈 - /// - 浼樺厛浠庣紦瀛樿鍙 Admin 瀵硅薄 - /// - 濡傛灉缂撳瓨涓嶅瓨鍦ㄥ垯浠庢暟鎹簱璇诲彇骞剁紦瀛橈紙鐭湡锛 - /// - public async Task GetAdminFromTokenAsync(string token) - { - try - { - var principal = GetPrincipalFromToken(token); - var idClaim = principal.FindFirst(ClaimKeys.Id)?.Value; - - if (!long.TryParse(idClaim, out var adminId) || adminId == 0) - return null; - - // 灏濊瘯浠庣紦瀛樿幏鍙 - var cacheKey = $"user:{adminId}"; - var cachedUser = await _cacheService.GetCacheString(cacheKey); - - if (!string.IsNullOrEmpty(cachedUser)) - { - return JsonSerializer.Deserialize(cachedUser); - } - - // 浠庢暟鎹簱鑾峰彇 Admin - var admin = await _dbContext.Admins.FirstOrDefaultAsync(a => a.Id == adminId); - if (admin != null) - { - // 缂撳瓨 admin 淇℃伅锛堝崟浣嶏細鍒嗛挓锛岀煭鏈熺紦瀛橈級 - await _cacheService.SetCacheAsync(cacheKey, JsonSerializer.Serialize(admin), 5); - } - - return admin; - } - catch - { - return null; - } - } - - /// - /// 鐢熸垚璁块棶浠ょ墝锛屽寘鍚繀瑕 claims銆 - /// 浣跨敤椤圭洰甯搁噺 ClaimKeys 浠ヤ繚璇佸墠鍚庣涓鑷淬 - /// - private string GenerateAccessToken(Atomx.Common.Entities.Admin admin) - { - var claims = new List - { - new Claim(ClaimKeys.Id, admin.Id.ToString()), - new Claim("jti", Guid.NewGuid().ToString()), - new Claim(ClaimKeys.Name, admin.Username ?? string.Empty), - new Claim(ClaimKeys.Email, admin.Email ?? string.Empty), - new Claim("iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64) - }; - - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(claims), - Expires = DateTime.UtcNow.AddMinutes(_jwtSetting.AccessTokenExpirationMinutes), - Issuer = _jwtSetting.Issuer, - Audience = _jwtSetting.Audience, - SigningCredentials = _signingCredentials, - NotBefore = DateTime.UtcNow - }; - - var tokenHandler = new JwtSecurityTokenHandler(); - var token = tokenHandler.CreateToken(tokenDescriptor); - return tokenHandler.WriteToken(token); - } - - /// - /// 鐢熸垚闅忔満 refresh token锛堟槑鏂囷紝鐢辨湇鍔¤繑鍥炲埌瀹㈡埛绔紝鏁版嵁搴撲粎瀛樺搱甯岋級 - /// - private string GenerateRefreshToken() - { - var randomNumber = new byte[64]; - using var rng = RandomNumberGenerator.Create(); - rng.GetBytes(randomNumber); - return Convert.ToBase64String(randomNumber); - } - - /// - /// 鏍规嵁 access token 楠岃瘉骞惰繑鍥 ClaimsPrincipal锛堣姹 token 鏈繃鏈燂級 - /// - private ClaimsPrincipal GetPrincipalFromToken(string token) - { - var tokenHandler = new JwtSecurityTokenHandler(); - - try - { - var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = _securityKey, - ValidateIssuer = true, - ValidIssuer = _jwtSetting.Issuer, - ValidateAudience = true, - ValidAudience = _jwtSetting.Audience, - ValidateLifetime = true, - ClockSkew = TimeSpan.Zero - }, out _); - - return principal; - } - catch (Exception ex) - { - _logger.LogError(ex, "浠ょ墝楠岃瘉澶辫触"); - throw new SecurityTokenException("鏃犳晥鐨勪护鐗", ex); - } - } - - /// - /// 浠庡凡杩囨湡鐨 access token 涓鍙 ClaimsPrincipal锛堜笉楠岃瘉 lifetime锛岀敤浜 refresh 鎿嶄綔锛 - /// - private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) - { - var tokenHandler = new JwtSecurityTokenHandler(); - - try - { - var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters - { - ValidateIssuerSigningKey = true, - IssuerSigningKey = _securityKey, - ValidateIssuer = true, - ValidIssuer = _jwtSetting.Issuer, - ValidateAudience = true, - ValidAudience = _jwtSetting.Audience, - ValidateLifetime = false, // 涓嶉獙璇佽繃鏈熶互渚 refresh - ClockSkew = TimeSpan.Zero - }, out _); - - return principal; - } - catch (Exception ex) - { - _logger.LogError(ex, "杩囨湡浠ょ墝楠岃瘉澶辫触"); - throw new SecurityTokenException("鏃犳晥鐨勪护鐗", ex); - } - } - - /// - /// 鍝堝笇鍒锋柊浠ょ墝锛堜笉鍙嗭級锛歋HA256( refreshToken + secret ) - /// 鏁版嵁搴撲粎淇濆瓨璇ュ硷紝瀹㈡埛绔繚瀛樻槑鏂 refreshToken - /// - private string HashRefreshToken(string refreshToken) - { - using var sha256 = SHA256.Create(); - var bytes = Encoding.UTF8.GetBytes(refreshToken + _jwtSetting.SecurityKey); - var hash = sha256.ComputeHash(bytes); - return Convert.ToBase64String(hash); - } - - /// - /// 鍝堝笇 access token锛堢敤浜庢挙閿缂撳瓨 key锛 - /// - private string HashToken(string token) - { - using var sha256 = SHA256.Create(); - var bytes = Encoding.UTF8.GetBytes(token); - var hash = sha256.ComputeHash(bytes); - return Convert.ToBase64String(hash); - } - - /// - /// 鎾ら攢骞朵繚鐣欐渶杩 N 涓湭鎾ら攢鐨勫埛鏂颁护鐗岋紙瓒呭嚭閮ㄥ垎鏍囪涓哄凡鎾ら攢锛 - /// - private async Task RemoveOldRefreshTokensAsync(long userId) - { - var tokens = await _dbContext.RefreshTokens - .Where(rt => rt.UserId == userId && !rt.IsRevoked) - .OrderByDescending(rt => rt.IssuedTime) - .Skip(Math.Max(0, _jwtSetting.MaxRefreshTokensPerUser - 1)) - .ToListAsync(); - - foreach (var token in tokens) - { - token.IsRevoked = true; - token.RevokedTime = DateTime.UtcNow; - } - - // 娉ㄦ剰锛氳皟鐢ㄦ柟闇瑕佸湪閫傚綋浣嶇疆 SaveChangesAsync锛圙enerateTokenAsync 宸茬粡鍦ㄦ坊鍔犳柊 token 鍚庝繚瀛橈級 - } - - /// - /// 灏 access token 鍝堝笇鍐欏叆缂撳瓨锛堢敤浜庡揩閫熸嫆缁濈瓑锛夛紝杩囨湡鏃堕棿涓 access token 淇濇寔涓鑷达紙鍒嗛挓锛 - /// - private async Task CacheTokenAsync(string token, long userId) - { - var cacheKey = $"token:{HashToken(token)}"; - await _cacheService.SetCacheAsync(cacheKey, userId, _jwtSetting.AccessTokenExpirationMinutes); - } - } -} diff --git a/Atomx.Admin/Atomx.Admin/Utils/ServerTokenProvider.cs b/Atomx.Admin/Atomx.Admin/Utils/ServerTokenProvider.cs deleted file mode 100644 index d7cb686..0000000 --- a/Atomx.Admin/Atomx.Admin/Utils/ServerTokenProvider.cs +++ /dev/null @@ -1,108 +0,0 @@ -锘縰sing Atomx.Admin.Client.Services; -using Microsoft.AspNetCore.Authentication; -using System.IdentityModel.Tokens.Jwt; - -namespace Atomx.Admin.Utils -{ - /// - /// Server 妯″紡涓嬬殑 ITokenProvider 瀹炵幇锛圔lazor Server锛 - /// - 浠庡綋鍓 HttpContext 涓皾璇曡鍙 access token锛堟寜浼樺厛绾э級 - /// 1. Authorization header ("Bearer ...") - /// 2. Query string "access_token"锛圫ignalR/WebSocket 浣跨敤锛 - /// 3. HttpContext.GetTokenAsync("access_token")锛堜繚瀛 token 鐨 auth 涓棿浠讹級 - /// 4. Cookie "access_token" - /// 5. HttpContext.Items["access_token"] - /// - 鎻愪緵蹇熺殑 JWT 杩囨湡鍒ゆ柇锛圛sTokenValidAsync锛 - /// - public class ServerTokenProvider : ITokenProvider - { - private readonly IHttpContextAccessor _httpContextAccessor; - - public ServerTokenProvider(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } - - public async Task GetTokenAsync() - { - var ctx = _httpContextAccessor.HttpContext; - if (ctx == null) - return null; - - // 1) Authorization header - if (ctx.Request.Headers.TryGetValue("Authorization", out var authHeaderValues)) - { - var authHeader = authHeaderValues.ToString(); - if (!string.IsNullOrWhiteSpace(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - { - var token = authHeader.Substring("Bearer ".Length).Trim(); - if (!string.IsNullOrEmpty(token)) - return token; - } - } - - // 2) SignalR / websocket: query string access_token - if (ctx.Request.Query.TryGetValue("access_token", out var queryToken)) - { - var token = queryToken.ToString(); - if (!string.IsNullOrEmpty(token)) - return token; - } - - // 3) 浠庤璇佺郴缁熶腑璇诲彇锛堜緥濡 UseAuthentication + SaveToken = true 鐨勫満鏅級 - try - { - var saved = await ctx.GetTokenAsync("access_token"); - if (!string.IsNullOrEmpty(saved)) - return saved; - } - catch - { - // 瀹夊叏蹇界暐 - } - - // 4) Cookie锛堝吋瀹规э級 - if (ctx.Request.Cookies.TryGetValue("access_token", out var cookieToken) && !string.IsNullOrEmpty(cookieToken)) - { - return cookieToken; - } - - // 5) Items锛堜腑闂翠欢涓存椂娉ㄥ叆锛 - if (ctx.Items.TryGetValue("access_token", out var itemToken) && itemToken is string sToken && !string.IsNullOrEmpty(sToken)) - { - return sToken; - } - - return null; - } - - public async Task IsTokenValidAsync() - { - var token = await GetTokenAsync(); - if (string.IsNullOrEmpty(token)) - return false; - - try - { - var handler = new JwtSecurityTokenHandler(); - if (handler.CanReadToken(token)) - { - var jwt = handler.ReadJwtToken(token); - var expClaim = jwt.Claims.FirstOrDefault(c => c.Type == "exp")?.Value; - if (long.TryParse(expClaim, out var expSec)) - { - var exp = DateTimeOffset.FromUnixTimeSeconds(expSec).UtcDateTime; - return exp > DateTime.UtcNow; - } - // 娌℃湁 exp claim锛屾棤娉曞垽鏂繃鏈 -> 瑙嗕负涓嶅彲鐢 - return false; - } - } - catch - { - // 瑙f瀽澶辫触 -> 瑙嗕负涓嶅彲鐢 - } - return false; - } - } -}