---
url: /handbook/环境变量参考.md
---
# 环境变量参考

**本文引用的文件**

* [server/internal/config/env.go](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/env.go)
* [server/internal/config/config.go](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go)
* [server/cmd/skin-nexus/main.go](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/cmd/skin-nexus/main.go)
* [server/internal/siteurl/siteurl.go](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/siteurl/siteurl.go)
* [server/internal/mailer/config.go](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/mailer/config.go)
* [server/internal/union/config.go](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/union/config.go)
* [server/internal/ratelimit/limiter.go](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/ratelimit/limiter.go)
* [server/internal/ygg/service.go](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/ygg/service.go)
* [server/internal/accesslog/accesslog.go](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/accesslog/accesslog.go)

## 目录

1. [简介](#简介)
2. [项目结构与变量入口](#项目结构与变量入口)
3. [核心环境变量总览](#核心环境变量总览)
4. [架构概览：环境变量如何影响系统](#架构概览环境变量如何影响系统)
5. [详细变量说明](#详细变量说明)
6. [依赖关系分析](#依赖关系分析)
7. [性能与限流特性](#性能与限流特性)
8. [安全与敏感信息保护](#安全与敏感信息保护)
9. [故障排查指南](#故障排查指南)
10. [结论](#结论)

## 简介

本文是 SkinNexus 所有 `SKIN_*` 环境变量的权威参考。它按功能域分组，列出每个变量的名称、类型、默认值、是否必需、示例值和用途，并重点解释数据库连接串、SMTP、站点 URL、OIDC、Union 联邦、Yggdrasil 回源、限流、日志、安全头、插件信任、主题市场和访问日志等关键行为。

该文档只描述代码实际读取的变量；未出现在代码中的变量即使被某些部署脚本设置，也不会被进程使用。

## 项目结构与变量入口

`SKIN_*` 环境变量的“唯一真实来源”是配置包的已知列表。程序启动时会检查是否有未知变量，避免“拼写错误导致配置静默失效”。

```mermaid
flowchart TD
    Env["环境变量 SKIN_*"] --> Known["已知变量清单<br/>server/internal/config/env.go"]
    Env --> Load["加载配置<br/>server/internal/config/config.go"]
    Load --> SiteURL["站点基础地址<br/>server/internal/siteurl/siteurl.go"]
    Load --> Store["数据库连接<br/>store.Open(...)"]
    Load --> OIDC["OIDC 提供者"]
    Load --> Union["Union 成员站"]
    Load --> Ygg["Yggdrasil 服务"]
    Load --> Mailer["SMTP 邮件"]
    Load --> RateLimit["认证限流器"]
    Load --> AccessLog["访问日志记录器"]
```

**图表来源**

* [server/internal/config/env.go:24-69](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/env.go#L24-L69)
* [server/internal/config/config.go:1-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L1-L120)
* [server/internal/siteurl/siteurl.go:1-30](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/siteurl/siteurl.go#L1-L30)

**章节来源**

* [server/internal/config/env.go:1-104](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/env.go#L1-L104)
* [server/internal/config/config.go:1-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L1-L120)
* [server/cmd/skin-nexus/main.go:60-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/cmd/skin-nexus/main.go#L60-L120)

## 核心环境变量总览

下表按功能域组织所有 `SKIN_*` 变量。

| 功能域 | 变量 | 类型 | 默认值 | 必需 | 示例值 | 用途 |
|---|---|---|---|---|---|---|
| 网络与站点 | `SKIN_LISTEN` | 字符串 | `:8080` | 否 | `:8080`、`0.0.0.0:8080` | HTTP 监听地址 |
| 网络与站点 | `SKIN_BASE_URL` | 字符串 | `http://localhost:8080` | 否 | `https://skin.example.com` | 站点根地址；影响 OIDC issuer、Webhook 回调、皮肤分发 URL、纹理 URL |
| 网络与站点 | `SKIN_ENV` | 字符串 | `production` | 否 | `production`、`staging` | 部署标签 |
| 网络与站点 | `SKIN_DATA_DIR` | 字符串 | `./data` | 否 | `/var/lib/skin-nexus` | 数据根目录；签名密钥、主题、插件通常位于其下 |
| 网络与站点 | `SKIN_TEXTURES_DIR` | 字符串 | `<DataDir>/textures` | 否 | `/var/lib/skin-nexus/textures` | 纹理文件目录 |
| 网络与站点 | `SKIN_THEMES_DIR` | 字符串 | `<DataDir>/themes` | 否 | `<DataDir>/themes` | 主题包目录 |
| 网络与站点 | `SKIN_PLUGINS_DIR` | 字符串 | `<DataDir>/plugins` | 否 | `<DataDir>/plugins` | 插件包目录 |
| 网络与站点 | `SKIN_MIGRATIONS_DIR` | 字符串 | `./migrations` | 否 | `./migrations` | SQL 迁移目录 |
| 网络与站点 | `SKIN_AUTO_MIGRATE` | 布尔 | `false` | 否 | `true` | 启动时自动应用迁移 |
| 网络与站点 | `SKIN_LEGACY_ENV` | 字符串路径 | 无 | 否 | `/etc/skin-nexus/.env` | 导入旧版 `.env` 验证方法 |
| 身份与安全 | `SKIN_SETUP_TOKEN` | 字符串 | 启动时生成并输出到日志 | 否 | `a1b2c3d4e5f6` | 安装向导令牌 |
| 身份与安全 | `SKIN_SITE_NAME` | 字符串 | `SkinNexus` | 否 | `我的皮肤站` | 站点展示名 |
| 身份与安全 | `SKIN_TRUSTED_PROXIES` | 逗号分隔字符串 | 历史默认（环回或 RFC1918） | 否 | `10.0.0.0/8,172.16.0.0/12,192.168.0.0/16` | 允许转发真实 IP 的反向代理网段 |
| 身份与安全 | `SKIN_SERVER_NAME` | 字符串 | `SkinNexus` | 否 | `SkinNexus` | Yggdrasil 元数据中的服务器名 |
| 身份与安全 | `SKIN_SKIN_DOMAINS` | 逗号分隔字符串 | 空 | 否 | `skin.example.com` | 允许的皮肤域名 |
| 身份与安全 | `SKIN_SIGN_KEY` | 文件路径 | `<DataDir>/sign-key.pem` | 否 | `<DataDir>/sign-key.pem` | 站点 RSA 私钥 PEM 路径 |
| 身份与安全 | `SKIN_ADMIN_TOKEN` | 字符串 | 无 | 否 | `admin-secret-token` | 管理接口令牌 |
| 身份与安全 | `SKIN_CSP_SCRIPT_EXTRA_SOURCES` | 字符串 | 空 | 否 | `'https://cdn.example.com'` | 追加到 CSP `script-src` 的来源 |
| 身份与安全 | `SKIN_CSP_CONNECT_EXTRA_SOURCES` | 字符串 | 空 | 否 | `'https://api.example.com'` | 追加到 CSP `connect-src` 的来源 |
| 注册与认证 | `SKIN_OIDC_ENABLED` | 布尔 | `false` | 否 | `true` | 启用内部 OIDC 提供者和无密码登录流程 |
| 注册与认证 | `SKIN_OIDC_ISSUER` | 字符串 | 继承 `SKIN_BASE_URL` | 否 | `https://skin.example.com` | OIDC issuer；应等于站点公开根 |
| 注册与认证 | `SKIN_OIDC_SIGN_KEY` | 文件路径 | `<DataDir>/oidc-key.pem` | 否 | `<DataDir>/oidc-key.pem` | OIDC JWT 签名私钥 PEM 路径 |
| 注册与认证 | `SKIN_OIDC_SHARED_CLIENT_ID` | 字符串 | `2` | 否 | `2` | 公共客户端发现 ID |
| 注册与认证 | `SKIN_REQUIRE_VERIFICATION` | 布尔 | `false` | 否 | `true` | 要求邮箱验证后才能加入服务器 |
| 注册与认证 | `SKIN_INVITE_REQUIRED` | 布尔 | `false` | 否 | `true` | 注册需要邀请码 |
| 注册与认证 | `SKIN_LOGIN_FAIL_LIMIT` | 整数 | `5` | 否 | `5` | 同一标识连续失败后锁定次数 |
| 注册与认证 | `SKIN_RATE_LIMIT_AUTH` | 整数 | `120` | 否 | `120` | 每 IP 每 10 分钟滑动窗口最大请求数 |
| 邮件 | `SKIN_SMTP_HOST` | 字符串 | 空 | 条件必需 | `smtp.example.com` | SMTP 主机；为空表示关闭邮件功能 |
| 邮件 | `SKIN_SMTP_PORT` | 整数 | `465` | 否 | `465`、`587` | SMTP 端口；非法值回退 465 |
| 邮件 | `SKIN_SMTP_USER` | 字符串 | 空 | 条件必需 | `noreply@example.com` | SMTP 用户名；当 From 为空时可充当 From |
| 邮件 | `SKIN_SMTP_PASS` | 字符串 | 空 | 否 | `secret` | SMTP 密码 |
| 邮件 | `SKIN_SMTP_FROM` | 字符串 | 空 | 条件必需 | `noreply@example.com` | 发件人地址；为空且用户名为空时邮件功能不可用 |
| 插件市场 | `SKIN_PLUGIN_SECRET_KEY` | 字符串 | 无 | 否 | `plugin-signing-secret` | 插件签名相关密钥 |
| 插件市场 | `SKIN_PLUGIN_STATE_DIR` | 字符串 | `<PluginsDir>/.state` | 否 | `<PluginsDir>/.state` | 插件状态目录 |
| 插件市场 | `SKIN_PLUGIN_MARKET_URL` | 字符串 | 官方插件注册表 | 可显式设为空以禁用 | `https://yimingzhanshen.github.io/skin-nexus-plugins/v1/registry.json` | 插件市场地址 |
| 插件市场 | `SKIN_PLUGIN_TRUST_KEYS` | 字符串 | 官方插件信任键 | 可显式设为空以关闭受信任安装 | `market-2026-10:ID2EWgcfs1E4UbGCCX9WjD0aCgTof3oeuHV03IpdGNY=` | 插件市场信任键 |
| 插件市场 | `SKIN_PLUGIN_UPLOAD_MAX` | 整数字节 | `64 MiB` | 否 | `134217728` | 插件上传大小上限 |
| 主题市场 | `SKIN_THEME_MARKET_URL` | 字符串 | 官方主题注册表 | 可显式设为空以禁用 | `https://yimingzhanshen.github.io/skin-nexus-plugins/themes/v1/registry.json` | 主题市场地址 |
| 主题市场 | `SKIN_THEME_TRUST_KEYS` | 字符串 | 官方主题信任键 | 可显式设为空以关闭受信任安装 | `theme-market-2026-11:EXxNehbsxNG9gdYUztMl/EU/lZ09+fPftPvmjeqPr/I=` | 主题市场信任键 |
| Union 联邦 | `SKIN_UNION_ENABLED` | 布尔 | `false` | 否 | `true` | 启用 Union 成员模块 |
| Union 联邦 | `SKIN_UNION_API_ROOT` | 绝对 URL | `https://skin.mualliance.ltd/api/union` | 启用时为必需 | `https://hub.example.com/api/union` | Hub 端点根地址 |
| Union 联邦 | `SKIN_UNION_MEMBER_KEY` | 字符串 | 无 | 启用时为必需 | `member-shared-secret` | 成员共享密钥，用于出站调用和 OAuth2 grant 签名 |
| Union 联邦 | `SKIN_UNION_HOST_PUBKEY` | 文件路径 | `<DataDir>/union-host-pubkey.pem` | 启用时为必需 | `<DataDir>/union-host-pubkey.pem` | Hub 消息签名公钥 PEM |
| Union 联邦 | `SKIN_UNION_ALLOW_UPDATEBACKENDKEY` | 布尔 | `false` | 否 | `false` | 是否允许远端更新后端成员密钥 |
| Union 联邦 | `SKIN_UNION_QUERYEMAIL_MODE` | 枚举 | `full` | 否 | `full`、`off` | 邮箱查询模式：`full` 返回邮箱，`off` 拒绝 |
| Union 联邦 | `SKIN_UNION_OAUTH2_SIG_KEY` | 文件路径 | `<DataDir>/union-oauth2-sig.pem` | 启用时为必需 | `<DataDir>/union-oauth2-sig.pem` | Union OAuth2 用户信息签名私钥 PEM |
| Union 联邦 | `SKIN_UNION_HUB_ORIGIN` | 字符串 | `https://skin.mualliance.ltd` | 否 | `https://hub.example.com` | Hub CORS 白名单来源 |
| Union 联邦 | `SKIN_UNION_RESOLVE_NAMES` | 布尔 | `true` | 否 | `true` | 是否解析各节点自身发现文档显示站点名 |
| Yggdrasil 回源 | `SKIN_MOJANG_FALLBACK` | 字符串 | 由配置加载逻辑处理 | 在已知清单中声明 | `https://sessionserver.mojang.com` | Mojang 回源地址（存在于已知变量清单） |

**章节来源**

* [server/internal/config/env.go:24-69](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/env.go#L24-L69)
* [server/internal/config/config.go:120-200](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L120-L200)

## 架构概览：环境变量如何影响系统

`SKIN_BASE_URL` 是整个站点对外地址的核心锚点。它不仅决定 Web 界面链接，还影响 OIDC issuer、Webhook 回调和皮肤分发 URL。

```mermaid
flowchart LR
    Base["SKIN_BASE_URL"] --> SiteURL["站点基础地址层<br/>siteurl.Get()"]
    Base --> OIDCIssuer["OIDC issuer<br/>SKIN_OIDC_ISSUER"]
    Base --> Webhook["Webhook 回调地址"]
    Base --> SkinDist["皮肤分发 URL"]
    Base --> Texture["纹理绝对 URL"]
```

**图表来源**

* [server/internal/config/config.go:140-170](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L140-L170)
* [server/internal/siteurl/siteurl.go:31-87](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/siteurl/siteurl.go#L31-L87)

## 详细变量说明

### 数据库连接：`SKIN_DATABASE_DSN`

该变量直接传给底层存储打开函数，不经过通用配置解析器。因此它的格式取决于数据库驱动：

| 数据库 | 连接串格式 | 说明 |
|---|---|---|
| MySQL | `user:pass@tcp(host:port)/database?parseTime=true&charset=utf8mb4` | 现有生产部署默认走 MySQL；裸 MySQL DSN 保持 MySQL |
| PostgreSQL | `postgres://user:pass@host:port/database` | 带 `postgres://` 前缀会被识别为 PostgreSQL |
| SQLite | `sqlite:/path/to/file.db` | 带 `sqlite:` 前缀会被识别为 SQLite |

注意：

* 该变量没有统一默认值；如果未设置，存储初始化会接收空 DSN。
* 文件名和连接参数应由运维人员根据部署环境填写。
* SQLite 适合单实例开发或小规模部署；MySQL 和 PostgreSQL 适合多实例和生产环境。

```mermaid
flowchart TD
    DSN["SKIN_DATABASE_DSN"] --> Open["store.Open(..., DatabaseDSN, legacy)"]
    Open --> MySQL["MySQL"]
    Open --> Postgres["PostgreSQL"]
    Open --> SQLite["SQLite"]
```

**图表来源**

* [server/cmd/skin-nexus/main.go:100-140](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/cmd/skin-nexus/main.go#L100-L140)

**章节来源**

* [server/internal/config/config.go:120-150](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L120-L150)
* [server/cmd/skin-nexus/main.go:100-140](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/cmd/skin-nexus/main.go#L100-L140)

### SMTP 邮件：`SKIN_SMTP_*`

邮件配置由独立的配置解析逻辑处理，规则如下：

| 变量 | 类型 | 默认值 | 必需性 | 说明 |
|---|---|---|---|---|
| `SKIN_SMTP_HOST` | 字符串 | 空 | 条件必需 | 为空表示邮件功能关闭 |
| `SKIN_SMTP_PORT` | 整数 | `465` | 否 | 必须为正数且小于 65536；非法值回退 465 |
| `SKIN_SMTP_USER` | 字符串 | 空 | 条件必需 | 若 `SKIN_SMTP_FROM` 也为空，则用它作为 From |
| `SKIN_SMTP_PASS` | 字符串 | 空 | 否 | SMTP 密码 |
| `SKIN_SMTP_FROM` | 字符串 | 空 | 条件必需 | 发件人地址；为空且用户名为空时邮件不可用 |

关键行为：

* 只要 `SKIN_SMTP_HOST` 为空，整个邮件配置就视为未配置。
* 如果没有有效的 From 地址，邮件功能不可用；此时管理面板不会伪装成功，而是返回 503。
* 从协议兼容性看，`SKIN_SMTP_PORT=465` 对应隐式 TLS，`SKIN_SMTP_PORT=587` 通常配合 STARTTLS。代码实现中仅解析端口，并未暴露独立 `starttls` 或 `tls` 布尔开关。

```mermaid
flowchart TD
    Host["SKIN_SMTP_HOST"] --> CheckHost{"是否为空？"}
    CheckHost -->|是| Disabled["邮件功能关闭"]
    CheckHost -->|否| Port["SKIN_SMTP_PORT"]
    Port --> FromUser["SKIN_SMTP_FROM / SKIN_SMTP_USER"]
    FromUser --> ValidFrom{"有有效 From 吗？"}
    ValidFrom -->|否| Disabled
    ValidFrom -->|是| Ready["邮件配置就绪"]
```

**图表来源**

* [server/internal/mailer/config.go:1-43](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/mailer/config.go#L1-L43)

**章节来源**

* [server/internal/mailer/config.go:1-43](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/mailer/config.go#L1-L43)

### 站点 URL：`SKIN_BASE_URL`

`SKIN_BASE_URL` 是站点对外根地址，最终通过 `siteurl.Set()` 安装到全局原子指针中。它会影响：

1. **OIDC issuer**：如果未显式设置 `SKIN_OIDC_ISSUER`，则回退到 `SKIN_BASE_URL`。
2. **纹理 URL**：纹理资源使用站点根地址拼接 `/textures/{hash}`。
3. **头像 URL**：头像和资源预览都基于站点根地址。
4. **页面 URL**：所有页面跳转都基于站点根地址。
5. **皮肤分发 URL 和 Webhook 回调**：这些对外回调地址依赖已解析的站点基础地址。

```mermaid
flowchart TD
    Base["SKIN_BASE_URL"] --> Set["siteurl.Set()"]
    Set --> Get["siteurl.Get()"]
    Get --> Texture["/textures/{hash}"]
    Get --> Avatar["/avatar/hash/{hash}"]
    Get --> Preview["/preview/{tid}"]
    Get --> Page["/skinlib/..."]
```

**图表来源**

* [server/internal/siteurl/siteurl.go:31-87](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/siteurl/siteurl.go#L31-L87)

**章节来源**

* [server/internal/siteurl/siteurl.go:1-87](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/siteurl/siteurl.go#L1-L87)
* [server/internal/config/config.go:140-170](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L140-L170)

### OIDC：`SKIN_OIDC_*`

当 `SKIN_OIDC_ENABLED=true` 时，系统启用内部 OIDC 提供者，支持设备授权、刷新 token、撤销、用户信息等标准端点。

| 变量 | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
| `SKIN_OIDC_ENABLED` | 布尔 | `false` | 否 | 启用 OIDC |
| `SKIN_OIDC_ISSUER` | 字符串 | 继承 `SKIN_BASE_URL` | 否 | OIDC issuer；应与外部可见站点根一致 |
| `SKIN_OIDC_SIGN_KEY` | 文件路径 | `<DataDir>/oidc-key.pem` | 启用时为必需 | OIDC JWT 签名私钥 PEM |
| `SKIN_OIDC_SHARED_CLIENT_ID` | 字符串 | `2` | 否 | 公共客户端 discovery ID |

OIDC 端点包括：

* `/.well-known/openid-configuration`
* `/authorize`
* `/oauth/token`
* `/oauth/introspect`
* `/authorize/callback`
* `/revoke`
* `/end_session`
* `/keys`
* `/device_authorization`
* `/userinfo`
* `/yggc/userinfo`
* `/device.html`
* `/api/v1/oidc/...`

```mermaid
sequenceDiagram
    participant Client as "客户端"
    participant Provider as "OIDC 提供者"
    participant Storage as "后端存储"

    Client->>Provider: "获取发现文档"
    Provider-->>Client: "issuer、shared_client_id、端点"
    Client->>Provider: "/authorize"
    Provider->>Storage: "创建或更新客户端"
    Provider-->>Client: "授权响应"
    Client->>Provider: "/oauth/token"
    Provider->>Storage: "签发 token"
    Provider-->>Client: "ID token / access token"
```

**图表来源**

* [server/internal/oidc/provider.go:1-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/oidc/provider.go#L1-L120)
* [server/internal/oidc/provider.go:120-200](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/oidc/provider.go#L120-L200)

**章节来源**

* [server/internal/config/config.go:140-170](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L140-L170)
* [server/internal/oidc/provider.go:1-200](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/oidc/provider.go#L1-L200)

### Union 成员站：`SKIN_UNION_*`

Union 是 MUA 联盟的成员站协议。启用后，站点可与 hub 和其他成员站通信，包括签名消息、OAuth2 grant、邮箱查询、成员名解析等。

| 变量 | 类型 | 默认值 | 必需 | 说明 |
|---|---|---|---|---|
| `SKIN_UNION_ENABLED` | 布尔 | `false` | 否 | 启用 Union 模块 |
| `SKIN_UNION_API_ROOT` | 绝对 URL | `https://skin.mualliance.ltd/api/union` | 启用时为必需 | Hub API 根地址 |
| `SKIN_UNION_MEMBER_KEY` | 字符串 | 无 | 启用时为必需 | 成员共享密钥 |
| `SKIN_UNION_HOST_PUBKEY` | 文件路径 | `<DataDir>/union-host-pubkey.pem` | 启用时为必需 | Hub 消息签名公钥 |
| `SKIN_UNION_ALLOW_UPDATEBACKENDKEY` | 布尔 | `false` | 否 | 是否允许远端更新后端密钥 |
| `SKIN_UNION_QUERYEMAIL_MODE` | 枚举 | `full` | 否 | `full` 或 `off` |
| `SKIN_UNION_OAUTH2_SIG_KEY` | 文件路径 | `<DataDir>/union-oauth2-sig.pem` | 启用时为必需 | Union OAuth2 签名私钥 |
| `SKIN_UNION_HUB_ORIGIN` | 字符串 | `https://skin.mualliance.ltd` | 否 | Hub CORS 来源 |
| `SKIN_UNION_RESOLVE_NAMES` | 布尔 | `true` | 否 | 是否解析节点发现文档显示站点名 |

安全要点：

* `SKIN_UNION_MEMBER_KEY` 是出站调用的 HMAC 密钥，也是 OAuth2 grant token 的签名密钥。
* `SKIN_UNION_HOST_PUBKEY` 是固定 pin 的 Hub 公钥，不应依赖每次远程拉取。
* `SKIN_UNION_ALLOW_UPDATEBACKENDKEY=false` 是默认安全策略，防止远端改写共享密钥。
* `SKIN_UNION_QUERYEMAIL_MODE` 为非法值时采用 fail-closed 策略。

```mermaid
flowchart TD
    Enabled{"SKIN_UNION_ENABLED"} -->|false| Inert["Union 模块处于惰性状态"]
    Enabled -->|true| Validate["校验 ApiRoot、QueryEmailMode"]
    Validate --> MemberKey["SKIN_UNION_MEMBER_KEY"]
    Validate --> HostPubkey["SKIN_UNION_HOST_PUBKEY"]
    Validate --> SigKey["SKIN_UNION_OAUTH2_SIG_KEY"]
```

**图表来源**

* [server/internal/union/config.go:1-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/union/config.go#L1-L120)
* [server/internal/union/config.go:120-200](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/union/config.go#L120-L200)

**章节来源**

* [server/internal/config/config.go:160-200](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L160-L200)
* [server/internal/union/config.go:1-200](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/union/config.go#L1-L200)

### Yggdrasil 回源与回退：`SKIN_MOJANG_FALLBACK`

`SKIN_MOJANG_FALLBACK` 出现在已知环境变量清单中，代表 Yggdrasil 协议的 Mojang 回源配置。具体回源行为由 Yggdrasil 服务层处理，并与限流、会话 TTL、玩家匹配等逻辑协同。

**章节来源**

* [server/internal/config/env.go:24-69](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/env.go#L24-L69)
* [server/internal/ygg/service.go:1-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/ygg/service.go#L1-L120)

### 限流：`SKIN_RATE_LIMIT_AUTH` 与 `SKIN_LOGIN_FAIL_LIMIT`

认证限流由内存中的滑动窗口 + 连续失败锁机制组成：

| 变量 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `SKIN_RATE_LIMIT_AUTH` | 整数 | `120` | 每 IP 每 10 分钟滑动窗口最大请求数；0 关闭窗口层 |
| `SKIN_LOGIN_FAIL_LIMIT` | 整数 | `5` | 同一标识连续失败达到阈值后锁定 15 分钟；0 关闭锁层 |

重要限制：

* 失败计数针对“标识”，不是 IP；防止 NAT 出口被误伤。
* 请求窗口针对 IP；防止资源滥用。
* 两个维度相互独立：失败不会锁定 IP，窗口命中不会锁定标识。
* 时间戳使用毫秒精度，清理循环定期释放过期条目。

```mermaid
flowchart TD
    Request["认证请求"] --> Window["IP 滑动窗口"]
    Window --> Allowed{"是否超过窗口上限？"}
    Allowed -->|是| Throttle["429 限流"]
    Allowed -->|否| Identifier["标识失败计数"]
    Identifier --> Locked{"是否处于锁定窗口？"}
    Locked -->|是| Lockout["锁定并重试间隔"]
    Locked -->|否| Process["继续认证流程"]
```

**图表来源**

* [server/internal/ratelimit/limiter.go:1-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/ratelimit/limiter.go#L1-L120)
* [server/internal/ratelimit/limiter.go:120-187](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/ratelimit/limiter.go#L120-L187)

**章节来源**

* [server/internal/config/config.go:150-170](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L150-L170)
* [server/internal/ratelimit/limiter.go:1-187](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/ratelimit/limiter.go#L1-L187)
* [server/internal/ygg/service.go:100-180](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/ygg/service.go#L100-L180)

### 日志级别与访问日志

代码中没有名为 `SKIN_LOG_LEVEL` 的环境变量。系统的可观测性主要通过访问日志模块实现，而不是通过一个通用的日志级别开关。

访问日志记录以下类别：

* Web 登录、注册、邮箱验证门控
* Yggdrasil 认证、刷新、校验、加入、hasJoined、profile
* Union 入站和出站
* 插件事件

结果分类包括：

* `ok`：成功
* `fail`：业务拒绝
* `denied`：限流、锁死或模块禁用
* `miss`：未知资料或会话
* `error`：内部错误

访问日志写入失败不会影响正常请求路径，只会记录操作者日志。

```mermaid
flowchart TD
    Event["Web/Yggdrasil/Union/Plugin 事件"] --> Logger["访问日志记录器"]
    Logger --> Sanitize["字段截断与清洗"]
    Sanitize --> Sink["持久化到存储"]
    Sink --> AdminPanel["管理面板查看"]
```

**图表来源**

* [server/internal/accesslog/accesslog.go:1-134](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/accesslog/accesslog.go#L1-L134)

**章节来源**

* [server/internal/accesslog/accesslog.go:1-134](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/accesslog/accesslog.go#L1-L134)

### 安全头：`SKIN_CSP_SCRIPT_EXTRA_SOURCES`、`SKIN_CSP_CONNECT_EXTRA_SOURCES`

这两个变量向内容安全策略追加第三方来源：

| 变量 | 用途 |
|---|---|
| `SKIN_CSP_SCRIPT_EXTRA_SOURCES` | 追加到 `script-src` |
| `SKIN_CSP_CONNECT_EXTRA_SOURCES` | 追加到 `connect-src` |

它们保留原始值，由头部层负责校验来源合法性；无效值会在启动阶段拒绝。

**章节来源**

* [server/internal/config/config.go:40-80](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L40-L80)

### 插件信任：`SKIN_PLUGIN_TRUST_KEYS`

插件信任键格式为 `id:base64pub,...`。默认值是官方插件信任键；可以显式设置为空以关闭受信任安装，也可以替换为自定义键。

| 变量 | 默认值 | 说明 |
|---|---|---|
| `SKIN_PLUGIN_MARKET_URL` | 官方插件注册表 | 插件市场地址；空值表示关闭 |
| `SKIN_PLUGIN_TRUST_KEYS` | 官方插件信任键 | 插件签名信任键；空值关闭受信任安装 |
| `SKIN_PLUGIN_UPLOAD_MAX` | `64 MiB` | 插件上传大小上限 |

**章节来源**

* [server/internal/config/config.go:80-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L80-L120)

### 主题市场：`SKIN_THEME_MARKET_URL`、`SKIN_THEME_TRUST_KEYS`

主题市场与插件市场共用发行仓库，但使用独立的信任键。这样任何一个密钥泄露都不会同时破坏两个表面。

| 变量 | 默认值 | 说明 |
|---|---|---|
| `SKIN_THEME_MARKET_URL` | 官方主题注册表 | 主题市场地址；空值表示关闭 |
| `SKIN_THEME_TRUST_KEYS` | 官方主题信任键 | 主题签名信任键；空值关闭受信任安装 |

**章节来源**

* [server/internal/config/config.go:80-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L80-L120)

## 依赖关系分析

```mermaid
graph TB
    Env["SKIN_* 环境变量"] --> Config["配置加载"]
    Config --> DB["数据库连接"]
    Config --> SiteURL["站点 URL"]
    Config --> OIDC["OIDC"]
    Config --> Union["Union"]
    Config --> Mailer["SMTP"]
    Config --> Limit["认证限流"]
    Config --> Log["访问日志"]
    SiteURL --> OIDC
    SiteURL --> Texture["纹理 URL"]
    SiteURL --> Webhook["Webhook 回调"]
    Limit --> Ygg["Yggdrasil 认证"]
```

**图表来源**

* [server/internal/config/env.go:24-69](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/env.go#L24-L69)
* [server/internal/config/config.go:1-200](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L1-L200)
* [server/internal/siteurl/siteurl.go:1-87](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/siteurl/siteurl.go#L1-L87)

## 性能与限流特性

* 认证限流使用内存映射，无外部依赖，适合单实例部署。
* 滑动窗口和失败锁都有清理协程，按活跃键释放过期数据。
* 默认 `SKIN_RATE_LIMIT_AUTH=120` 是为了解决共享 NAT 出口导致的批量触发问题。
* 默认 `SKIN_LOGIN_FAIL_LIMIT=5` 防止暴力破解，并在超时后自动解除锁定。
* 访问日志写入失败不会重试，也不会影响主请求路径。

**章节来源**

* [server/internal/ratelimit/limiter.go:1-187](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/ratelimit/limiter.go#L1-L187)
* [server/internal/accesslog/accesslog.go:1-134](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/accesslog/accesslog.go#L1-L134)

## 安全与敏感信息保护

以下变量属于敏感信息，不应直接写入镜像、源码仓库或普通配置文件：

| 敏感变量 | 风险 | 建议 |
|---|---|---|
| `SKIN_SIGN_KEY` | 站点签名私钥泄露会导致伪造内容 | 使用 secret manager、docker secrets 或运行时注入 |
| `SKIN_OIDC_SIGN_KEY` | OIDC JWT 签名密钥泄露可伪造身份 | 单独保管，不与站点签名密钥混用 |
| `SKIN_ADMIN_TOKEN` | 管理接口越权 | 使用强随机值并通过运行环境注入 |
| `SKIN_SMTP_PASS` | 邮件凭据泄露 | 不要硬编码进镜像 |
| `SKIN_UNION_MEMBER_KEY` | Union 出站认证和 OAuth2 grant 签名 | 只在目标部署中注入 |
| `SKIN_UNION_HOST_PUBKEY` | 消息验签公钥 | 应 pin 到可信版本 |
| `SKIN_UNION_OAUTH2_SIG_KEY` | Union OAuth2 用户信息签名私钥 | 严格隔离 |
| `SKIN_PLUGIN_SECRET_KEY` | 插件签名相关密钥 | 按需最小权限注入 |

最佳实践：

1. 不要把密钥写入 Docker 镜像层。
2. 使用 secret manager、Kubernetes Secret、Docker Secrets 或编排平台提供的机密注入能力。
3. 不同服务使用不同密钥：站点签名密钥、OIDC 密钥、Union 密钥、SMTP 密码应分开。
4. 对 Union 和 OIDC 密钥做轮换计划，并确保旧密钥过渡期兼容。
5. 审计日志可能包含 IP、UA、部分诊断字段；确认访问日志存储位置不在公开仓库中。

**章节来源**

* [server/internal/config/config.go:1-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/config.go#L1-L120)
* [server/internal/union/config.go:1-120](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/union/config.go#L1-L120)
* [server/internal/accesslog/accesslog.go:1-134](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/accesslog/accesslog.go#L1-L134)

## 故障排查指南

| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 设置了一个 `SKIN_*` 变量但不起作用 | 变量名拼写错误或未被代码读取 | 检查启动警告；对照已知变量清单 |
| 邮件功能看似开启但无法发送 | `SKIN_SMTP_FROM` 和 `SKIN_SMTP_USER` 都为空 | 至少提供一个有效 From 地址 |
| 邮箱功能直接返回 503 | 邮件配置被视为未配置 | 检查 `SKIN_SMTP_HOST`、From、User |
| OIDC issuer 与实际域名不一致 | 未设置 `SKIN_OIDC_ISSUER` 且 `SKIN_BASE_URL` 指向内网 | 显式设置 `SKIN_OIDC_ISSUER` |
| 纹理或头像链接错误 | `SKIN_BASE_URL` 不包含正确域名或端口 | 检查站点根地址 |
| Union 启用时报错 | 缺少必填成员密钥、Hub 公钥或 API Root | 检查 `SKIN_UNION_*` 配置 |
| 认证频繁 429 | `SKIN_RATE_LIMIT_AUTH` 过低或存在共享 NAT | 调整限流阈值或扩容实例 |
| 登录失败后被锁定 | `SKIN_LOGIN_FAIL_LIMIT` 触发连续失败锁 | 等待锁定时长或检查客户端重试逻辑 |
| 迁移未执行 | `SKIN_AUTO_MIGRATE` 未启用或迁移目录不正确 | 检查 `SKIN_AUTO_MIGRATE` 和 `SKIN_MIGRATIONS_DIR` |
| 访问日志没有详情 | 诊断窗口未开启 | 确认诊断模式开关 |

**章节来源**

* [server/internal/config/env.go:70-104](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/config/env.go#L70-L104)
* [server/internal/mailer/config.go:1-43](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/mailer/config.go#L1-L43)
* [server/internal/union/config.go:120-200](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/union/config.go#L120-L200)
* [server/internal/ratelimit/limiter.go:1-187](https://github.com/YimingZhanshen/skin-nexus/blob/797ae1a/server/internal/ratelimit/limiter.go#L1-L187)

## 结论

`SKIN_*` 环境变量构成了 SkinNexus 的全部运行时配置入口。其中：

* `SKIN_DATABASE_DSN` 决定数据库引擎和连接方式。
* `SKIN_BASE_URL` 是 OIDC issuer、Webhook 回调、皮肤分发 URL 和纹理 URL 的共同基础。
* `SKIN_SMTP_*` 控制密码恢复邮件；为空表示关闭。
* `SKIN_OIDC_*` 启用内部 OIDC 提供者。
* `SKIN_UNION_*` 控制联盟成员站行为，尤其是成员密钥、Hub 公钥和安全级别。
* `SKIN_RATE_LIMIT_AUTH` 和 `SKIN_LOGIN_FAIL_LIMIT` 共同保护认证接口。
* 插件和主题市场各自拥有独立的市场地址和信任键。
* 访问日志提供审计能力，但没有 `SKIN_LOG_LEVEL` 这类通用日志级别变量。
* 所有密钥都应通过 secret manager 或容器编排平台的机密机制注入，不要写入镜像。
