499 lines
14 KiB
Markdown
499 lines
14 KiB
Markdown
# YARP 网关项目技术债务与关注点分析
|
||
|
||
> 分析日期:2026-02-28
|
||
> 分析范围:核心代码、配置、数据访问层
|
||
|
||
---
|
||
|
||
## 一、严重安全问题 🔴
|
||
|
||
### 1.1 硬编码凭据泄露
|
||
|
||
**文件位置:** `src/Config/RedisConfig.cs:5`
|
||
```csharp
|
||
public string ConnectionString { get; set; } = "81.68.223.70:16379,password=sl52788542";
|
||
```
|
||
|
||
**问题描述:** Redis 连接字符串包含明文密码,直接硬编码在源代码中。此代码提交到版本控制系统后,密码将永久暴露。
|
||
|
||
**影响范围:**
|
||
- 攻击者获取代码后可直接访问 Redis 服务
|
||
- 违反安全合规要求(如等保、GDPR)
|
||
|
||
**改进建议:**
|
||
```csharp
|
||
// 使用环境变量或密钥管理服务
|
||
public string ConnectionString { get; set; } =
|
||
Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING") ?? string.Empty;
|
||
```
|
||
|
||
---
|
||
|
||
### 1.2 配置文件凭据泄露
|
||
|
||
**文件位置:** `src/appsettings.json:19,28`
|
||
```json
|
||
"DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=sl52788542"
|
||
"ConnectionString": "81.68.223.70:6379"
|
||
```
|
||
|
||
**问题描述:** 数据库连接字符串和 Redis 配置包含明文凭据,且这些配置文件通常会被提交到 Git 仓库。
|
||
|
||
**改进建议:**
|
||
- 使用 `appsettings.Development.json` 存储开发环境配置,并加入 `.gitignore`
|
||
- 生产环境使用环境变量或 Azure Key Vault / AWS Secrets Manager
|
||
- 敏感配置使用 `dotnet user-secrets` 管理
|
||
|
||
---
|
||
|
||
### 1.3 JWT 令牌未验证
|
||
|
||
**文件位置:** `src/Middleware/JwtTransformMiddleware.cs:39-40`
|
||
```csharp
|
||
var jwtHandler = new JwtSecurityTokenHandler();
|
||
var jwtToken = jwtHandler.ReadJwtToken(token);
|
||
```
|
||
|
||
**问题描述:** 中间件仅**读取**JWT令牌,未进行签名验证、过期检查或颁发者验证。攻击者可伪造任意JWT令牌。
|
||
|
||
**影响范围:**
|
||
- 任何人可伪造租户ID、用户ID、角色信息
|
||
- 可冒充任意用户访问系统
|
||
|
||
**改进建议:**
|
||
```csharp
|
||
// 应使用标准的 JWT 验证流程
|
||
var validationParameters = new TokenValidationParameters
|
||
{
|
||
ValidateIssuer = true,
|
||
ValidateAudience = true,
|
||
ValidateLifetime = true,
|
||
ValidateIssuerSigningKey = true,
|
||
ValidIssuer = _jwtConfig.Authority,
|
||
ValidAudience = _jwtConfig.Audience,
|
||
IssuerSigningKey = GetSigningKey() // 从配置获取公钥
|
||
};
|
||
|
||
var principal = jwtHandler.ValidateToken(token, validationParameters, out _);
|
||
```
|
||
|
||
---
|
||
|
||
### 1.4 API 端点无认证保护
|
||
|
||
**文件位置:** `src/Controllers/GatewayConfigController.cs` 和 `src/Controllers/PendingServicesController.cs`
|
||
|
||
**问题描述:** 所有管理API端点均未添加 `[Authorize]` 特性,任何人可直接调用:
|
||
- `POST /api/gateway/tenants` - 创建租户
|
||
- `POST /api/gateway/routes` - 创建路由
|
||
- `POST /api/gateway/clusters/{clusterId}/instances` - 添加服务实例
|
||
- `POST /api/gateway/pending-services/{id}/assign` - 分配服务
|
||
|
||
**影响范围:**
|
||
- 攻击者可随意修改网关配置
|
||
- 可注入恶意服务地址进行流量劫持
|
||
|
||
**改进建议:**
|
||
```csharp
|
||
[ApiController]
|
||
[Route("api/gateway")]
|
||
[Authorize(Roles = "Admin")] // 添加认证要求
|
||
public class GatewayConfigController : ControllerBase
|
||
```
|
||
|
||
---
|
||
|
||
### 1.5 租户ID头部信任问题
|
||
|
||
**文件位置:** `src/Middleware/TenantRoutingMiddleware.cs:25`
|
||
```csharp
|
||
var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
|
||
```
|
||
|
||
**问题描述:** 直接从请求头读取租户ID,未与JWT中的租户声明进行比对验证。攻击者可伪造 `X-Tenant-Id` 头部访问其他租户数据。
|
||
|
||
**改进建议:**
|
||
```csharp
|
||
// 从已验证的 JWT claims 中获取租户ID
|
||
var jwtTenantId = context.User.FindFirst("tenant")?.Value;
|
||
var headerTenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
|
||
|
||
if (!string.IsNullOrEmpty(jwtTenantId) && jwtTenantId != headerTenantId)
|
||
{
|
||
// 记录安全事件
|
||
_logger.LogWarning("Tenant ID mismatch: JWT={JwtTenant}, Header={HeaderTenant}",
|
||
jwtTenantId, headerTenantId);
|
||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
||
return;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 二、技术债务 🟠
|
||
|
||
### 2.1 ID生成策略问题
|
||
|
||
**文件位置:** `src/Controllers/GatewayConfigController.cs:484-487`
|
||
```csharp
|
||
private long GenerateId()
|
||
{
|
||
return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||
}
|
||
```
|
||
|
||
**问题描述:** 使用时间戳毫秒生成ID,在高并发场景下可能产生重复ID。
|
||
|
||
**改进建议:**
|
||
- 使用数据库自增主键(已有配置)
|
||
- 或使用雪花算法(Snowflake ID)
|
||
- 或使用 `Guid.NewGuid()`
|
||
|
||
---
|
||
|
||
### 2.2 Redis连接重复初始化
|
||
|
||
**文件位置:**
|
||
- `src/Program.cs:39-60` - 注册 `IConnectionMultiplexer`
|
||
- `src/Services/RedisConnectionManager.cs:25-46` - 内部再次创建连接
|
||
|
||
**问题描述:** Redis连接被初始化两次,造成资源浪费和配置不一致风险。
|
||
|
||
**改进建议:**
|
||
```csharp
|
||
// Program.cs 中只注册一次
|
||
builder.Services.AddSingleton<IRedisConnectionManager, RedisConnectionManager>();
|
||
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
|
||
sp.GetRequiredService<IRedisConnectionManager>().GetConnection());
|
||
```
|
||
|
||
---
|
||
|
||
### 2.3 DTO 内嵌定义
|
||
|
||
**文件位置:** `src/Controllers/GatewayConfigController.cs:444-481`
|
||
|
||
**问题描述:** 多个DTO类定义在Controller内部,不利于复用和测试。
|
||
|
||
**改进建议:**
|
||
- 将 DTO 移至 `src/DTOs/` 或 `src/Models/Dto/` 目录
|
||
- 使用 Auto Mapper 或 Mapster 进行对象映射
|
||
|
||
---
|
||
|
||
### 2.4 魔法数字
|
||
|
||
**文件位置:** 多处使用数字常量
|
||
|
||
```csharp
|
||
// RouteCache.cs:99
|
||
.Where(r => r.Status == 1 && !r.IsDeleted)
|
||
|
||
// GatewayConfigController.cs:239
|
||
route.Status = 1;
|
||
|
||
// KubernetesPendingSyncService.cs:13
|
||
private readonly TimeSpan _syncInterval = TimeSpan.FromSeconds(30);
|
||
```
|
||
|
||
**问题描述:** 状态值、超时时间等使用硬编码数字,降低代码可读性和可维护性。
|
||
|
||
**改进建议:**
|
||
```csharp
|
||
// 定义常量或枚举
|
||
public static class RouteStatus
|
||
{
|
||
public const int Active = 1;
|
||
public const int Inactive = 0;
|
||
}
|
||
|
||
public static class ServiceConstants
|
||
{
|
||
public static readonly TimeSpan DefaultSyncInterval = TimeSpan.FromSeconds(30);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2.5 异步方法命名不一致
|
||
|
||
**文件位置:** `src/Config/DatabaseRouteConfigProvider.cs:23`
|
||
```csharp
|
||
_ = LoadConfigAsync(); // Fire-and-forget without await
|
||
```
|
||
|
||
**问题描述:** 构造函数中调用异步方法但未等待完成,可能导致初始化竞态条件。
|
||
|
||
**改进建议:**
|
||
- 使用工厂模式异步初始化
|
||
- 或在 `Program.cs` 中显式调用初始化方法
|
||
|
||
---
|
||
|
||
## 三、性能瓶颈风险 🟡
|
||
|
||
### 3.1 负载均衡锁竞争
|
||
|
||
**文件位置:** `src/LoadBalancing/DistributedWeightedRoundRobinPolicy.cs:48-53`
|
||
```csharp
|
||
var lockAcquired = db.StringSet(
|
||
lockKey,
|
||
lockValue,
|
||
TimeSpan.FromMilliseconds(500),
|
||
When.NotExists
|
||
);
|
||
```
|
||
|
||
**问题描述:** 每次请求都需要获取Redis分布式锁,高并发下会成为瓶颈。锁获取失败时降级策略不可靠。
|
||
|
||
**影响:**
|
||
- 单集群QPS受限
|
||
- Redis延迟增加时网关吞吐量下降
|
||
|
||
**改进建议:**
|
||
- 考虑使用本地缓存 + 定期同步策略
|
||
- 或使用一致性哈希算法避免锁需求
|
||
- 增加本地计数器作为快速路径
|
||
|
||
---
|
||
|
||
### 3.2 路由缓存全量加载
|
||
|
||
**文件位置:** `src/Services/RouteCache.cs:94-137`
|
||
```csharp
|
||
var routes = await db.TenantRoutes
|
||
.Where(r => r.Status == 1 && !r.IsDeleted)
|
||
.ToListAsync();
|
||
```
|
||
|
||
**问题描述:** 每次重载都清空并重新加载所有路由,大数据量下性能差。
|
||
|
||
**改进建议:**
|
||
- 实现增量更新机制
|
||
- 使用版本号比对只更新变更项
|
||
- 添加分页加载支持
|
||
|
||
---
|
||
|
||
### 3.3 数据库查询未优化
|
||
|
||
**文件位置:** `src/Controllers/GatewayConfigController.cs:145-148`
|
||
```csharp
|
||
var currentRouteVersion = await db.TenantRoutes
|
||
.OrderByDescending(r => r.Version)
|
||
.Select(r => r.Version)
|
||
.FirstOrDefaultAsync(stoppingToken);
|
||
```
|
||
|
||
**问题描述:** 每次轮询都执行 `ORDER BY` 查询获取最大版本号,缺少索引优化。
|
||
|
||
**改进建议:**
|
||
```sql
|
||
-- 添加索引
|
||
CREATE INDEX IX_TenantRoutes_Version ON "TenantRoutes" ("Version" DESC);
|
||
|
||
-- 或使用 MAX 聚合
|
||
SELECT MAX("Version") FROM "TenantRoutes";
|
||
```
|
||
|
||
---
|
||
|
||
### 3.4 PostgreSQL NOTIFY 连接管理
|
||
|
||
**文件位置:** `src/Data/GatewayDbContext.cs:72-75`
|
||
```csharp
|
||
using var connection = new NpgsqlConnection(connectionString);
|
||
connection.Open();
|
||
using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection);
|
||
cmd.ExecuteNonQuery();
|
||
```
|
||
|
||
**问题描述:** 每次保存变更都创建新的数据库连接发送通知,连接开销大。
|
||
|
||
**改进建议:**
|
||
- 使用连接池中的连接
|
||
- 或复用 `PgSqlConfigChangeListener` 中的连接发送通知
|
||
|
||
---
|
||
|
||
## 四、脆弱区域 🟠
|
||
|
||
### 4.1 租户路由外键约束
|
||
|
||
**文件位置:** `src/Migrations/20260201120312_InitialCreate.cs:83-89`
|
||
```csharp
|
||
table.ForeignKey(
|
||
name: "FK_TenantRoutes_Tenants_TenantCode",
|
||
column: x => x.TenantCode,
|
||
principalTable: "Tenants",
|
||
principalColumn: "TenantCode",
|
||
onDelete: ReferentialAction.Restrict);
|
||
```
|
||
|
||
**问题描述:** `TenantRoutes.TenantCode` 有外键约束,但全局路由(`IsGlobal=true`)时 `TenantCode` 可为空字符串,可能导致数据一致性问题。
|
||
|
||
**改进建议:**
|
||
- 全局路由使用特定的占位符(如 "GLOBAL")
|
||
- 或修改外键约束为条件约束
|
||
|
||
---
|
||
|
||
### 4.2 健康检查配置硬编码
|
||
|
||
**文件位置:** `src/Config/DatabaseClusterConfigProvider.cs:77-86`
|
||
```csharp
|
||
HealthCheck = new HealthCheckConfig
|
||
{
|
||
Active = new ActiveHealthCheckConfig
|
||
{
|
||
Enabled = true,
|
||
Interval = TimeSpan.FromSeconds(30),
|
||
Timeout = TimeSpan.FromSeconds(5),
|
||
Path = "/health"
|
||
}
|
||
}
|
||
```
|
||
|
||
**问题描述:** 健康检查路径和间隔硬编码,不同服务可能需要不同的健康检查配置。
|
||
|
||
**改进建议:**
|
||
- 将健康检查配置存储在数据库
|
||
- 或在模型中添加健康检查配置字段
|
||
|
||
---
|
||
|
||
### 4.3 端口选择逻辑
|
||
|
||
**文件位置:** `src/Controllers/PendingServicesController.cs:119-120`
|
||
```csharp
|
||
var discoveredPorts = JsonSerializer.Deserialize<List<int>>(pendingService.DiscoveredPorts) ?? new List<int>();
|
||
var primaryPort = discoveredPorts.FirstOrDefault() > 0 ? discoveredPorts.First() : 80;
|
||
```
|
||
|
||
**问题描述:** 简单选择第一个端口作为主端口,可能不适合所有服务场景。
|
||
|
||
**改进建议:**
|
||
- 支持端口选择策略配置
|
||
- 优先选择知名端口(如 80, 443, 8080)
|
||
- 允许用户在审批时选择端口
|
||
|
||
---
|
||
|
||
### 4.4 异常处理不完整
|
||
|
||
**文件位置:** `src/Services/PgSqlConfigChangeListener.cs:59-62`
|
||
```csharp
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Failed to initialize PgSql listener");
|
||
// 未重试或终止服务
|
||
}
|
||
```
|
||
|
||
**问题描述:** 初始化失败后仅记录日志,服务继续运行但功能不完整。
|
||
|
||
**改进建议:**
|
||
```csharp
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Failed to initialize PgSql listener, retrying in 5 seconds...");
|
||
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
|
||
await InitializeListenerAsync(stoppingToken); // 重试
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4.5 状态变更无事务保护
|
||
|
||
**文件位置:** `src/Controllers/PendingServicesController.cs:137-145`
|
||
```csharp
|
||
db.ServiceInstances.Add(newInstance);
|
||
pendingService.Status = (int)PendingServiceStatus.Approved;
|
||
// ...
|
||
await db.SaveChangesAsync();
|
||
```
|
||
|
||
**问题描述:** 创建实例和更新状态在同一事务中,但如果缓存重载失败,数据可能不一致。
|
||
|
||
**改进建议:**
|
||
- 使用 TransactionScope 或数据库事务明确边界
|
||
- 添加补偿机制处理失败情况
|
||
|
||
---
|
||
|
||
## 五、可维护性问题 🟡
|
||
|
||
### 5.1 日志结构不统一
|
||
|
||
**问题描述:** 日志消息格式不统一,有的包含结构化数据,有的仅是文本。
|
||
|
||
**改进建议:**
|
||
- 制定统一的日志格式规范
|
||
- 使用结构化日志模板:`LogInformation("Operation {Operation} completed for {Entity} with ID {Id}", "Create", "Route", route.Id)`
|
||
|
||
---
|
||
|
||
### 5.2 缺少单元测试
|
||
|
||
**问题描述:** 项目中未发现测试项目,核心逻辑缺少测试覆盖。
|
||
|
||
**改进建议:**
|
||
- 创建 `tests/YarpGateway.Tests/` 测试项目
|
||
- 对以下核心组件编写单元测试:
|
||
- `RouteCache` - 路由查找逻辑
|
||
- `JwtTransformMiddleware` - JWT 解析逻辑
|
||
- `DistributedWeightedRoundRobinPolicy` - 负载均衡算法
|
||
|
||
---
|
||
|
||
### 5.3 配置验证缺失
|
||
|
||
**文件位置:** `src/Config/JwtConfig.cs`, `src/Config/RedisConfig.cs`
|
||
|
||
**问题描述:** 配置类没有验证逻辑,无效配置可能导致运行时错误。
|
||
|
||
**改进建议:**
|
||
```csharp
|
||
public class JwtConfig : IValidatableObject
|
||
{
|
||
public string Authority { get; set; } = string.Empty;
|
||
|
||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(Authority))
|
||
yield return new ValidationResult("Authority is required", new[] { nameof(Authority) });
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、改进优先级建议
|
||
|
||
| 优先级 | 问题 | 风险等级 | 建议处理时间 |
|
||
|--------|------|----------|--------------|
|
||
| P0 | 硬编码凭据泄露 | 严重 | 立即修复 |
|
||
| P0 | JWT未验证 | 严重 | 立即修复 |
|
||
| P0 | API无认证保护 | 严重 | 立即修复 |
|
||
| P1 | 租户ID信任问题 | 高 | 1周内 |
|
||
| P1 | ID生成策略 | 高 | 1周内 |
|
||
| P2 | 负载均衡锁竞争 | 中 | 2周内 |
|
||
| P2 | 路由缓存优化 | 中 | 2周内 |
|
||
| P3 | DTO内嵌定义 | 低 | 1个月内 |
|
||
| P3 | 缺少单元测试 | 低 | 持续改进 |
|
||
|
||
---
|
||
|
||
## 七、总结
|
||
|
||
本项目存在多个**严重安全漏洞**,主要涉及:
|
||
1. 敏感信息硬编码
|
||
2. 认证授权缺失
|
||
3. 输入验证不足
|
||
|
||
技术债务主要集中在代码组织、异常处理和性能优化方面。建议优先处理安全相关问题,然后逐步优化性能和可维护性。
|
||
|
||
---
|
||
|
||
*文档由自动化分析生成,建议人工复核后纳入迭代计划。* |