153 lines
4.8 KiB
Plaintext
153 lines
4.8 KiB
Plaintext
using System.Collections.Concurrent;
|
|
using System.Security.Cryptography;
|
|
using ShrinkNetwork;
|
|
|
|
namespace ShrinkNetwork.ServerHost.Framework;
|
|
|
|
public sealed class ServerAuthStore
|
|
{
|
|
public sealed class AuthState
|
|
{
|
|
public string AuthName { get; set; } = string.Empty;
|
|
public bool IsAuthenticated { get; set; }
|
|
public DateTimeOffset AuthenticatedAtUtc { get; set; }
|
|
public DateTimeOffset LastSeenAtUtc { get; set; }
|
|
public string SessionToken { get; set; } = string.Empty;
|
|
public DateTimeOffset SessionTokenExpiresAtUtc { get; set; }
|
|
}
|
|
|
|
private readonly ConcurrentDictionary<(ShrinkNetworkService service, long sessionId), AuthState> _states = new();
|
|
|
|
public AuthState MarkAuthenticated(ShrinkNetworkService service, long sessionId, string authName, TimeSpan tokenTtl)
|
|
{
|
|
var now = DateTimeOffset.UtcNow;
|
|
var state = new AuthState
|
|
{
|
|
AuthName = authName,
|
|
IsAuthenticated = true,
|
|
AuthenticatedAtUtc = now,
|
|
LastSeenAtUtc = now,
|
|
SessionToken = CreateSessionToken(),
|
|
SessionTokenExpiresAtUtc = now.Add(tokenTtl)
|
|
};
|
|
_states[(service, sessionId)] = state;
|
|
return CloneState(state);
|
|
}
|
|
|
|
public void Remove(ShrinkNetworkService service, long sessionId)
|
|
{
|
|
_states.TryRemove((service, sessionId), out _);
|
|
}
|
|
|
|
public bool IsAuthenticated(ShrinkNetworkService service, long sessionId)
|
|
{
|
|
return _states.TryGetValue((service, sessionId), out var state) && state.IsAuthenticated;
|
|
}
|
|
|
|
public bool TryGetState(ShrinkNetworkService service, long sessionId, out AuthState state)
|
|
{
|
|
if (_states.TryGetValue((service, sessionId), out var current))
|
|
{
|
|
state = CloneState(current);
|
|
return true;
|
|
}
|
|
|
|
state = null!;
|
|
return false;
|
|
}
|
|
|
|
public bool TryValidateSessionToken(ShrinkNetworkService service, long sessionId, string? sessionToken, out AuthState state,
|
|
out string failureReason)
|
|
{
|
|
if (!_states.TryGetValue((service, sessionId), out var current) || !current.IsAuthenticated)
|
|
{
|
|
state = null!;
|
|
failureReason = "会话未认证。";
|
|
return false;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(sessionToken))
|
|
{
|
|
state = null!;
|
|
failureReason = "缺少会话令牌。";
|
|
return false;
|
|
}
|
|
|
|
if (!string.Equals(current.SessionToken, sessionToken.Trim(), StringComparison.Ordinal))
|
|
{
|
|
state = null!;
|
|
failureReason = "会话令牌不匹配。";
|
|
return false;
|
|
}
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
if (current.SessionTokenExpiresAtUtc <= now)
|
|
{
|
|
state = null!;
|
|
failureReason = "会话令牌已过期。";
|
|
return false;
|
|
}
|
|
|
|
current.LastSeenAtUtc = now;
|
|
state = CloneState(current);
|
|
failureReason = string.Empty;
|
|
return true;
|
|
}
|
|
|
|
public bool TryRefreshSessionToken(ShrinkNetworkService service, long sessionId, string? sessionToken, TimeSpan tokenTtl,
|
|
TimeSpan refreshWindow, out AuthState state, out string failureReason)
|
|
{
|
|
if (!TryValidateSessionToken(service, sessionId, sessionToken, out _, out failureReason))
|
|
{
|
|
state = null!;
|
|
return false;
|
|
}
|
|
|
|
var key = (service, sessionId);
|
|
if (!_states.TryGetValue(key, out var current))
|
|
{
|
|
state = null!;
|
|
failureReason = "认证状态不存在。";
|
|
return false;
|
|
}
|
|
|
|
var now = DateTimeOffset.UtcNow;
|
|
if (refreshWindow > TimeSpan.Zero && current.SessionTokenExpiresAtUtc - now > refreshWindow)
|
|
{
|
|
state = null!;
|
|
failureReason = "当前还不在会话令牌续期窗口内。";
|
|
return false;
|
|
}
|
|
|
|
current.SessionToken = CreateSessionToken();
|
|
current.SessionTokenExpiresAtUtc = now.Add(tokenTtl);
|
|
current.LastSeenAtUtc = now;
|
|
state = CloneState(current);
|
|
failureReason = string.Empty;
|
|
return true;
|
|
}
|
|
|
|
private static AuthState CloneState(AuthState state)
|
|
{
|
|
return new AuthState
|
|
{
|
|
AuthName = state.AuthName,
|
|
IsAuthenticated = state.IsAuthenticated,
|
|
AuthenticatedAtUtc = state.AuthenticatedAtUtc,
|
|
LastSeenAtUtc = state.LastSeenAtUtc,
|
|
SessionToken = state.SessionToken,
|
|
SessionTokenExpiresAtUtc = state.SessionTokenExpiresAtUtc
|
|
};
|
|
}
|
|
|
|
private static string CreateSessionToken()
|
|
{
|
|
var buffer = new byte[32];
|
|
RandomNumberGenerator.Fill(buffer);
|
|
return Convert.ToBase64String(buffer)
|
|
.TrimEnd('=')
|
|
.Replace('+', '-')
|
|
.Replace('/', '_');
|
|
}
|
|
}
|