Files
tiku-backend.net/Tiku.Infrastructure/Security/RedisAuthorizationCache.cs
xiong c497a3ca8d
Some checks failed
ci / release-gate (push) Has been cancelled
清理代码
2026-08-03 12:31:39 +08:00

263 lines
10 KiB
C#

using System.Diagnostics;
using System.Text.Json;
using Microsoft.Extensions.Options;
using StackExchange.Redis;
using Tiku.Application.Security;
using Tiku.Domain.Tenancy;
namespace Tiku.Infrastructure.Security;
internal sealed class RedisAuthorizationCache(
IConnectionMultiplexer connection,
IOptions<AuthorizationCacheOptions> options,
string environmentName) : IAccessSecurityCache, IAuthorizationSnapshotCache
{
private const string SetVersionScript = """
local current = redis.call('GET', KEYS[1])
if current then
local decoded = cjson.decode(current)
if tonumber(decoded.version) > tonumber(ARGV[1]) then return 0 end
end
redis.call('SET', KEYS[1], ARGV[2], 'PX', ARGV[3])
return 1
""";
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
private readonly AuthorizationCacheOptions options = options.Value;
private readonly string prefix = $"tiku:{Normalize(environmentName)}";
public bool IsConfigured => true;
public async Task<AccessSecurityCacheState?> GetAsync(
AccessSecurityCacheLookup lookup,
CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();
var keys = lookup.Realm == AuthRealm.Tenant
? new[]
{
SessionKey(lookup.SessionId), UserKey(lookup.UserId), VersionKey(lookup.Realm, lookup.TenantId),
TenantKey(lookup.TenantId!.Value), MembershipKey(lookup.TenantId.Value, lookup.UserId)
}
: new[]
{
SessionKey(lookup.SessionId), UserKey(lookup.UserId), VersionKey(lookup.Realm, null),
PlatformAccessKey(lookup.UserId)
};
var started = Stopwatch.GetTimestamp();
var values = await connection.GetDatabase().StringGetAsync(keys).WaitAsync(cancellationToken);
AuthorizationCacheTelemetry.RecordRedisDuration(Stopwatch.GetElapsedTime(started).TotalMilliseconds);
var state = new AccessSecurityCacheState(
Deserialize<CachedSessionSecurityState>(values[0]),
Deserialize<CachedUserSecurityState>(values[1]),
lookup.Realm == AuthRealm.Tenant ? Deserialize<CachedTenantSecurityState>(values[3]) : null,
lookup.Realm == AuthRealm.Tenant ? Deserialize<CachedMembershipSecurityState>(values[4]) : null,
lookup.Realm == AuthRealm.Platform ? Deserialize<CachedPlatformAccessState>(values[3]) : null,
Deserialize<CachedAuthorizationVersion>(values[2]));
AuthorizationCacheTelemetry.Read("redis_state", state.Complete);
return state.Complete ? state : null;
}
public async Task SetAsync(AccessSecurityCacheState state, CancellationToken cancellationToken = default)
{
var ttl = StateTtl();
var database = connection.GetDatabase();
var writes = new List<Task>();
Add(writes, database, state.Session is null ? default : SessionKey(state.Session.SessionId), state.Session,
ttl);
Add(writes, database, state.User is null ? default : UserKey(state.User.UserId), state.User, ttl);
Add(writes, database, state.Tenant is null ? default : TenantKey(state.Tenant.TenantId), state.Tenant, ttl);
Add(writes, database,
state.Membership is null ? default : MembershipKey(state.Membership.TenantId, state.Membership.UserId),
state.Membership, ttl);
Add(writes, database, state.PlatformAccess is null ? default : PlatformAccessKey(state.PlatformAccess.UserId),
state.PlatformAccess, ttl);
await Task.WhenAll(writes).WaitAsync(cancellationToken);
if (state.AuthorizationVersion is { } version)
await SetAuthorizationVersionAsync(
version.Realm, version.TenantId, version.Version, cancellationToken);
}
public Task InvalidateSessionAsync(Guid sessionId, CancellationToken cancellationToken = default)
{
return DeleteAsync(SessionKey(sessionId), cancellationToken);
}
public Task InvalidateUserAsync(Guid userId, CancellationToken cancellationToken = default)
{
return DeleteAsync(UserKey(userId), cancellationToken);
}
public Task InvalidateTenantAsync(Guid tenantId, CancellationToken cancellationToken = default)
{
return DeleteAsync(TenantKey(tenantId), cancellationToken);
}
public Task InvalidateMembershipAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken = default)
{
return DeleteAsync(MembershipKey(tenantId, userId), cancellationToken);
}
public async Task SetAuthorizationVersionAsync(
AuthRealm realm, Guid? tenantId, long version, CancellationToken cancellationToken = default)
{
var value = new CachedAuthorizationVersion(realm, tenantId, version);
var ttl = StateTtl();
await connection.GetDatabase().ScriptEvaluateAsync(
SetVersionScript,
[VersionKey(realm, tenantId)],
[version, JsonSerializer.Serialize(value, SerializerOptions), (long)ttl.TotalMilliseconds])
.WaitAsync(cancellationToken);
}
async Task<CachedAuthorizationSnapshot?> IAuthorizationSnapshotCache.GetAsync(
AuthRealm realm, Guid? tenantId, Guid userId, CancellationToken cancellationToken)
{
var value = await connection.GetDatabase().StringGetAsync(SnapshotKey(realm, tenantId, userId))
.WaitAsync(cancellationToken);
return Deserialize<CachedAuthorizationSnapshot>(value);
}
Task IAuthorizationSnapshotCache.SetAsync(
AuthRealm realm,
Guid? tenantId,
Guid userId,
CachedAuthorizationSnapshot snapshot,
CancellationToken cancellationToken)
{
return SetValueAsync(
SnapshotKey(realm, tenantId, userId),
snapshot,
TimeSpan.FromSeconds(Math.Max(1, options.DistributedSnapshotSeconds)),
cancellationToken);
}
private async Task DeleteAsync(RedisKey key, CancellationToken cancellationToken)
{
await connection.GetDatabase().KeyDeleteAsync(key).WaitAsync(cancellationToken);
}
private async Task SetValueAsync<T>(RedisKey key, T value, TimeSpan ttl, CancellationToken cancellationToken)
{
await connection.GetDatabase().StringSetAsync(key, JsonSerializer.Serialize(value, SerializerOptions), ttl)
.WaitAsync(cancellationToken);
}
private static void Add<T>(List<Task> writes, IDatabase database, RedisKey key, T? value, TimeSpan ttl)
{
if (value is not null)
writes.Add(database.StringSetAsync(key, JsonSerializer.Serialize(value, SerializerOptions), ttl));
}
private TimeSpan StateTtl()
{
var seconds = Math.Max(1, options.DistributedStateSeconds);
var jitter = Math.Clamp(options.JitterPercent, 0, 50);
return TimeSpan.FromSeconds(seconds * (1 + Random.Shared.Next(-jitter, jitter + 1) / 100d));
}
private static T? Deserialize<T>(RedisValue value)
{
return value.IsNullOrEmpty
? default
: JsonSerializer.Deserialize<T>(value.ToString(), SerializerOptions);
}
private RedisKey SessionKey(Guid id)
{
return $"{prefix}:auth:session:v1:{id:N}";
}
private RedisKey UserKey(Guid id)
{
return $"{prefix}:auth:user:v1:{id:N}";
}
private RedisKey TenantKey(Guid id)
{
return $"{prefix}:auth:tenant:v1:{id:N}";
}
private RedisKey MembershipKey(Guid tenantId, Guid userId)
{
return $"{prefix}:auth:membership:v1:{tenantId:N}:{userId:N}";
}
private RedisKey PlatformAccessKey(Guid userId)
{
return $"{prefix}:auth:platform-access:v1:{userId:N}";
}
private RedisKey VersionKey(AuthRealm realm, Guid? tenantId)
{
return
$"{prefix}:authz:version:v1:{realm.ToString().ToLowerInvariant()}:{tenantId?.ToString("N") ?? "platform"}";
}
private RedisKey SnapshotKey(AuthRealm realm, Guid? tenantId, Guid userId)
{
return
$"{prefix}:authz:snapshot:v1:{realm.ToString().ToLowerInvariant()}:{tenantId?.ToString("N") ?? "platform"}:{userId:N}";
}
private static string Normalize(string value)
{
return new string(value.Trim().ToLowerInvariant()
.Select(character => char.IsLetterOrDigit(character) || character is '-' or '_' ? character : '-')
.ToArray());
}
}
internal sealed class NullAuthorizationCache : IAccessSecurityCache, IAuthorizationSnapshotCache
{
public bool IsConfigured => false;
public Task<AccessSecurityCacheState?> GetAsync(AccessSecurityCacheLookup lookup,
CancellationToken cancellationToken = default)
{
return Task.FromResult<AccessSecurityCacheState?>(null);
}
public Task SetAsync(AccessSecurityCacheState state, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task InvalidateSessionAsync(Guid sessionId, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task InvalidateUserAsync(Guid userId, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task InvalidateTenantAsync(Guid tenantId, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task InvalidateMembershipAsync(Guid tenantId, Guid userId, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task SetAuthorizationVersionAsync(AuthRealm realm, Guid? tenantId, long version,
CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task<CachedAuthorizationSnapshot?> GetAsync(AuthRealm realm, Guid? tenantId, Guid userId,
CancellationToken cancellationToken = default)
{
return Task.FromResult<CachedAuthorizationSnapshot?>(null);
}
public Task SetAsync(AuthRealm realm, Guid? tenantId, Guid userId, CachedAuthorizationSnapshot snapshot,
CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}