♻️ Mix things up

This commit is contained in:
2025-07-13 01:55:35 +08:00
parent 4a7f2e18b3
commit e66abe2e0c
19 changed files with 546 additions and 78 deletions

View File

@@ -0,0 +1,22 @@
namespace DysonNetwork.Shared.Auth;
public static class AuthConstants
{
public const string SchemeName = "DysonToken";
public const string TokenQueryParamName = "tk";
public const string CookieTokenName = "AuthToken";
}
public enum TokenType
{
AuthKey,
ApiKey,
OidcKey,
Unknown
}
public class TokenInfo
{
public string Token { get; set; } = string.Empty;
public TokenType Type { get; set; } = TokenType.Unknown;
}

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="dotnet-etcd" Version="8.0.1" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.17.0"/>
<PackageReference Include="Google.Protobuf" Version="3.31.1" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.31.1" />
@@ -17,6 +18,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MaxMind.GeoIP2" Version="5.3.0"/>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.7"/>
<PackageReference Include="NetTopologySuite" Version="2.6.0"/>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3"/>
@@ -24,6 +27,7 @@
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0"/>
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2"/>
<PackageReference Include="StackExchange.Redis" Version="2.8.41"/>
<PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,107 @@
using Grpc.Core;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using DysonNetwork.Shared.Proto;
using System.Threading.Tasks;
using DysonNetwork.Shared.Auth;
namespace DysonNetwork.Shared.Middleware;
public class AuthMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<AuthMiddleware> _logger;
public AuthMiddleware(RequestDelegate next, ILogger<AuthMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context, AuthService.AuthServiceClient authServiceClient)
{
var tokenInfo = _ExtractToken(context.Request);
if (tokenInfo == null || string.IsNullOrEmpty(tokenInfo.Token))
{
await _next(context);
return;
}
try
{
var authSession = await authServiceClient.AuthenticateAsync(new AuthenticateRequest { Token = tokenInfo.Token });
context.Items["AuthSession"] = authSession;
context.Items["CurrentTokenType"] = tokenInfo.Type.ToString();
// Assuming AuthSession contains Account information or can be retrieved
// context.Items["CurrentUser"] = authSession.Account; // You might need to fetch Account separately if not embedded
}
catch (RpcException ex)
{
_logger.LogWarning(ex, "Authentication failed for token: {Token}", tokenInfo.Token);
// Optionally, you can return an unauthorized response here
// context.Response.StatusCode = StatusCodes.Status401Unauthorized;
// return;
}
await _next(context);
}
private TokenInfo? _ExtractToken(HttpRequest request)
{
// Check for token in query parameters
if (request.Query.TryGetValue(AuthConstants.TokenQueryParamName, out var queryToken))
{
return new TokenInfo
{
Token = queryToken.ToString(),
Type = TokenType.AuthKey
};
}
// Check for token in Authorization header
var authHeader = request.Headers["Authorization"].ToString();
if (!string.IsNullOrEmpty(authHeader))
{
if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
var token = authHeader["Bearer ".Length..].Trim();
var parts = token.Split('.');
return new TokenInfo
{
Token = token,
Type = parts.Length == 3 ? TokenType.OidcKey : TokenType.AuthKey
};
}
else if (authHeader.StartsWith("AtField ", StringComparison.OrdinalIgnoreCase))
{
return new TokenInfo
{
Token = authHeader["AtField ".Length..].Trim(),
Type = TokenType.AuthKey
};
}
else if (authHeader.StartsWith("AkField ", StringComparison.OrdinalIgnoreCase))
{
return new TokenInfo
{
Token = authHeader["AkField ".Length..].Trim(),
Type = TokenType.ApiKey
};
}
}
// Check for token in cookies
if (request.Cookies.TryGetValue(AuthConstants.CookieTokenName, out var cookieToken))
{
return new TokenInfo
{
Token = cookieToken,
Type = cookieToken.Count(c => c == '.') == 2 ? TokenType.OidcKey : TokenType.AuthKey
};
}
return null;
}
}

View File

@@ -0,0 +1,104 @@
using Grpc.Net.Client;
using System.Security.Cryptography.X509Certificates;
using Grpc.Core;
using dotnet_etcd.interfaces;
namespace DysonNetwork.Shared.Proto;
public static class GrpcClientHelper
{
private static CallInvoker CreateCallInvoker(
string url,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(
clientCertPassword is null ?
X509Certificate2.CreateFromPemFile(clientCertPath, clientKeyPath) :
X509Certificate2.CreateFromEncryptedPemFile(clientCertPath, clientCertPassword, clientKeyPath)
);
return GrpcChannel.ForAddress(url, new GrpcChannelOptions { HttpHandler = handler }).CreateCallInvoker();
}
private static async Task<string> GetServiceUrlFromEtcd(IEtcdClient etcdClient, string serviceName)
{
var response = await etcdClient.GetAsync($"/services/{serviceName}");
if (response.Kvs.Count == 0)
{
throw new InvalidOperationException($"Service '{serviceName}' not found in Etcd.");
}
return response.Kvs[0].Value.ToStringUtf8();
}
public static AccountService.AccountServiceClient CreateAccountServiceClient(
string url,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<AccountService.AccountServiceClient> CreateAccountServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "AccountService");
return new AccountService.AccountServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static AuthService.AuthServiceClient CreateAuthServiceClient(
string url,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<AuthService.AuthServiceClient> CreateAuthServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "AuthService");
return new AuthService.AuthServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static PusherService.PusherServiceClient CreatePusherServiceClient(
string url,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
public static async Task<PusherService.PusherServiceClient> CreatePusherServiceClient(
IEtcdClient etcdClient,
string clientCertPath,
string clientKeyPath,
string? clientCertPassword = null
)
{
var url = await GetServiceUrlFromEtcd(etcdClient, "PusherService");
return new PusherService.PusherServiceClient(CreateCallInvoker(url, clientCertPath, clientKeyPath,
clientCertPassword));
}
}

View File

@@ -0,0 +1,28 @@
using System.Text;
using dotnet_etcd.interfaces;
using Etcdserverpb;
using Google.Protobuf;
namespace DysonNetwork.Shared.Registry;
public class ServiceRegistry(IEtcdClient etcd)
{
public async Task RegisterService(string serviceName, string serviceUrl, long leaseTtlSeconds = 60)
{
var key = $"/services/{serviceName}";
var leaseResponse = await etcd.LeaseGrantAsync(new LeaseGrantRequest { TTL = leaseTtlSeconds });
await etcd.PutAsync(new PutRequest
{
Key = ByteString.CopyFrom(key, Encoding.UTF8),
Value = ByteString.CopyFrom(serviceUrl, Encoding.UTF8),
Lease = leaseResponse.ID
});
await etcd.LeaseKeepAlive(leaseResponse.ID, CancellationToken.None);
}
public async Task UnregisterService(string serviceName)
{
var key = $"/services/{serviceName}";
await etcd.DeleteAsync(key);
}
}

View File

@@ -0,0 +1,23 @@
using dotnet_etcd.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DysonNetwork.Shared.Registry;
public static class EtcdStartup
{
public static IServiceCollection AddEtcdService(
this IServiceCollection services,
IConfiguration configuration
)
{
services.AddEtcdClient(options =>
{
options.ConnectionString = configuration.GetConnectionString("Etcd");
options.UseInsecureChannel = configuration.GetValue<bool>("Etcd:Insecure");
});
services.AddSingleton<ServiceRegistry>();
return services;
}
}