权限模型与鉴权体系
- 00-inbox/product-bot-answers/2026-09-22-P1-r2-Q10-权限模型与鉴权全景回答.md(源码确认级·22条确认结论)
- octo-server/modules/space, modules/group, modules/oidc, modules/bot_api, modules/app_bot, pkg/authtree, pkg/auth, pkg/space
OCTO 权限模型与鉴权体系 v0.1
文档定位:跨模块架构文档(架构·安全·售前口径三合一)。所有售前/交付/API集成人员在回答客户权限/租户/SSO问题前必须先读本页顶部三条硬口径。
源码证据范围:`octo-server` 全仓(`modules/space`、`modules/group`、`modules/oidc`、`modules/bot_api`、`modules/app_bot`、`pkg/authtree`、`pkg/auth`、`pkg/space`),证据级别=源码确认级(confidence: high)。
关联文档:
- 02-architecture/OCTO总体架构.md(§授权层宏观描述,本页为权威细化)
- 02-architecture/安全权限与鉴权-v0.1.md(早期v0.1推断版,本页替代其核心结论,旧版待归档)
- 06-api-integration/Bot-API概览.md(Bot token使用侧)
- 01-product/核心能力概览.md(产品能力描述侧)
🔴🔴🔴 三条售前安全硬口径(必读 · 红色安全红线)
⚠️ 以下三条为源码级确认的安全红线,任何售前方案、POC承诺、客户答复、招标应答均不得违背。若客户需求明确要求相反能力(例如物理隔离、自定义RBAC、独立organization层级),必须走"产品定制/规划中能力"路径,不得口头承诺已支持。
🔴 硬口径①:无 organization 概念,租户边界唯一是 Space
Octo 源码里根本没有 "organization"(组织)作为一级租户实体。全仓 `org_id` 字段搜索结果为 0 命中。租户边界、成员管理、权限隔离的唯一锚点是 Space(团队空间)。
- Space = organization = workspace 三词同义,代码只使用 "Space" 一词。
- 售前/交付材料中若出现 "组织/organization",实际指向都是 Space。
- 不存在"组织下多Space"的层级,Space 就是最顶层租户单元。
源码证据:全仓 `grep -r "org_id" modules/ pkg/` 返回 0 命中;`modules/space/model.go` 定义 SpaceModel/MemberModel 为顶层实体。
🔴 硬口径②:不支持自定义 RBAC(角色硬编码枚举)
权限角色是代码硬编码枚举,没有"管理员自定义角色""细粒度权限点配置""可视化权限矩阵"等通用 RBAC 能力。
- `marketAdmin` 角色的代码注释明确写着 `"before general manager RBAC exists"`——通用 RBAC 系统尚不存在,是规划项而非已交付项。
- 客户若要求"自定义角色/自定义权限点/按岗位授权",必须走产品规划沟通,不得承诺当前版本支持。
源码证据:`pkg/auth/manager_roles.go`、`modules/user/role_service.go`、`modules/group/const.go` 角色枚举硬编码;`modules/market/...` 注释原文。
🔴 硬口径③:多租户是同库逻辑隔离(space_id 过滤),非物理隔离
Octo 的多租户是逻辑隔离:所有 Space 的数据存于同一个数据库实例、同一套表、同一套 OpenSearch 索引,通过 `space_id` 字段在查询层强制过滤实现隔离。
- ❌ 不是独立数据库 / 独立 Schema / 独立集群 / 物理隔离部署。
- ✅ 跨 Space 泄露由多层 middleware + handler + 搜索层 fail-closed guard 显式封堵(历史漏洞 PR#713 已修复,见§四)。
- 客户若有"金融/政企强隔离"合规需求,必须采用"一客户一部署实例"方案(独立数据库+独立服务+独立OpenSearch),不是产品内置能力。
源码证据:`pkg/space/middleware.go`、`modules/messages_search/space_scope.go`(强制 term filter + fail-closed)、`pkg/authtree/authtree.go`。
§一、总览:用户-空间-群数据模型
1.1 实体层级关系
┌─────────────────────────────────────────────────────────────────┐
│ 系统平台(全局) │
│ - 全局唯一 superAdmin/admin/dashboardReader/marketAdmin │
│ - uk_* API Key(可绑定单一Space) │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Space A │ │ Space B │ ... │ Space N │ │
│ │ (租户隔离单元)│ │ (租户隔离单元)│ │ (租户隔离单元)│ │
│ │ │ │ │ │ │ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ │ User × Space │ │
│ │ │ Group G1 │ │ │ │ Group G3 │ │ │ = 多对多 │ │
│ │ │ (默认1:1 │ │ │ │ (默认1:1)│ │ │ space_member │ │
│ │ │ 归属Space)│ │ │ │ │ │ │ (uid,space_id)│ │
│ │ └──────────┘ │ │ └──────────┘ │ │ │ │
│ │ ┌──────────┐ │ │ │ │ │ │
│ │ │ Group G2 │ │ │ 外部群: │ │ │ │
│ │ │is_external│ │ │ is_external_ │ │ │ │
│ │ │ _group=1 │ │ │ group=1 时 │ │ │ │
│ │ │(跨Space) │ │ │ 跨Space成员 │ │ │ │
│ │ └──────────┘ │ │ source_space_id│ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ P2P 私聊:不归属任何 Space(直接 User ↔ User / Bot) │
└─────────────────────────────────────────────────────────────────┘
1.2 核心关系规则(源码确认)
| 关系 | 规则 | 源码位置 |
|---|---|---|
| User ⇄ Space | 多对多,通过 space_member 关联,唯一键 (space_id, uid)(非 uid) |
modules/space/model.go MemberModel |
| Group → Space | 默认 1:1 强归属,group.space_id NOT NULL;建群/加人强制校验 Space 成员资格 |
modules/group/sql/*_group_legacy01.sql、docs/external-group-design.md v1.1、PR#1167 |
| P2P 私聊 | 不绑定 Space(1:1 单聊没有 space_id,是独立通道) |
modules/group/ p2p 分支 |
| 跨 Space 协作 | 走"外部群"机制:邀请外部 Space 用户入群时,群标 is_external_group=1,外部成员标 source_space_id;跨第三方 Space 邀请天然不可能(通讯录隔离) |
docs/external-group-design.md |
| Bot | User Bot(bf_)/App Bot(app_) 均无 space_member 行(无 Space 座位),通过 token 作用域+authtree 路由守卫获得访问权(详见§三) |
modules/bot_api/auth.go、modules/app_bot/app_bot.go |
1.3 关键推论
- 一个用户可同时属于多个 Space(例如员工同时在公司Space和客户协作外部群所属Space),但每个Space下的身份/角色独立。
- Group 是 Space 内的沟通单元,默认不跨 Space;跨 Space 场景必须显式启用外部群并打标。
- P2P 私聊跳出 Space 边界,属于用户层直连,不参与 Space 成员过滤(但仍受用户层面的好友/黑名单/策略控制)。
§二、角色与权限体系
2.1 三套独立角色
① Space 成员角色(租户内身份)
| 值 | 角色 | 典型权限 |
|---|---|---|
| 0 | 普通成员 (member) | 日常消息、文件、建群(受Space策略) |
| 1 | 管理员 (admin) | 成员管理、Space设置、部分管理操作 |
| 2 | 拥有者 (owner) | Space所有权、最高权限、转让/解散 |
② 群成员角色(频道/群内身份)
| 值 | 角色 | 典型权限 |
|---|---|---|
| 0 | 普通群成员 | 收发消息、@人、文件 |
| 1 | 群主 (owner) | 群设置、成员管理、解散群 |
| 2 | 管理者 (admin) | 协助群主管理(加人/踢人/公告等) |
| — | bot_admin 标志 |
指定群内Bot管理权限(单独布尔位,非role值) |
③ 系统平台角色(全局身份)
| 值 | 角色 | 说明 |
|---|---|---|
| admin | 平台管理员 | 平台级管理权限 |
| superAdmin | 超级管理员 | 最高系统权限 |
| dashboardReader | 看板只读 | 受限角色:仅能读数据看板 |
| marketAdmin | 市场发布 | 受限角色:仅能发布到技能/插件市场。源码注释:"before general manager RBAC exists"(通用RBAC尚不存在) |
2.2 ❌ 不支持自定义 RBAC
- 以上三套角色均为代码硬编码枚举。
- 没有角色自定义、权限点粒度拆分、可视化权限矩阵、按岗位授权等能力。
- 售前/交付遇到"自定义角色""精细到API/功能点授权"类需求,必须标记为规划项,不得声称已支持。
marketAdmin注释"before general manager RBAC exists"是源码级明证——通用 RBAC 是未来规划,当前未交付。
2.3 鉴权执行:middleware + handler 双层检查
HTTP 请求
↓
[1] 认证层 (pkg/auth)
- 解析 token(user_token / bot_token / app_token / uk_* API Key / obo_*)
- 解析系统角色(token快照 + Redis `user_role:{uid}` TTL 60s)
↓
[2] Space 中间件 (pkg/space/middleware.go → SpaceMiddleware)
- 从请求路径/参数提取 space_id
- 查 Redis 成员缓存(正向命中60s / 否定命中30s)
- 未命中则回源 DB space_member 表
- 非成员→直接拒绝(anti-enumeration:统一返NOT_FOUND,见§四)
↓
[3] Handler 层细粒度检查
- Space owner/admin/member 具体操作级检查(散在各handler)
- 群角色(群主/管理员/普通/bot_admin)检查
- Bot authtree 路由级守卫(见§三)
↓
业务逻辑执行
2.4 缓存策略
| 缓存对象 | Redis Key | TTL | 位置 |
|---|---|---|---|
| Space 成员资格(正向) | SpaceMiddleware 内部缓存 | 60s | pkg/space/middleware.go |
| Space 成员资格(否定/非成员) | SpaceMiddleware 内部缓存 | 30s | pkg/space/middleware.go |
| 用户系统角色 | user_role:{uid} |
60s | pkg/auth/manager_roles.go |
| Bot Token tombstone(吊销) | tombstone 缓存 | 短TTL | modules/bot_api/ |
⚠️ 潜在风险:权限变更最长有 60s 传播延迟(移除某人管理员权限后,最坏情况下60s内仍可能执行管理操作)。高安全场景需评估。
§三、🔴 Bot 权限边界(安全章节)
🔴 售前必知:Bot 不是普通用户,不占 Space 座位,权限模型与人类用户根本不同。错误配置 Bot token 权限是最常见的集成安全事故源。
3.1 Bot 两类身份对照
| 维度 | User Bot(用户机器人) | App Bot(应用机器人) |
|---|---|---|
| Token 前缀 | bf_ |
app_ |
| 存储表 | robot 表 |
app_bot 表 |
| 创建入口 | BotFather | App管理后台/API |
| 典型用途 | 群内助手、技能专家、工作流Bot | 企业自建应用、OBO调用、第三方集成 |
| Scope 粒度 | 受群成员资格控制 | platform(全平台)/space(单Space)两档粗粒度 |
| Space 座位(space_member行) | ❌ 无 | ❌ 无 |
| 群端点访问 | ✅ 受active成员资格门控 | ❌ 完全拒绝群端点,仅限DM |
| DM端点访问 | ✅ | ✅ |
| OBO(on-behalf-of)支持 | 有限 | ✅ obo_* token |
3.2 Bot 关键安全规则(源码确认)
- Bot 无 `space_member` 行——Bot 不占 Space 座位,不占用"人头数",不通过 Space 成员表鉴权;Bot 在 Space 的存在/权限通过 token scope + authtree 路由守卫单独判定。
- App Bot 完全拒绝群端点——`pkg/authtree/authtree.go` TreeBotToken 分支对 App Bot 调用群相关 API 直接拒绝访问(返回权限错误),App Bot 只能用 DM(私聊)通道。售前若承诺"App Bot 群聊能力"是错误的。
- User Bot 受 active 群成员资格门控——User Bot 若要读/发某群消息,必须是该群的 active 成员(被拉入群且未被踢出)。被踢出后立即失去该群访问权(tombstone缓存短TTL内收敛)。
- 新成员看历史消息受 `group.allow_view_history_msg` 开关控制——群级别开关,决定新加入成员(含Bot)是否能看到入群前的历史消息。
- 文件URL签名安全:
- 下载URL:使用
ScopeUnscoped(对调用方提供的 key 签名),用于已授权后访问对象存储。 - 上传(presigned POST/PUT):
ScopeRouteGuard拒绝调用方自定义 object key——key 由服务端铸造(防路径穿越/覆盖/越权写)。 - 源码:
modules/bot_api/space_principal.go签名scope分支。
- ⚠️ 无标准 OAuth2 scope 体系:
- App Bot 只有
platform/space两档粗粒度,没有"只读消息""只发消息""管文件不管用户"这类细粒度scope。 - OBO(on-behalf-of)token(
obo_*前缀)代表某用户执行,权限等同于该用户+应用授权上下文,但也不是标准scope矩阵。 - 售前若被问"是否支持OAuth2细粒度scope授权"——答案是不支持标准scope体系,仅有platform/space粗粒度+OBO。
3.3 Bot 权限配置待确认项
- 群主/管理员 UI 侧"只发不读历史"开关是否暴露给 Bot 配置?(后端有
allow_view_history_msg但UI入口未确认) - User Bot 能否主动退群/被禁言?
- App Bot DM 对象范围(全平台任意用户/仅同Space)边界未逐端点验证。
§四、🔴 数据隔离机制
🔴 售前/交付必读:Octo 多租户不是物理隔离。所有隔离保证均来自代码层 guard,任何 guard 漏洞=跨租户数据泄露。本章节说明隔离机制及历史漏洞修复情况。
4.1 隔离模型:逻辑隔离(同库 + space_id 过滤)
| 维度 | 隔离方式 | 说明 |
|---|---|---|
| 数据库 | 同库同表 | 所有Space数据共用MySQL/PostgreSQL实例,业务表携带space_id字段 |
| 对象存储 | 同bucket(默认) | MinIO/S3按bucket划分业务域,不按Space分bucket;单bucket内按key前缀区分 |
| 消息队列 | 同topic | 消息投递不按Space物理隔离 |
| OpenSearch | 同索引 | 所有Space消息/文件在同一索引,查询强制注入space_id term filter |
| 缓存(Redis) | 同实例 | key中编码space_id/uid做逻辑区分 |
🔴 强合规场景(金融/政企/涉密) 必须采用"一客户一部署"方案:独立数据库实例 + 独立 OpenSearch 集群 + 独立 MinIO bucket(或独立MinIO)+ 独立服务副本。不要在同实例内承诺"强物理隔离"。
4.2 多层隔离Guard(fail-closed)
- SpaceMiddleware 边界守卫(`pkg/space/middleware.go`)
- 所有携带
space_id的请求必经中间件; - 未通过成员资格校验直接中断。
- OpenSearch 强制 term filter(`modules/messages_search/space_scope.go`)
- 搜索请求强制注入
payload.space_idterm filter; - 环境变量
OCTO_SEARCH_REQUIRE_SPACE_ID开启 fail-closed(未带space_id直接拒查); - 这是防数据泄露的最后一道关键防线——即便上层遗漏鉴权,搜索层也不会返回其他Space数据。
- Anti-enumeration(防枚举)(`authz.go`)
- 非成员访问任何Space内资源(查群/查用户/查消息)统一返回 NOT_FOUND(404)而不是 FORBIDDEN(403);
- 防止攻击者通过"403 vs 404"差异枚举Space/群/用户的存在性。
- API Key 绑定 Space(`uk_*`前缀)
uk_*API Key 创建时冻结绑定单一Space;- 使用该Key的所有请求自动限定在该Space内,无法访问其他Space。
- Authtree 路由守卫(`pkg/authtree/authtree.go`)
- Bot/App Bot 请求走树形路由权限判定;
- 注释明确:"Space remains the only security boundary"。
4.3 🔴 历史修复记录:PR#713 跨租户读漏洞
⚠️ 历史安全事件,用于向客户说明Octo团队的漏洞响应能力及当前状态。
- 问题:PR#713 代码审计发现 4 处跨租户读取漏洞——
/users/:uid端点的group_no查询参数未校验调用者是否在该 group 所属 Space 内,导致可通过构造参数读取其他Space的群信息。 - 修复:4处漏洞已逐条pin(加space_id断言)/strip(移除非必要跨Space查询路径),修复后authtree.go新增注释明确Space为唯一安全边界。
- 当前状态:已修复并入主干;本知识基于修复后版本源码确认。
- 交付说明:部署时必须使用包含PR#713修复的版本(v2026.09之后版本均含),不要部署过旧版本。
4.4 隔离机制已知局限
| 项 | 状态 | 说明 |
|---|---|---|
| group/thread 搜索 | 部分确认 | group/thread 搜索靠 channel_id 内编码Space(前缀/嵌入),非独立 space_id term;未逐行验证所有搜索路径 |
| OpenSearch mapping | 待确认 | payload.space_id 字段索引定义文件未逐行读到(由 space_scope.go 反证存在且被强制注入) |
| 文件URL签名 | 已确认 | 下载ScopeUnscoped、上传服务端铸造key,无路径穿越风险 |
| 跨Space邀请 | 已确认 | 通讯录隔离天然阻止跨第三方Space邀请 |
§五、SSO / 身份集成
5.1 已支持协议(源码确认)
| 协议 | Kind 常量 | 说明 |
|---|---|---|
| OIDC(OpenID Connect) | KindOIDC |
标准 OIDC:Discovery(.well-known/openid-configuration)+ id_token 验证 + JWKS 动态密钥拉取 |
| OAuth2(纯授权码) | KindOAuth2 |
标准 OAuth2 authorization_code 流程(无OIDC id_token时可用) |
- 内置对接 Aegis(明略自研 IdP),通过
DM_OIDC_AEGIS_*环境变量配置。 - 协议层不绑厂商,可对接任意标准 OIDC Provider:Google / Okta / Keycloak / Azure AD(通用OIDC档)/ Auth0 / 自研IdP 等。
- 自动关联:支持
AutoLinkByEmail/AutoLinkByPhone通过邮箱/手机号自动绑定现有账号。 - 新用户控制:
AllowNewUser开关控制SSO登录时是否自动创建新用户。
DM_OIDC_ENABLED=true/false
DM_OIDC_PROVIDER_ISSUER=
DM_OIDC_PROVIDER_CLIENT_ID=
DM_OIDC_PROVIDER_CLIENT_SECRET=
DM_OIDC_PROVIDER_REDIRECT_URI=
DM_OIDC_PROVIDER_SCOPES=openid,profile,email
DM_OIDC_PROVIDER_KIND=oidc|oauth2
DM_OIDC_AEGIS_*=
5.2 ❌ 明确不支持(售前硬口径)
| 能力 | 状态 | 说明 |
|---|---|---|
| SAML | ❌ 不支持 | SAML 仅在文档/测试中有对比性提及,无生产实现;客户要求SAML必须走定制/IdP代理(如用Keycloak做SAML→OIDC桥接) |
| LDAP / AD 直连 | ❌ 不支持 | 无LDAP bind/查询代码;需用LDAP→OIDC桥接(如Keycloak LDAP federation) |
| 企业微信扫码 | ❌ 独立集成不支持 | scanlogin_* 是Octo自有IM扫码登录(移动端App扫Web码),不是企业微信/钉钉/飞书扫码 |
| 钉钉扫码 | ❌ 独立集成不支持 | 同上 |
| 飞书扫码 | ❌ 独立集成不支持 | 同上 |
| SCIM 自动provisioning/deprovisioning | ❌ 不支持 | SCIM 仅1处设计对比文档提及,无实现;sync_worker.go 只做:① refresh_token 轮转 ② 失败吊销+踢线 ③ 实名claims 同步,不做账号生命周期自动创建/禁用/删除 |
| JIT Provisioning 细粒度 | 部分 | 仅有 AllowNewUser 开关+email/phone自动绑定,无基于claims的角色/部门映射 |
5.3 SSO 集成建议方案
客户IdP(SAML/LDAP/企微/钉钉/飞书)
│
▼
┌─────────────────┐ 标准OIDC ┌──────────────┐
│ 桥接IdP │─────────────▶│ Octo │
│ (Keycloak/Auth0/ │ │ modules/oidc │
│ Aegis/自研) │◀─────────────│ │
└─────────────────┘ JWKS/ └──────────────┘
▲ userinfo
│
└── 可在桥接层做:SAML/LDAP→OIDC、SCIM provisioning、
企微/钉钉/飞书OAuth映射、部门/角色claims转换
售前口径:Octo 原生支持标准 OIDC/OAuth2 SSO;对接 SAML/LDAP/企微/钉钉/飞书等需通过 Keycloak 等标准IdP做协议桥接,Octo 团队可提供桥接方案但不承诺原生开箱即用。
§六、待确认项(to-follow-up)
以下问题在本次源码确认中未得到完整答案,列入后续复核清单,不得在客户面前做确定性表述。
| 编号 | 待确认项 | 优先级 | 复核方式 |
|---|---|---|---|
| AUTH-01 | 三套角色(Space/Group/系统)对每个REST端点的完整权限矩阵(当前散在各handler,无集中定义) | P1 | 逐handler梳理+生成权限矩阵文档 |
| AUTH-02 | 群主/管理员 UI 是否暴露"Bot只发不读历史"开关(后端 allow_view_history_msg 字段已存在但UI入口未确认) |
P2 | Web前端代码走查+产品管家确认 |
| AUTH-03 | OpenSearch mapping 中 payload.space_id 字段的索引定义、analyzer、routing策略(由space_scope.go反证存在但mapping文件未逐行读) |
P2 | 查 octo-search-indexer mapping JSON + 索引模板 |
| AUTH-04 | group/thread 搜索路径通过 channel_id 编码 Space 的具体机制(前缀位宽/碰撞风险/跨Space拼接攻击面) | P2 | 查 messages_search 模块 channel_id 解析逻辑 |
| AUTH-05 | App Bot DM 对象范围边界:能否对平台任意用户发DM?是否限同Space? | P2 | 逐endpoint验证+集成测试 |
| AUTH-06 | 权限变更缓存60s延迟是否有"强制失效"管理API(踢人/降权时即时收敛) | P2 | 查 Redis 缓存失效逻辑 |
| AUTH-07 | User Bot 主动退群/被禁言/被永久封禁的token生命周期 | P3 | bot_api 模块 state machine |
§七、版本记录
| 版本 | 日期 | 变更说明 | 作者 |
|---|---|---|---|
| v0.1 | 2026-09-22 | 初版。基于Q10源码确认级回答(22条确认结论),建立跨模块权限/鉴权/隔离/SSO全景文档。含三条售前安全硬口径、Bot安全边界、数据隔离机制与PR#713历史修复、SSO支持矩阵、7项待确认清单。 | twb-knowledge-octo |
附录A:源码证据索引
| 结论域 | 核心文件 |
|---|---|
| Space模型/成员 | modules/space/model.go |
| Space中间件/缓存 | pkg/space/middleware.go |
| 群模型/角色/外部群 | modules/group/const.go、modules/group/sql/*_group_legacy01.sql、docs/external-group-design.md |
| 系统角色 | pkg/auth/manager_roles.go、modules/user/role_service.go |
| Bot认证/Principal | modules/bot_api/auth.go、modules/bot_api/space_principal.go |
| App Bot | modules/app_bot/app_bot.go |
| 路由级Authtree守卫 | pkg/authtree/authtree.go |
| OpenSearch space_id强制过滤 | modules/messages_search/space_scope.go、modules/messages_search/authz.go |
| 反枚举 | 各模块 authz.go |
| OIDC/OAuth2 SSO | modules/oidc/{provider,provider_factory,config,sync_worker}.go |
| PR#713 跨租户漏洞修复 | pkg/authtree/authtree.go 注释 + git history |
附录B:术语对照
| 术语 | 含义 | 易混点 |
|---|---|---|
| Space | 租户隔离唯一单元 = organization = workspace | 不是"组织下的子空间" |
| Group | 群/频道,默认归属一个Space | 不是"用户组(权限组)" |
User Bot (bf_) |
群聊Bot,需被拉入群才能访问群 | 不是App Bot |
App Bot (app_) |
应用级Bot,DM-only,有platform/space两档scope | 不能进群 |
OBO (obo_*) |
on-behalf-of,代表用户调用的token | 不是独立Bot类型 |
uk_* |
API Key,冻结绑定单一Space | 不是user token |
is_external_group |
外部群标志(跨Space协作) | 不是"外部分享链接群" |
allow_view_history_msg |
新成员是否看入群前历史 | 群级别开关 |
| ScopeUnscoped | 文件下载URL签名模式(对caller-named key签名) | 不是上传用 |
| ScopeRouteGuard | 上传presigned签名守卫(服务端铸造key) | 防路径穿越 |