Compare commits
3 Commits
607b60a28f
...
2e00719d56
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e00719d56 | ||
|
|
4ad36addea | ||
|
|
8d1928a247 |
@ -4,13 +4,17 @@
|
||||
|
||||
`YD_Event.Web` 的“官网赛事信息”页面请求当前环境的 `GET/PUT /api/website-competition`。此接口由 `YD_Event.Admin` 提供,沿用后台登录认证;管理页不需要填写接口地址或管理密钥。
|
||||
|
||||
`YD_Event.Admin` 在服务端把请求转发到官网赛事服务的 `/api/competition/admin`。服务间密钥只保存在服务器配置中,不发送给浏览器。官网赛事服务负责校验数据、持久化草稿或发布状态,并通知公开页面刷新。本仓库不新增赛事数据库表。
|
||||
正式环境访问基地址为 `https://jitsi-web.yuedongsports.com/`,管理接口完整地址为 `https://jitsi-web.yuedongsports.com/api/website-competition`。此地址是后台对外入口;`CompetitionApi:AdminUrl` 是后端转发到官网赛事服务的地址,需单独配置。
|
||||
|
||||
`YD_Event.Admin` 在服务端把请求转发到官网赛事服务的内部 `/api/competition/admin`。两台服务器通过私网连接,不使用服务间管理密钥。官网赛事服务按来源 IP 放行内部管理请求,持久化草稿或发布状态,供公开页面读取。本仓库不新增赛事数据库表。
|
||||
|
||||
官网公开页面不访问需要登录的管理接口,而是匿名请求 `GET /api/competition/state`。此路由由 `CompetitionPublicController` 通过同一私网赛事服务读取 `/api/competition/state`;赛事服务在未发布时只返回空态。公开接口只支持读取,不提供草稿或写入能力。官网每 10 秒重新读取一次。
|
||||
|
||||
```text
|
||||
YD_Event.Web 管理页
|
||||
→ 当前域名 /api/website-competition(后台登录态)
|
||||
→ YD_Event.Admin(附加服务间 Bearer 密钥)
|
||||
→ 官网赛事服务 /api/competition/admin
|
||||
→ YD_Event.Admin(通过私网转发)
|
||||
→ 官网赛事服务 /api/competition/admin(来源 IP 白名单)
|
||||
→ 持久化赛事 JSON / 官网公开接口
|
||||
```
|
||||
|
||||
@ -20,8 +24,9 @@ YD_Event.Web 管理页
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/api/website-competition` | 读取当前环境的完整赛事草稿,包括未发布内容 |
|
||||
| `PUT` | `/api/website-competition` | 保存赛事信息及 `published` 状态,JSON 请求体上限为 8 MB |
|
||||
| `GET` | `/api/competition/state` | 匿名读取已发布数据;未发布时返回空态 |
|
||||
|
||||
两个方法都要求已有的后台登录态。控制器原样转发上游的 JSON 响应和 HTTP 状态码。数据的主要结构如下;详细字段由管理页 Excel 模板及官网赛事服务校验规则约束。
|
||||
前两个管理方法要求已有的后台登录态;公开方法不要求登录。控制器原样转发上游的 JSON 响应和 HTTP 状态码。数据的主要结构如下;详细字段由管理页 Excel 模板及官网赛事服务校验规则约束。
|
||||
|
||||
```json
|
||||
{
|
||||
@ -39,31 +44,31 @@ YD_Event.Web 管理页
|
||||
|
||||
| 配置键 | 环境变量写法 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `CompetitionApi:AdminUrl` | `CompetitionApi__AdminUrl` | 官网赛事服务管理接口完整 URL;默认 `http://127.0.0.1:8787/api/competition/admin` |
|
||||
| `CompetitionApi:AdminToken` | `CompetitionApi__AdminToken` | 与官网赛事服务 `COMPETITION_ADMIN_TOKEN` 相同的服务间密钥 |
|
||||
| `CompetitionApi:AdminTokenFile` | `CompetitionApi__AdminTokenFile` | 可选,密钥文件的绝对路径;未配置密钥值时读取 |
|
||||
| `COMPETITION_ADMIN_TOKEN` | `COMPETITION_ADMIN_TOKEN` | 密钥值的兼容性回退配置 |
|
||||
| `CompetitionApi:AdminUrl` | `CompetitionApi__AdminUrl` | 官网赛事服务内部管理接口完整 URL;本地默认 `http://127.0.0.1:8787/api/competition/admin` |
|
||||
|
||||
官网赛事服务要求 `COMPETITION_ADMIN_TOKEN` 至少 32 字符。不要将密钥写入前端环境变量、前端代码或公开仓库。若两个服务不在同一网络命名空间,必须配置可由 `YD_Event.Admin` 访问的 `CompetitionApi__AdminUrl`,不能使用默认的 `127.0.0.1`。
|
||||
正式环境两台服务器分机部署,必须将 `CompetitionApi__AdminUrl` 配置为 `http://<赛事服务私网 IP>:8787/api/competition/admin`,不能使用默认的 `127.0.0.1`。赛事服务应只在私网地址监听,并通过 `COMPETITION_ADMIN_ALLOWED_IPS` 放行 `YD_Event.Admin` 的私网 IP;防火墙只允许后台服务器连接该端口。公网 Nginx 必须屏蔽 `/api/competition/admin`,避免绕过后台登录认证直接修改赛事数据。
|
||||
|
||||
公开状态接口从 `CompetitionApi__AdminUrl` 推导同源的 `/api/competition/state`,因此不需要额外的地址或密钥配置。正式环境沿用现有 `/api/` 反向代理到 YD_Event.Admin;不要把公开接口直接转发到赛事服务。
|
||||
|
||||
## 部署步骤
|
||||
|
||||
1. 启动官网赛事服务,设置持久化数据路径及 `COMPETITION_ADMIN_TOKEN`;其健康检查为 `GET /api/competition/ping`。
|
||||
2. 在 `YD_Event.Admin` 的运行环境配置管理接口 URL 与相同的服务间密钥,然后启动或重启后端。`Startup` 已注册专用 `WebsiteCompetition` HttpClient。
|
||||
1. 启动官网赛事服务,设置持久化数据路径、私网监听地址、后台服务器的来源 IP 白名单;其健康检查为 `GET /api/competition/ping`。
|
||||
2. 在 `YD_Event.Admin` 的运行环境配置内网管理接口 URL,然后启动或重启后端。`Startup` 已注册专用 `WebsiteCompetition` HttpClient。
|
||||
3. 将后台站点同源的 `/api/website-competition` 路由转发到 `YD_Event.Admin`。不要把该路径直接暴露或转发到官网赛事服务管理接口。
|
||||
4. 管理员登录 `YD_Event.Web` 后打开“官网赛事信息”,导入 `.xlsx`、核对草稿并保存;需要公开展示时再开启“公开发布”。
|
||||
5. 从 `https://www.yuedongsports.com` 携带 `Origin` 请求正式环境的 `GET /api/competition/state`,确认返回已发布数据或空态,以及允许官网来源的 CORS 响应头。
|
||||
|
||||
## 错误排查
|
||||
|
||||
| 状态 | 常见原因 |
|
||||
| --- | --- |
|
||||
| `401/403` | 后台未登录或无权访问;若由上游返回,也可能是服务间密钥不一致 |
|
||||
| `401/403` | 后台未登录或无权访问;若由上游返回 `403`,检查后台服务器私网 IP 是否在 `COMPETITION_ADMIN_ALLOWED_IPS` 中 |
|
||||
| `413` | `PUT` 请求体超过 8 MB |
|
||||
| `502` | 官网赛事服务无法连接 |
|
||||
| `503` | 服务端密钥缺失、密钥文件不可读取,或上游 URL 无效 |
|
||||
| `503` | 上游 URL 无效 |
|
||||
| `504` | 官网赛事服务超过 30 秒未响应 |
|
||||
|
||||
控制器不会在错误响应或日志中输出密钥。保存时上游的字段校验错误会原样返回,管理页会展示错误信息。
|
||||
保存时上游的字段校验错误会原样返回,管理页会展示错误信息。
|
||||
|
||||
## 验证
|
||||
|
||||
@ -72,3 +77,5 @@ dotnet build .\YD_Event.Web.Entry\YD_Event.Web.Entry.csproj --no-restore
|
||||
```
|
||||
|
||||
确认官网赛事服务的 `/api/competition/ping` 返回成功;使用已有后台登录态访问 `GET /api/website-competition`,应返回上述赛事结构。未经登录直接访问应被后台认证拒绝。`PUT` 应先在测试环境使用草稿数据验证,再按发布流程操作。
|
||||
|
||||
匿名访问 `GET /api/competition/state` 应返回公开空态或已发布数据。若返回 `404`,检查新控制器是否已部署,以及 Nginx 是否仍使用直接代理赛事服务的旧片段;若返回 `502`,检查私网连接及 `CompetitionApi__AdminUrl`。
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace YD_Event.Web.Entry.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 官网赛事公开数据。只转发赛事服务的已发布状态,不提供草稿或写入接口。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[AllowAnonymous]
|
||||
[Route("api/competition")]
|
||||
public sealed class CompetitionPublicController : ControllerBase
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public CompetitionPublicController(IConfiguration configuration, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[HttpGet("state")]
|
||||
public async Task<IActionResult> GetState()
|
||||
{
|
||||
var adminUrl = _configuration["CompetitionApi:AdminUrl"]
|
||||
?? "http://127.0.0.1:8787/api/competition/admin";
|
||||
if (!Uri.TryCreate(adminUrl, UriKind.Absolute, out var adminTarget)
|
||||
|| (adminTarget.Scheme != Uri.UriSchemeHttp && adminTarget.Scheme != Uri.UriSchemeHttps)
|
||||
|| adminTarget.AbsolutePath != "/api/competition/admin"
|
||||
|| !string.IsNullOrEmpty(adminTarget.Query))
|
||||
return StatusCode(503, new { error = "官网赛事接口地址无效" });
|
||||
|
||||
var target = new Uri(adminTarget, "state");
|
||||
|
||||
Response.Headers["Cache-Control"] = "no-store";
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("WebsiteCompetition");
|
||||
client.Timeout = TimeSpan.FromSeconds(15);
|
||||
using var upstreamResponse = await client.GetAsync(target, HttpContext.RequestAborted);
|
||||
var response = await upstreamResponse.Content.ReadAsStringAsync(HttpContext.RequestAborted);
|
||||
return new ContentResult
|
||||
{
|
||||
Content = response,
|
||||
ContentType = "application/json; charset=utf-8",
|
||||
StatusCode = (int)upstreamResponse.StatusCode
|
||||
};
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return StatusCode(502, new { error = "官网赛事接口连接失败" });
|
||||
}
|
||||
catch (TaskCanceledException) when (!HttpContext.RequestAborted.IsCancellationRequested)
|
||||
{
|
||||
return StatusCode(504, new { error = "官网赛事接口响应超时" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -6,7 +5,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
namespace YD_Event.Web.Entry.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 官网赛事信息管理。使用后台登录态访问,服务间密钥只保存在服务端。
|
||||
/// 官网赛事信息管理。使用后台登录态访问,将请求转发到内部赛事服务。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
@ -31,24 +30,6 @@ public sealed class WebsiteCompetitionController : ControllerBase
|
||||
|
||||
private async Task<IActionResult> ForwardAsync(HttpMethod method)
|
||||
{
|
||||
var token = _configuration["CompetitionApi:AdminToken"]
|
||||
?? _configuration["COMPETITION_ADMIN_TOKEN"];
|
||||
var tokenFile = _configuration["CompetitionApi:AdminTokenFile"];
|
||||
if (string.IsNullOrWhiteSpace(token) && !string.IsNullOrWhiteSpace(tokenFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
token = (await System.IO.File.ReadAllTextAsync(tokenFile, HttpContext.RequestAborted)).Trim();
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return StatusCode(503, new { error = "官网赛事接口密钥文件不可读取" });
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
return StatusCode(503, new { error = "官网赛事接口未配置服务端密钥" });
|
||||
|
||||
var adminUrl = _configuration["CompetitionApi:AdminUrl"]
|
||||
?? "http://127.0.0.1:8787/api/competition/admin";
|
||||
if (!Uri.TryCreate(adminUrl, UriKind.Absolute, out var target)
|
||||
@ -56,7 +37,6 @@ public sealed class WebsiteCompetitionController : ControllerBase
|
||||
return StatusCode(503, new { error = "官网赛事接口地址无效" });
|
||||
|
||||
using var upstreamRequest = new HttpRequestMessage(method, target);
|
||||
upstreamRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
if (method == HttpMethod.Put)
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user