diff --git a/DFApp.LotteryProxy/Dockerfile b/DFApp.LotteryProxy/Dockerfile index 807c4a66..992c5a37 100644 --- a/DFApp.LotteryProxy/Dockerfile +++ b/DFApp.LotteryProxy/Dockerfile @@ -1,10 +1,9 @@ -# 使用官方.NET 8运行时作为基础镜像 -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base +# 使用官方.NET 10运行时作为基础镜像 +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base WORKDIR /app -EXPOSE 5000 -# 使用.NET 9 SDK作为构建镜像 -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +# 使用.NET 10 SDK作为构建镜像 +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src # 复制项目文件 @@ -26,16 +25,15 @@ RUN dotnet publish "DFApp.LotteryProxy.csproj" -c Release -o /app/publish /p:Use FROM base AS final WORKDIR /app +# 安装 curl(用于健康检查) +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + # 创建非root用户 -RUN adduser --disabled-password --gecos '' appuser && chown -R appuser /app +RUN useradd -m -s /bin/bash appuser && chown -R appuser /app USER appuser # 复制发布的应用 COPY --from=publish /app/publish . -# 设置健康检查 -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:5000/api/health || exit 1 - # 启动应用 ENTRYPOINT ["dotnet", "DFApp.LotteryProxy.dll"] \ No newline at end of file diff --git a/DFApp.LotteryProxy/Middleware/IpWhitelistMiddleware.cs b/DFApp.LotteryProxy/Middleware/IpWhitelistMiddleware.cs index 01275b22..e3f19c3f 100644 --- a/DFApp.LotteryProxy/Middleware/IpWhitelistMiddleware.cs +++ b/DFApp.LotteryProxy/Middleware/IpWhitelistMiddleware.cs @@ -26,12 +26,16 @@ public IpWhitelistMiddleware( public async Task InvokeAsync(HttpContext context) { var clientIp = GetClientIpAddress(context); - - _logger.LogDebug("客户端IP: {ClientIP}", clientIp); + + _logger.LogInformation("客户端IP: {ClientIP}", clientIp); + _logger.LogInformation("允许的IP列表: {AllowedIPs}", string.Join(", ", _proxySettings.AllowedIPs)); + _logger.LogInformation("允许的IP数量: {Count}", _proxySettings.AllowedIPs?.Count ?? 0); if (!IsIpAllowed(clientIp)) { _logger.LogWarning("未授权的IP访问尝试: {ClientIP}", clientIp); + _logger.LogWarning("请求路径: {Path}", context.Request.Path); + _logger.LogWarning("请求方法: {Method}", context.Request.Method); context.Response.StatusCode = (int)HttpStatusCode.Forbidden; await context.Response.WriteAsync("403 Forbidden: IP地址不在允许列表中"); return; @@ -42,35 +46,25 @@ public async Task InvokeAsync(HttpContext context) private string GetClientIpAddress(HttpContext context) { - // 尝试从各种头部获取真实IP - var ip = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); - - if (!string.IsNullOrEmpty(ip)) - { - // X-Forwarded-For可能包含多个IP,取第一个 - var ips = ip.Split(','); - if (ips.Length > 0) - { - ip = ips[0].Trim(); - } - } - - if (string.IsNullOrEmpty(ip)) - { - ip = context.Request.Headers["X-Real-IP"].FirstOrDefault(); - } - - if (string.IsNullOrEmpty(ip)) - { - ip = context.Connection.RemoteIpAddress?.ToString(); - } + // 仅使用 RemoteIpAddress,因为它来自TCP连接,无法被客户端伪造 + // 不使用 X-Forwarded-For 和 X-Real-IP 头部,因为它们可以被伪造 + var ip = context.Connection.RemoteIpAddress?.ToString(); + _logger.LogInformation("RemoteIpAddress: {IP}", ip); // 如果是IPv6回环地址,转换为IPv4 if (ip == "::1") { ip = "127.0.0.1"; + _logger.LogInformation("IPv6回环地址转换为IPv4: {IP}", ip); + } + // 如果是IPv4映射的IPv6地址(::ffff:x.x.x.x),提取IPv4部分 + else if (ip != null && ip.StartsWith("::ffff:")) + { + ip = ip.Substring(7); // 移除 "::ffff:" 前缀 + _logger.LogInformation("IPv6映射的IPv4地址转换为IPv4: {IP}", ip); } + _logger.LogInformation("最终获取的客户端IP: {IP}", ip); return ip ?? "unknown"; } @@ -81,13 +75,20 @@ private bool IsIpAllowed(string? clientIp) return false; } - // 如果允许列表为空,则允许所有IP(开发环境) - if (_proxySettings.AllowedIPs == null || _proxySettings.AllowedIPs.Count == 0) + // 默认允许本地访问(127.0.0.1 和 ::1) + if (clientIp == "127.0.0.1" || clientIp == "::1") { - _logger.LogDebug("IP白名单为空,允许所有IP访问"); + _logger.LogInformation("本地IP访问,自动允许: {IP}", clientIp); return true; } + // 如果允许列表为空,则拒绝所有IP(生产环境) + if (_proxySettings.AllowedIPs == null || _proxySettings.AllowedIPs.Count == 0) + { + _logger.LogInformation("IP白名单为空,拒绝所有IP访问(除本地外)"); + return false; + } + return _proxySettings.AllowedIPs.Contains(clientIp); } } diff --git a/DFApp.LotteryProxy/docker-compose.yml b/DFApp.LotteryProxy/docker-compose.yml index f838d322..f2f6939e 100644 --- a/DFApp.LotteryProxy/docker-compose.yml +++ b/DFApp.LotteryProxy/docker-compose.yml @@ -1,11 +1,9 @@ -version: '3.8' - services: lottery-proxy: build: . container_name: lottery-proxy ports: - - "5000:5000" + - "45000:8080" environment: - ASPNETCORE_ENVIRONMENT=Production - ProxySettings__AllowedIPs__0= @@ -17,7 +15,7 @@ services: - ./logs:/app/logs restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"] + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] interval: 30s timeout: 10s retries: 3 diff --git a/src/DFApp.Application/Background/LotteryResultTimer.cs b/src/DFApp.Application/Background/LotteryResultTimer.cs index a08d100f..32dfb2f6 100644 --- a/src/DFApp.Application/Background/LotteryResultTimer.cs +++ b/src/DFApp.Application/Background/LotteryResultTimer.cs @@ -64,7 +64,7 @@ public override async Task Execute(IJobExecutionContext context) private async Task StartWork(string lotteryType, string lotteryTypeEng, string code) { Logger.LogInformation($"开始任务......{lotteryType} (英文类型: {lotteryTypeEng}, 起始代码: {code})"); - + try { // 检查是否已有数据 @@ -91,7 +91,7 @@ private async Task StartWork(string lotteryType, string lotteryTypeEng, string c || DateTime.Now.DayOfWeek == DayOfWeek.Thursday || DateTime.Now.DayOfWeek == DayOfWeek.Tuesday || lotteryType == LotteryConst.KL8; - + Logger.LogInformation($"是否需要获取最新数据: {shouldFetchLatest} (当前星期: {DateTime.Now.DayOfWeek})"); if (shouldFetchLatest) @@ -100,7 +100,7 @@ private async Task StartWork(string lotteryType, string lotteryTypeEng, string c Logger.LogInformation($"检查今天 ({day}) 是否已有数据"); List result1 = await _resultReadOnly.GetListAsync(item => item.Date != null && item.Date.StartsWith(day)); Logger.LogInformation($"今天的数据条数: {result1?.Count ?? 0}"); - + if (result1 == null || result1.Count <= 0) { Logger.LogInformation("今天没有数据,开始获取最新数据"); @@ -110,13 +110,12 @@ private async Task StartWork(string lotteryType, string lotteryTypeEng, string c { Logger.LogInformation("开始更新奖级信息"); await UpdatePrizegrades(lotteryType, lotteryTypeEng); - + Logger.LogInformation("获取最新一期开奖结果作为起始点"); LotteryResult lotteryResult = (await _resultReadOnly.GetQueryableAsync()).OrderByDescending(x => x.Code).First(); string dayStart = (lotteryResult.Date!.Split('('))[0]; - dayStart = DateTime.Parse(dayStart).AddDays(1).ToString("yyyy-MM-dd"); Logger.LogInformation($"从 {dayStart} 开始获取最新数据"); - + await GetCurrentLotteryResult(dayStart, 0, lotteryTypeEng); await uom.CompleteAsync(); Logger.LogInformation("最新数据获取完成并提交事务"); @@ -149,9 +148,9 @@ private async Task GetCurrentLotteryResult(string dayStart, int pageNo, string l { string dayEnd = DateTime.Now.ToString("yyyy-MM-dd"); Logger.LogInformation($"获取当前彩票结果 - 起始日期: {dayStart}, 结束日期: {dayEnd}, 彩票类型: {lotteryType}, 页码: {pageNo}"); - - LotteryInputDto dto = await GetLotteryResult(dayStart, dayEnd, 1, lotteryType); - + + LotteryInputDto dto = await GetLotteryResult(dayStart, dayEnd, pageNo, lotteryType); + if (dto.Result != null && dto.Result.Count > 0) { Logger.LogInformation($"获取到 {dto.Result.Count} 条数据,开始映射并保存到数据库"); @@ -188,9 +187,9 @@ private async Task GetCurrentLotteryResult(string dayStart, int pageNo, string l private async Task GetAllLotteryResults(string dayStart, string dayEnd, int pageNo, string lotteryType) { Logger.LogInformation($"获取所有历史彩票结果 - 起始日期: {dayStart}, 结束日期: {dayEnd}, 彩票类型: {lotteryType}, 页码: {pageNo}"); - + LotteryInputDto dto = await GetLotteryResult(dayStart, dayEnd, pageNo, lotteryType); - + if (dto.Result != null && dto.Result.Count > 0) { Logger.LogInformation($"获取到 {dto.Result.Count} 条历史数据,开始映射并保存到数据库"); @@ -227,7 +226,7 @@ private async Task GetAllLotteryResults(string dayStart, string dayEnd, int page private async Task UpdatePrizegrades(string lotteryType, string lotteryTypeEng) { Logger.LogInformation($"开始更新奖级信息 - 彩票类型: {lotteryType} (英文: {lotteryTypeEng})"); - + try { // 查询没有奖级信息的彩票结果 @@ -241,7 +240,7 @@ from z2 in z.DefaultIfEmpty() var queryList = query.ToList(); Logger.LogInformation($"查询到 {queryList.Count} 条彩票结果记录"); - + int noPrizeCount = queryList.Count(x => x.prize == null); Logger.LogInformation($"其中 {noPrizeCount} 条记录没有奖级信息"); @@ -251,7 +250,7 @@ from z2 in z.DefaultIfEmpty() if (item.prize == null) { Logger.LogInformation($"处理彩票结果 ID: {item.result.Id}, 代码: {item.result.Code}, 日期: {item.result.Date}"); - + try { string dayStart = (item.result.Date!.Split('('))[0]; @@ -269,7 +268,7 @@ from z2 in z.DefaultIfEmpty() if (prize.Prizegrades != null && prize.Prizegrades.Count > 0) { Logger.LogInformation($"为彩票结果 ID: {item.result.Id} 添加 {prize.Prizegrades.Count} 条奖级信息"); - + foreach (var prizeId in prize.Prizegrades) { prizeId.LotteryResultId = item.result.Id; @@ -295,7 +294,7 @@ from z2 in z.DefaultIfEmpty() } } } - + Logger.LogInformation($"奖级信息更新完成,共处理 {processedCount} 条记录"); } catch (Exception ex) @@ -310,27 +309,27 @@ private async Task GetLotteryResult(string dayStart, string day // 使用代理服务器获取数据 string proxyServerUrl = LotteryConst.GetLotteryProxyUrl(_configuration); string requestUrl = $"{proxyServerUrl}/api/proxy/lottery/findDrawNotice?name={lotteryType}&dayStart={dayStart}&dayEnd={dayEnd}&pageNo={pageNo}&pageSize=30&week=&systemType=PC"; - + Logger.LogInformation($"开始通过代理获取彩票数据 - 彩票类型: {lotteryType}, 开始日期: {dayStart}, 结束日期: {dayEnd}, 页码: {pageNo}"); Logger.LogInformation($"代理请求URL: {requestUrl}"); - + try { using var client = _httpClientFactory.CreateClient(); // 设置超时时间 client.Timeout = TimeSpan.FromSeconds(60); - + Logger.LogInformation("发送代理HTTP请求..."); - + HttpResponseMessage message = await client.GetAsync(requestUrl); - + Logger.LogInformation($"代理HTTP响应状态码: {(int)message.StatusCode} ({message.StatusCode})"); - + message.EnsureSuccessStatusCode(); - + string responseContent = await message.Content.ReadAsStringAsync(); Logger.LogInformation($"代理响应内容长度: {responseContent.Length} 字符"); - + // 记录响应内容(仅前500字符,避免日志过长) if (responseContent.Length > 500) { @@ -340,9 +339,9 @@ private async Task GetLotteryResult(string dayStart, string day { Logger.LogInformation($"代理响应内容: {responseContent}"); } - + LotteryInputDto? dto = JsonSerializer.Deserialize(responseContent); - + if (dto == null) { Logger.LogWarning("反序列化代理响应失败,响应为null,创建空对象"); @@ -351,11 +350,11 @@ private async Task GetLotteryResult(string dayStart, string day else { Logger.LogInformation($"反序列化代理响应成功 - 总数据量: {dto.Total}, 当前页: {dto.PageNo}/{dto.PageNum}, 每页大小: {dto.PageSize}"); - + if (dto.Result != null) { Logger.LogInformation($"当前页数据条数: {dto.Result.Count}"); - + // 记录第一条数据的详细信息 if (dto.Result.Count > 0) {