using Cysharp.Threading.Tasks; using ShrinkNetwork; using ShrinkNetwork.ServerHost.Framework; namespace ShrinkNetwork.ServerHost; public sealed class AuthServerModule : IShrinkServerModule { public const int LoginRequestOpcode = 1201; public const int LoginResponseOpcode = 1202; public const int RefreshRequestOpcode = 1203; public const int RefreshResponseOpcode = 1204; public const string LoginRoute = "server/auth/login"; public const string RefreshRoute = "server/auth/refresh"; private static readonly string[] DefaultGrantedPermissions = { "auth.ok", "room.access" }; public string Name => "Auth"; public void ConfigureService(ServerModuleContext context, ShrinkNetworkService service, string transportName) { service.RegisterMessage(LoginRequestOpcode, LoginRoute); service.RegisterMessage(LoginResponseOpcode, "server/auth/login_response"); service.RegisterMessage(RefreshRequestOpcode, RefreshRoute); service.RegisterMessage(RefreshResponseOpcode, "server/auth/refresh_response"); service.RegisterRequestHandler((ctx, request) => HandleLoginAsync(context, transportName, ctx, request)); service.RegisterRequestHandler((ctx, request) => HandleRefreshAsync(context, ctx, request)); } public UniTask StartAsync(ServerModuleContext context, CancellationToken cancellationToken) { return UniTask.CompletedTask; } private UniTask HandleLoginAsync( ServerModuleContext moduleContext, string transportName, ShrinkNetworkContext context, ServerAuthLoginRequest request) { var authToken = moduleContext.Options.SharedAuthToken; var requiresAuth = !string.IsNullOrWhiteSpace(authToken); if (!requiresAuth && !moduleContext.Options.AllowAnonymousWhenAuthTokenMissing) { return UniTask.FromResult(new ServerAuthLoginResponse { ErrorCode = 503, ErrorMessage = "服务端未配置 SHRINK_SERVER_AUTH_TOKEN,且未显式允许匿名登录。", IsAuthenticated = false }); } if (requiresAuth && !string.Equals(authToken, request.Token, StringComparison.Ordinal)) { return UniTask.FromResult(new ServerAuthLoginResponse { ErrorCode = ShrinkRpcErrorCode.AuthenticationFailed, ErrorMessage = "鉴权失败,口令不正确。", IsAuthenticated = false }); } var authName = string.IsNullOrWhiteSpace(request.ClientName) ? $"{transportName}-{context.Session.SessionId}" : request.ClientName.Trim(); var authState = moduleContext.AuthStore.MarkAuthenticated(context.Service, context.Session.SessionId, authName, BuildSessionTokenTtl(moduleContext.Options)); context.Session.Items["auth.name"] = authName; context.Session.Items["auth.at_utc"] = DateTimeOffset.UtcNow; context.Session.SetSessionToken(authState.SessionToken, authState.SessionTokenExpiresAtUtc); foreach (var permission in DefaultGrantedPermissions) context.Session.GrantPermission(permission); return UniTask.FromResult(new ServerAuthLoginResponse { IsAuthenticated = true, AuthName = authName, SessionToken = authState.SessionToken, SessionTokenExpiresAtUnixTimeSeconds = authState.SessionTokenExpiresAtUtc.ToUnixTimeSeconds(), RefreshRecommendedAtUnixTimeSeconds = BuildRefreshRecommendedAtUnixTimeSeconds( authState.SessionTokenExpiresAtUtc, moduleContext.Options), GrantedPermissions = DefaultGrantedPermissions.ToArray(), ServerMessage = requiresAuth ? "鉴权成功。" : "服务端已显式允许匿名登录。" }); } private UniTask HandleRefreshAsync( ServerModuleContext moduleContext, ShrinkNetworkContext context, ServerAuthRefreshRequest request) { var sessionToken = string.IsNullOrWhiteSpace(request.SessionToken) ? context.Packet.SessionToken : request.SessionToken.Trim(); if (!moduleContext.AuthStore.TryRefreshSessionToken(context.Service, context.Session.SessionId, sessionToken, BuildSessionTokenTtl(moduleContext.Options), BuildRefreshWindow(moduleContext.Options), out var authState, out var failureReason)) { return UniTask.FromResult(new ServerAuthRefreshResponse { ErrorCode = string.Equals(failureReason, "会话令牌已过期。", StringComparison.Ordinal) ? ShrinkRpcErrorCode.SessionTokenExpired : ShrinkRpcErrorCode.AuthenticationFailed, ErrorMessage = failureReason, IsAuthenticated = false }); } context.Session.SetSessionToken(authState.SessionToken, authState.SessionTokenExpiresAtUtc); context.Session.Items["auth.name"] = authState.AuthName; foreach (var permission in DefaultGrantedPermissions) context.Session.GrantPermission(permission); return UniTask.FromResult(new ServerAuthRefreshResponse { IsAuthenticated = true, AuthName = authState.AuthName, SessionToken = authState.SessionToken, SessionTokenExpiresAtUnixTimeSeconds = authState.SessionTokenExpiresAtUtc.ToUnixTimeSeconds(), RefreshRecommendedAtUnixTimeSeconds = BuildRefreshRecommendedAtUnixTimeSeconds( authState.SessionTokenExpiresAtUtc, moduleContext.Options), ServerMessage = "会话令牌已刷新。" }); } private static TimeSpan BuildSessionTokenTtl(ServerHostOptions options) { return TimeSpan.FromSeconds(Math.Max(60, options.SessionTokenTtlSeconds)); } private static TimeSpan BuildRefreshWindow(ServerHostOptions options) { return TimeSpan.FromSeconds(Math.Max(0, options.SessionTokenRefreshWindowSeconds)); } private static long BuildRefreshRecommendedAtUnixTimeSeconds(DateTimeOffset expiresAtUtc, ServerHostOptions options) { var refreshWindow = BuildRefreshWindow(options); if (refreshWindow <= TimeSpan.Zero) return 0; return expiresAtUtc.Subtract(refreshWindow).ToUnixTimeSeconds(); } } [ShrinkNetworkMessage(AuthServerModule.LoginRequestOpcode, AuthServerModule.LoginRoute)] public sealed class ServerAuthLoginRequest : IShrinkNetworkRequest { public string ClientName { get; set; } = string.Empty; public string Token { get; set; } = string.Empty; } [ShrinkNetworkMessage(AuthServerModule.LoginResponseOpcode, "server/auth/login_response")] public sealed class ServerAuthLoginResponse : ShrinkRpcResponseBase { public bool IsAuthenticated { get; set; } public string AuthName { get; set; } = string.Empty; public string SessionToken { get; set; } = string.Empty; public long SessionTokenExpiresAtUnixTimeSeconds { get; set; } public long RefreshRecommendedAtUnixTimeSeconds { get; set; } public string[] GrantedPermissions { get; set; } = Array.Empty(); public string ServerMessage { get; set; } = string.Empty; } [ShrinkNetworkMessage(AuthServerModule.RefreshRequestOpcode, AuthServerModule.RefreshRoute)] public sealed class ServerAuthRefreshRequest : IShrinkNetworkRequest { public string SessionToken { get; set; } = string.Empty; } [ShrinkNetworkMessage(AuthServerModule.RefreshResponseOpcode, "server/auth/refresh_response")] public sealed class ServerAuthRefreshResponse : ShrinkRpcResponseBase { public bool IsAuthenticated { get; set; } public string AuthName { get; set; } = string.Empty; public string SessionToken { get; set; } = string.Empty; public long SessionTokenExpiresAtUnixTimeSeconds { get; set; } public long RefreshRecommendedAtUnixTimeSeconds { get; set; } public string ServerMessage { get; set; } = string.Empty; }