185 lines
8.2 KiB
Plaintext
185 lines
8.2 KiB
Plaintext
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<ServerAuthLoginRequest>(LoginRequestOpcode, LoginRoute);
|
|
service.RegisterMessage<ServerAuthLoginResponse>(LoginResponseOpcode, "server/auth/login_response");
|
|
service.RegisterMessage<ServerAuthRefreshRequest>(RefreshRequestOpcode, RefreshRoute);
|
|
service.RegisterMessage<ServerAuthRefreshResponse>(RefreshResponseOpcode, "server/auth/refresh_response");
|
|
service.RegisterRequestHandler<ServerAuthLoginRequest, ServerAuthLoginResponse>((ctx, request) =>
|
|
HandleLoginAsync(context, transportName, ctx, request));
|
|
service.RegisterRequestHandler<ServerAuthRefreshRequest, ServerAuthRefreshResponse>((ctx, request) =>
|
|
HandleRefreshAsync(context, ctx, request));
|
|
}
|
|
|
|
public UniTask StartAsync(ServerModuleContext context, CancellationToken cancellationToken)
|
|
{
|
|
return UniTask.CompletedTask;
|
|
}
|
|
|
|
private UniTask<ServerAuthLoginResponse> 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<ServerAuthRefreshResponse> 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<string>();
|
|
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;
|
|
}
|