perf: 登录单次快照、refresh 去 UserInfo,并失效角色菜单缓存
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b0813ea12d
commit
2b9c677fa7
@ -4,12 +4,13 @@ using Fake.Application;
|
|||||||
using Fake.Domain.Exceptions;
|
using Fake.Domain.Exceptions;
|
||||||
using Fake.Rbac.Application.Dtos.Auth;
|
using Fake.Rbac.Application.Dtos.Auth;
|
||||||
using Fake.Rbac.Application.Dtos.Menu;
|
using Fake.Rbac.Application.Dtos.Menu;
|
||||||
|
using Fake.Rbac.Application.Dtos.Role;
|
||||||
using Fake.Rbac.Application.Dtos.User;
|
using Fake.Rbac.Application.Dtos.User;
|
||||||
using Fake.Rbac.Application.Jwt;
|
using Fake.Rbac.Application.Jwt;
|
||||||
using Fake.Rbac.Application.Services;
|
|
||||||
using Fake.Rbac.Domain.Managers;
|
using Fake.Rbac.Domain.Managers;
|
||||||
using Fake.Rbac.Domain.UserAggregate;
|
using Fake.Rbac.Domain.UserAggregate;
|
||||||
using Fake.Rbac.Infrastructure;
|
using Fake.Rbac.Infrastructure;
|
||||||
|
using Fake.Security.Claims;
|
||||||
using Fake.Users;
|
using Fake.Users;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
@ -33,7 +34,6 @@ namespace SimpleAdmin.Api.Auth;
|
|||||||
[ApiExplorerSettings(GroupName = "RBAC")]
|
[ApiExplorerSettings(GroupName = "RBAC")]
|
||||||
public partial class AuthService(
|
public partial class AuthService(
|
||||||
AccountManager accountManager,
|
AccountManager accountManager,
|
||||||
UserService userService,
|
|
||||||
IJwtService jwtService,
|
IJwtService jwtService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IWebHostEnvironment webHostEnvironment,
|
IWebHostEnvironment webHostEnvironment,
|
||||||
@ -41,16 +41,15 @@ public partial class AuthService(
|
|||||||
FakeRbacDbContext rbacDbContext,
|
FakeRbacDbContext rbacDbContext,
|
||||||
ICustomerRepository customerRepository,
|
ICustomerRepository customerRepository,
|
||||||
IWeChatLoginSessionStore weChatLoginSessionStore,
|
IWeChatLoginSessionStore weChatLoginSessionStore,
|
||||||
IMemoryCache memoryCache)
|
IMemoryCache memoryCache,
|
||||||
|
IRbacAuthCache rbacAuthCache)
|
||||||
: ApplicationService
|
: ApplicationService
|
||||||
{
|
{
|
||||||
private const string AllMenusCacheKey = "rbac:menus:all:v1";
|
|
||||||
private const string UserInfoCacheKeyPrefix = "rbac:userinfo:v2:";
|
|
||||||
private static readonly TimeSpan UserInfoCacheTtl = TimeSpan.FromMinutes(3);
|
private static readonly TimeSpan UserInfoCacheTtl = TimeSpan.FromMinutes(3);
|
||||||
|
private static readonly TimeSpan AllMenusCacheTtl = TimeSpan.FromMinutes(5);
|
||||||
|
|
||||||
private Guid CurrentUserGuid => CurrentUser.FindClaimValueOrNull(ClaimTypes.NameIdentifier)!.To<Guid>();
|
private Guid CurrentUserGuid => CurrentUser.FindClaimValueOrNull(ClaimTypes.NameIdentifier)!.To<Guid>();
|
||||||
private static string UserInfoCacheKey(Guid userId) => UserInfoCacheKeyPrefix + userId;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户或客户登录。
|
/// 用户或客户登录。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -112,8 +111,7 @@ public partial class AuthService(
|
|||||||
throw new BusinessException("客户未关联用户");
|
throw new BusinessException("客户未关联用户");
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
return await BuildLoginResultAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
||||||
return await BuildLoginResultAsync(userGuid.Value, claims, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -189,8 +187,7 @@ public partial class AuthService(
|
|||||||
|
|
||||||
await weChatLoginSessionStore.RemoveAsync(loginKey, cancellationToken);
|
await weChatLoginSessionStore.RemoveAsync(loginKey, cancellationToken);
|
||||||
|
|
||||||
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
return await BuildLoginResultAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
||||||
return await BuildLoginResultAsync(userGuid.Value, claims, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Customer?> ResolveCustomerFromWxSessionAsync(
|
private async Task<Customer?> ResolveCustomerFromWxSessionAsync(
|
||||||
@ -248,8 +245,9 @@ public partial class AuthService(
|
|||||||
throw new BusinessException("用户不存在");
|
throw new BusinessException("用户不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims = await BuildAdminClaimsAsync(user.Id, cancellationToken);
|
// 刷新令牌只发新 token,不重建 UserInfo(前端忽略该字段)
|
||||||
return await BuildLoginResultAsync(user.Id, claims, cancellationToken);
|
var claims = ReuseClaimsFromRefreshToken(token.Claims, OmsLoginType.User);
|
||||||
|
return BuildTokenOnlyResult(user.Id, claims);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -386,8 +384,7 @@ public partial class AuthService(
|
|||||||
private async Task<OmsLoginResultDto> LoginAdminAsync(string account, string password, CancellationToken cancellationToken)
|
private async Task<OmsLoginResultDto> LoginAdminAsync(string account, string password, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var user = await accountManager.ValidateCredentialsAsync(account, password, cancellationToken);
|
var user = await accountManager.ValidateCredentialsAsync(account, password, cancellationToken);
|
||||||
var claims = await BuildAdminClaimsAsync(user.Id, cancellationToken);
|
return await BuildLoginResultAsync(user.Id, OmsLoginType.User, cancellationToken);
|
||||||
return await BuildLoginResultAsync(user.Id, claims, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<OmsLoginResultDto> LoginCustomerAsync(string account, string password, CancellationToken cancellationToken)
|
private async Task<OmsLoginResultDto> LoginCustomerAsync(string account, string password, CancellationToken cancellationToken)
|
||||||
@ -430,8 +427,7 @@ public partial class AuthService(
|
|||||||
throw new BusinessException("客户未关联用户");
|
throw new BusinessException("客户未关联用户");
|
||||||
}
|
}
|
||||||
|
|
||||||
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
return await BuildLoginResultAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
||||||
return await BuildLoginResultAsync(userGuid.Value, claims, cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<OmsLoginResultDto> RefreshCustomerTokenAsync(IEnumerable<Claim> claims, CancellationToken cancellationToken)
|
private async Task<OmsLoginResultDto> RefreshCustomerTokenAsync(IEnumerable<Claim> claims, CancellationToken cancellationToken)
|
||||||
@ -448,39 +444,77 @@ public partial class AuthService(
|
|||||||
throw new BusinessException("用户不存在");
|
throw new BusinessException("用户不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
var refreshedClaims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
var refreshedClaims = ReuseClaimsFromRefreshToken(claims, OmsLoginType.Customer);
|
||||||
return await BuildLoginResultAsync(userGuid.Value, refreshedClaims, cancellationToken);
|
return BuildTokenOnlyResult(userGuid.Value, refreshedClaims);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<UserInfoDto> BuildAdminUserInfoAsync(Guid userId, CancellationToken cancellationToken)
|
private async Task<UserInfoDto> BuildAdminUserInfoAsync(Guid userId, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var cacheKey = UserInfoCacheKey(userId);
|
var cacheKey = rbacAuthCache.UserInfoKey(userId);
|
||||||
if (memoryCache.TryGetValue(cacheKey, out UserInfoDto? cached) && cached != null)
|
if (memoryCache.TryGetValue(cacheKey, out UserInfoDto? cached) && cached != null)
|
||||||
{
|
{
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 同一 scoped DbContext 不能并行查询,必须串行
|
// 一次加载:用户 + 角色 + 权限,避免 Fake UserService 多次 GetWithRoles / GetRolesWithPermissions
|
||||||
var userInfo = await userService.GetAsync(userId, cancellationToken);
|
var user = await rbacDbContext.Users
|
||||||
var permissions = await userService.GetUserPermissionsAsync(userId, cancellationToken);
|
.AsNoTracking()
|
||||||
|
.Include(x => x.Roles)
|
||||||
|
.FirstOrDefaultAsync(x => x.Id == userId && !x.IsDeleted, cancellationToken);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new BusinessException("用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
var roleIds = user.Roles.Select(x => x.RoleId).Distinct().ToList();
|
||||||
|
var roles = roleIds.Count == 0
|
||||||
|
? []
|
||||||
|
: await rbacDbContext.Roles
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(x => x.Permissions)
|
||||||
|
.Where(x => roleIds.Contains(x.Id) && !x.IsDeleted)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
string? organizationName = null;
|
||||||
|
if (user.OrganizationId.HasValue)
|
||||||
|
{
|
||||||
|
organizationName = await rbacDbContext.Organizations
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.Id == user.OrganizationId.Value)
|
||||||
|
.Select(x => x.Name)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var permissions = roles
|
||||||
|
.SelectMany(x => x.Permissions.Select(p => p.PermissionCode))
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
var menus = await BuildMenusByPermissionsAsync(permissions, cancellationToken);
|
var menus = await BuildMenusByPermissionsAsync(permissions, cancellationToken);
|
||||||
|
|
||||||
var dto = new UserInfoDto
|
var dto = new UserInfoDto
|
||||||
{
|
{
|
||||||
Id = userInfo.Id,
|
Id = user.Id,
|
||||||
Name = userInfo.Name,
|
Name = user.Name,
|
||||||
Account = userInfo.Account,
|
Account = user.Account,
|
||||||
Email = userInfo.Email,
|
Email = user.Email,
|
||||||
Avatar = userInfo.Avatar,
|
Avatar = user.Avatar,
|
||||||
OrganizationId = userInfo.OrganizationId,
|
OrganizationId = user.OrganizationId,
|
||||||
OrganizationName = userInfo.OrganizationName,
|
OrganizationName = organizationName,
|
||||||
Roles = userInfo.Roles,
|
Roles = roles
|
||||||
|
.Select(x => new RoleSimpleDto
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
Name = x.Name,
|
||||||
|
Code = x.Code
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
Permissions = permissions,
|
Permissions = permissions,
|
||||||
Menus = menus,
|
Menus = menus,
|
||||||
CreatedAt = userInfo.CreatedAt,
|
CreatedAt = user.CreateTime,
|
||||||
CreatedBy = userInfo.CreatedBy,
|
CreatedBy = user.CreateUserId,
|
||||||
UpdatedAt = userInfo.UpdatedAt,
|
UpdatedAt = user.UpdateTime == default ? null : user.UpdateTime,
|
||||||
UpdatedBy = userInfo.UpdatedBy
|
UpdatedBy = user.UpdateUserId
|
||||||
};
|
};
|
||||||
|
|
||||||
memoryCache.Set(
|
memoryCache.Set(
|
||||||
@ -495,7 +529,7 @@ public partial class AuthService(
|
|||||||
|
|
||||||
private void InvalidateUserInfoCache(Guid userId)
|
private void InvalidateUserInfoCache(Guid userId)
|
||||||
{
|
{
|
||||||
memoryCache.Remove(UserInfoCacheKey(userId));
|
rbacAuthCache.InvalidateUser(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ChangeCustomerPasswordAsync(string oldPassword, string newPassword, CancellationToken cancellationToken)
|
private async Task ChangeCustomerPasswordAsync(string oldPassword, string newPassword, CancellationToken cancellationToken)
|
||||||
@ -512,22 +546,25 @@ public partial class AuthService(
|
|||||||
await customerRepository.UpdateAsync(context.Customer, cancellationToken);
|
await customerRepository.UpdateAsync(context.Customer, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<Claim>> BuildAdminClaimsAsync(Guid userId, CancellationToken cancellationToken)
|
/// <summary>
|
||||||
{
|
/// 登录快照:一次构建 UserInfo,并从中派生 JWT claims,避免再走 Fake GetUserRoles/GetAsync。
|
||||||
return await BuildClaimsAsync(userId, OmsLoginType.User, cancellationToken);
|
/// </summary>
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<List<Claim>> BuildClaimsAsync(Guid userId, OmsLoginType loginType, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var claims = await jwtService.GenerateClaimsByUserIdAsync(userId, cancellationToken);
|
|
||||||
ReplaceOrAppendClaim(claims, SimpleOmsClaimTypes.LoginType, ((int)loginType).ToString());
|
|
||||||
return claims;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<OmsLoginResultDto> BuildLoginResultAsync(
|
private async Task<OmsLoginResultDto> BuildLoginResultAsync(
|
||||||
Guid userId,
|
Guid userId,
|
||||||
IReadOnlyCollection<Claim> claims,
|
OmsLoginType loginType,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var userInfo = await BuildAdminUserInfoAsync(userId, cancellationToken);
|
||||||
|
var claims = await BuildClaimsFromUserInfoAsync(userId, userInfo, loginType, cancellationToken);
|
||||||
|
return BuildTokenResult(userId, claims, userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OmsLoginResultDto BuildTokenOnlyResult(Guid userId, List<Claim> claims)
|
||||||
|
{
|
||||||
|
return BuildTokenResult(userId, claims, userInfo: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OmsLoginResultDto BuildTokenResult(Guid userId, IReadOnlyCollection<Claim> claims, UserInfoDto? userInfo)
|
||||||
{
|
{
|
||||||
var accessClaims = CloneClaims(claims);
|
var accessClaims = CloneClaims(claims);
|
||||||
var refreshClaims = CloneClaims(claims);
|
var refreshClaims = CloneClaims(claims);
|
||||||
@ -539,10 +576,59 @@ public partial class AuthService(
|
|||||||
TokenType = "Bearer",
|
TokenType = "Bearer",
|
||||||
ExpiresIn = jwtService.GetExpiresInSeconds(),
|
ExpiresIn = jwtService.GetExpiresInSeconds(),
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
UserInfo = await BuildAdminUserInfoAsync(userId, cancellationToken)
|
UserInfo = userInfo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<List<Claim>> BuildClaimsFromUserInfoAsync(
|
||||||
|
Guid userId,
|
||||||
|
UserInfoDto userInfo,
|
||||||
|
OmsLoginType loginType,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(FakeClaimTypes.UserName, userInfo.Name ?? string.Empty)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var roleCode in (userInfo.Roles ?? [])
|
||||||
|
.Select(x => x.Code)
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x)))
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(FakeClaimTypes.Role, roleCode!));
|
||||||
|
}
|
||||||
|
|
||||||
|
var mapId = await GetMapIdByUserGuidAsync(userId, cancellationToken);
|
||||||
|
if (mapId.HasValue)
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(FakeClaimTypes.UserId, mapId.Value.ToString()));
|
||||||
|
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(FakeClaimTypes.UserId, userId.ToString()));
|
||||||
|
claims.Add(new Claim(ClaimTypes.NameIdentifier, userId.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ReplaceOrAppendClaim(claims, SimpleOmsClaimTypes.LoginType, ((int)loginType).ToString());
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Claim> ReuseClaimsFromRefreshToken(IEnumerable<Claim> source, OmsLoginType loginType)
|
||||||
|
{
|
||||||
|
var skip = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"jti", "iat", "nbf", "exp", "iss", "aud", "token_type"
|
||||||
|
};
|
||||||
|
|
||||||
|
var claims = source
|
||||||
|
.Where(x => !skip.Contains(x.Type))
|
||||||
|
.Select(x => new Claim(x.Type, x.Value, x.ValueType, x.Issuer, x.OriginalIssuer))
|
||||||
|
.ToList();
|
||||||
|
ReplaceOrAppendClaim(claims, SimpleOmsClaimTypes.LoginType, ((int)loginType).ToString());
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<Claim> CloneClaims(IEnumerable<Claim> claims)
|
private static List<Claim> CloneClaims(IEnumerable<Claim> claims)
|
||||||
{
|
{
|
||||||
return claims.Select(x => new Claim(x.Type, x.Value, x.ValueType, x.Issuer, x.OriginalIssuer)).ToList();
|
return claims.Select(x => new Claim(x.Type, x.Value, x.ValueType, x.Issuer, x.OriginalIssuer)).ToList();
|
||||||
@ -694,7 +780,8 @@ public partial class AuthService(
|
|||||||
private async Task<List<Fake.Rbac.Domain.MenuAggregate.Menu>> GetAllActiveMenusCachedAsync(
|
private async Task<List<Fake.Rbac.Domain.MenuAggregate.Menu>> GetAllActiveMenusCachedAsync(
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (memoryCache.TryGetValue(AllMenusCacheKey, out List<Fake.Rbac.Domain.MenuAggregate.Menu>? cached)
|
var cacheKey = rbacAuthCache.AllMenusKey();
|
||||||
|
if (memoryCache.TryGetValue(cacheKey, out List<Fake.Rbac.Domain.MenuAggregate.Menu>? cached)
|
||||||
&& cached is { Count: > 0 })
|
&& cached is { Count: > 0 })
|
||||||
{
|
{
|
||||||
return cached;
|
return cached;
|
||||||
@ -706,7 +793,7 @@ public partial class AuthService(
|
|||||||
.OrderBy(x => x.Order)
|
.OrderBy(x => x.Order)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
memoryCache.Set(AllMenusCacheKey, allMenus, TimeSpan.FromMinutes(5));
|
memoryCache.Set(cacheKey, allMenus, AllMenusCacheTtl);
|
||||||
return allMenus;
|
return allMenus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -751,6 +838,13 @@ public partial class AuthService(
|
|||||||
.FirstOrDefaultAsync(cancellationToken);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<int?> GetMapIdByUserGuidAsync(Guid userId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await rbacDbContext.Database.SqlQuery<int?>(
|
||||||
|
$"select `userid` as `Value` from `user` where `Id` = {userId}")
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
private static int ParseIntClaim(IEnumerable<Claim> claims, string claimType)
|
private static int ParseIntClaim(IEnumerable<Claim> claims, string claimType)
|
||||||
{
|
{
|
||||||
return int.TryParse(claims.FirstOrDefault(x => x.Type == claimType)?.Value, out var value) ? value : 0;
|
return int.TryParse(claims.FirstOrDefault(x => x.Type == claimType)?.Value, out var value) ? value : 0;
|
||||||
|
|||||||
18
src/SimpleAdmin.Api/Auth/IRbacAuthCache.cs
Normal file
18
src/SimpleAdmin.Api/Auth/IRbacAuthCache.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
namespace SimpleAdmin.Api.Auth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RBAC 登录用户信息 / 菜单缓存。通过 epoch 批量失效,避免角色权限变更后脏读。
|
||||||
|
/// </summary>
|
||||||
|
public interface IRbacAuthCache
|
||||||
|
{
|
||||||
|
string UserInfoKey(Guid userId);
|
||||||
|
|
||||||
|
string AllMenusKey();
|
||||||
|
|
||||||
|
void InvalidateUser(Guid userId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色/菜单等全局变更时 bump epoch,使全部 userInfo 与全量菜单缓存失效。
|
||||||
|
/// </summary>
|
||||||
|
void InvalidateAll();
|
||||||
|
}
|
||||||
35
src/SimpleAdmin.Api/Auth/RbacAuthCache.cs
Normal file
35
src/SimpleAdmin.Api/Auth/RbacAuthCache.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Api.Auth;
|
||||||
|
|
||||||
|
public sealed class RbacAuthCache(IMemoryCache memoryCache) : IRbacAuthCache
|
||||||
|
{
|
||||||
|
private const string EpochKey = "rbac:auth:epoch";
|
||||||
|
private const string UserInfoKeyPrefix = "rbac:userinfo:v3:";
|
||||||
|
private const string AllMenusKeyPrefix = "rbac:menus:all:v3:";
|
||||||
|
|
||||||
|
private string GetEpoch()
|
||||||
|
{
|
||||||
|
return memoryCache.GetOrCreate(
|
||||||
|
EpochKey,
|
||||||
|
entry =>
|
||||||
|
{
|
||||||
|
entry.Priority = CacheItemPriority.NeverRemove;
|
||||||
|
return "0";
|
||||||
|
})!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string UserInfoKey(Guid userId) => UserInfoKeyPrefix + GetEpoch() + ":" + userId;
|
||||||
|
|
||||||
|
public string AllMenusKey() => AllMenusKeyPrefix + GetEpoch();
|
||||||
|
|
||||||
|
public void InvalidateUser(Guid userId)
|
||||||
|
{
|
||||||
|
memoryCache.Remove(UserInfoKey(userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvalidateAll()
|
||||||
|
{
|
||||||
|
memoryCache.Set(EpochKey, Guid.NewGuid().ToString("N"));
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/SimpleAdmin.Api/Auth/RbacCacheInvalidationMiddleware.cs
Normal file
31
src/SimpleAdmin.Api/Auth/RbacCacheInvalidationMiddleware.cs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
namespace SimpleAdmin.Api.Auth;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色/菜单/分配角色等写操作成功后,失效 auth 相关内存缓存。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RbacCacheInvalidationMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
public async Task InvokeAsync(HttpContext context, IRbacAuthCache authCache)
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
|
||||||
|
if (context.Response.StatusCode is < 200 or >= 300)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var method = context.Request.Method;
|
||||||
|
if (method is not ("POST" or "PUT" or "DELETE"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = context.Request.Path.Value ?? string.Empty;
|
||||||
|
if (path.Equals("/rbac/user/assign-roles", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
path.StartsWith("/rbac/role/", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
path.StartsWith("/rbac/menu/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
authCache.InvalidateAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -61,7 +61,8 @@ public class SimpleAdminApiModule : FakeModule
|
|||||||
});
|
});
|
||||||
|
|
||||||
services.AddOmsWeChat(configuration);
|
services.AddOmsWeChat(configuration);
|
||||||
|
services.AddSingleton<IRbacAuthCache, RbacAuthCache>();
|
||||||
|
|
||||||
//Json格式化全局处理
|
//Json格式化全局处理
|
||||||
context.Services.AddFakeNewtonsoft();
|
context.Services.AddFakeNewtonsoft();
|
||||||
|
|
||||||
@ -121,6 +122,9 @@ public class SimpleAdminApiModule : FakeModule
|
|||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// 角色/菜单写操作后失效登录用户信息缓存
|
||||||
|
app.UseMiddleware<RbacCacheInvalidationMiddleware>();
|
||||||
|
|
||||||
// 端点映射
|
// 端点映射
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user