# 预签名URL功能 — 开发使用手册

> 版本: v1.1
> 更新时间: 2026-06-04
> 适用范围: BladeX v4.9.0
> 变更说明：@OssUrl 注解升级为全服务可用，支持多租户独立 OSS 配置

---

## 一、功能概述

预签名 URL 机制用于修复"图片下载或浏览路径未授权访问"漏洞。核心思路：

- 将对象存储 bucket 设为**私有**，禁止匿名访问
- 后端在 API 响应中自动将文件路径转为**带过期时间的临时访问 URL**
- 前端无需任何改动，拿到的仍然是一个可直接访问的 URL，只是从永久直链变为临时链接
- **所有微服务均可使用** `@OssUrl` 注解（不仅限于 blade-resource）

### 工作原理

```
数据库存储: upload/20250722/xxx.png          ← 永久不变的对象路径
    ↓ Jackson 序列化（@OssUrl 注解触发）
API 响应:   http://minio:9800/ieslab/upload/20250722/xxx.png?X-Amz-Expires=1800&...  ← 临时预签名URL
```

### 支持的存储后端

| 存储类型 | 签名方式 | 状态 |
|---------|---------|------|
| MinIO | `getPresignedObjectUrl` | 已实现 |
| 阿里云 OSS | `generatePresignedUrl` | 已实现 |
| 腾讯云 COS | `generatePresignedUrl` | 已实现 |
| 华为云 OBS | `createTemporarySignature` | 已实现 |
| Amazon S3 | `generatePresignedUrl` | 已实现 |
| 七牛云 | `privateDownloadUrl` | 已实现 |
| 本地存储 | 后端代理 `/oss/endpoint/view-file` | 已实现 |

---

## 二、使用注解

### 2.1 基本用法

在实体 VO 的文件路径字段上添加 `@OssUrl` 注解：

```java
import org.springblade.core.oss.annotation.OssUrl;

public class NoticeVO {

    private String title;

    // 使用全局默认过期时间（由 oss.presigned-expires 配置）
    @OssUrl
    private String imagePath;
}
```

**效果**：序列化为 JSON 时，`imagePath` 的值会自动从对象路径转为预签名 URL。

```json
// 序列化前（数据库值）
{ "imagePath": "upload/20250722/xxx.png" }

// 序列化后（API响应）
{ "imagePath": "http://10.77.65.2:9800/ieslab/upload/20250722/xxx.png?X-Amz-Algorithm=...&X-Amz-Expires=1800&..." }
```

### 2.2 自定义过期时间

```java
// 1小时有效
@OssUrl(expires = 3600)
private String imagePath;

// 24小时有效（适合报表等长期附件）
@OssUrl(expires = 86400)
private String reportFile;

// 使用全局默认（等价于不加参数）
@OssUrl
private String avatarPath;
```

### 2.3 旧数据兼容

数据库中可能存在多种格式的旧数据，**无需迁移**，注解会自动识别：

| 数据库存储值 | 处理方式 |
|-------------|---------|
| `upload/20250722/xxx.png` | 直接使用，生成预签名 URL |
| `http://10.77.65.2:9800/ieslab/upload/20250722/xxx.png` | 自动提取 `upload/20250722/xxx.png`，再生成预签名 URL |
| `https://bucket.oss-cn-hangzhou.aliyuncs.com/upload/xxx.png` | 自动提取 `upload/xxx.png`，再生成预签名 URL |
| `null` 或空字符串 | 原样返回，不做处理 |

### 2.4 生效范围

- 所有依赖 `blade-resource-api` 的微服务均可使用 `@OssUrl`（如 blade-system、blade-desk 等）
- 仅依赖 `blade-starter-oss` 的服务使用 `DefaultOssPresignHandler`（共享 OSS 场景）
- 注解由 `blade-starter-oss` 提供（`org.springblade.core.oss.annotation.OssUrl`）

### 2.5 注意事项

1. **标注位置**：建议标注在 **VO 类**（而非 Entity 类）上，避免影响内部逻辑中的路径使用
2. **降级安全**：即使签名生成异常，序列化器会降级返回原值，**不会导致 API 报错**
3. **前端无感知**：前端拿到的仍然是 URL 字符串，无需任何改动。但 URL 有过期时间，过期后需重新请求接口获取新 URL
4. **兼容 @Sensitive**：`@OssUrl` 字段同时标注 `@Sensitive` 时，会优先执行脱敏，不生成预签名 URL

---

## 三、配置说明

### 3.1 全局过期时间

在 Nacos 配置（`blade.yaml` 或各服务配置）中设置：

```yaml
oss:
  presigned-expires: 1800   # 预签名URL默认过期时间（秒），默认30分钟
```

取值优先级：`@OssUrl(expires=N)` > `oss.presigned-expires` > 默认 1800 秒

### 3.2 开箱即用

该功能**无需额外配置**，自动初始化：

- 引入 `blade-resource-api` 依赖的服务：自动使用 `MultiTenantOssPresignHandler`（Cache+Feign 多租户）
- 仅引入 `blade-starter-oss` 的服务：自动使用 `DefaultOssPresignHandler`（共享 OSS 模板）

### 3.3 多租户配置

各租户的 OSS 配置由 blade-resource 服务通过数据库管理（`blade_oss` 表），消费服务通过 Feign+Cache 自动获取，**无需在消费服务端配置**。

配置获取链路：

```
消费服务请求 → OssConfigCache.getConfig(tenantId)
  → 缓存命中 → 直接返回 OssPresignConfig（无网络调用）
  → 缓存未命中 → Feign 调用 blade-resource → 写入缓存 → 返回
```

---

## 四、修改 MinIO Bucket 策略

代码部署后，**必须**将 MinIO bucket 从公开读改为私有，否则原始直链仍然可访问，漏洞未真正修复。

### 4.1 通过 MinIO 控制台修改

1. 浏览器访问 MinIO 控制台：`http://10.77.65.2:9800`
2. 登录管理账号（Access Key / Secret Key）
3. 进入 **Buckets** → 选择目标 bucket（如 `ieslab`）
4. 点击 **Access Rules** 标签页
5. 删除或修改 `readwrite` / `readonly` 规则：
   - 将 `*` (Prefix) 的 Access 从 `readwrite` 改为 **`private`**
   - 或者直接删除该公开访问规则
6. 保存

### 4.2 通过 MinIO Client (mc) 命令行修改

```bash
# 配置 MinIO 连接
mc alias set myminio http://10.77.65.2:9800 <ACCESS_KEY> <SECRET_KEY>

# 查看当前策略
mc anonymous get myminio/ieslab

# 设置为私有（禁止匿名访问）
mc anonymous set none myminio/ieslab

# 验证：匿名访问应返回 403
curl -I http://10.77.65.2:9800/ieslab/upload/20250722/xxx.png
# 期望: HTTP/1.1 403 Forbidden
```

### 4.3 验证步骤

修改策略后，按以下步骤验证：

1. **匿名访问应失败**：用无痕窗口直接访问文件 URL，应返回 403
   ```
   http://10.77.65.2:9800/ieslab/upload/20250722/xxx.png → 403 Forbidden
   ```

2. **API 响应应包含签名**：登录系统，调用包含 `@OssUrl` 字段的接口，检查返回的 URL 是否包含签名参数
   ```json
   { "imagePath": "http://10.77.65.2:9800/ieslab/upload/...?X-Amz-Expires=1800&X-Amz-Signature=..." }
   ```

3. **签名 URL 应可访问**：复制 API 响应中的预签名 URL，在浏览器中直接访问，应能正常显示文件

4. **过期后应不可访问**：等待过期时间过后，再次访问同一 URL，应返回 403

---

## 五、架构参考

### 5.1 核心类清单

#### blade-starter-oss（核心模块）

| 类名 | 职责 |
|------|------|
| `@OssUrl` | 注解，标注在字段上，声明过期时间 |
| `OssUrlSerializer` | Jackson 序列化器，核心转换逻辑 |
| `OssUrlJacksonModule` | Jackson Module，注册 String 类型拦截器 |
| `OssUrlSerializerHolder` | 静态持有者，桥接 Spring Bean |
| `OssUrlPresignProvider` | 函数式接口，生成预签名 URL |
| `OssPresignHandler` | 接口：`presignedUrl()` + `getGlobalExpires()` |
| `DefaultOssPresignHandler` | 默认实现，使用 Spring 管理的单 OssTemplate |
| `OssPresignInitializer` | `@PostConstruct` 桥接 + `registerModule` |

#### blade-resource-api（API 模块）

| 类名 | 职责 |
|------|------|
| `OssPresignConfig` | DTO，Feign 传输各租户 OSS 凭证（不含 @Sensitive） |
| `IOssPresignClient` | Feign 接口，获取租户 OSS 配置 |
| `IOssPresignClientFallback` | Feign 降级 |
| `OssConfigCache` | Cache-Aside 缓存，按 tenantId 缓存 OSS 配置 |
| `MultiTenantOssPresignHandler` | 多租户实现，按租户缓存 OssTemplate |
| `OssTemplateFactory` | 工厂类，按 category 创建 7 种 OssTemplate |
| `OssPresignConfiguration` | `@AutoConfigureBefore` 抢先注册多租户实现 |

#### blade-resource（服务模块）

| 类名 | 职责 |
|------|------|
| `OssPresignClient` | Feign 实现（implements IOssPresignClient），从 OssBuilder 获取租户原始凭证 |

### 5.2 调用链路

#### 引入 blade-resource-api 的服务（多租户模式）

```
前端请求 API
  → Controller 返回 VO（含 @OssUrl 字段）
    → Jackson 序列化触发 OssUrlSerializer（OssUrlJacksonModule 注册）
      → extractObjectKey() 兼容旧数据
        → OssUrlSerializerHolder → OssPresignHandler
          → MultiTenantOssPresignHandler
            → AuthUtil.getTenantId()
            → OssConfigCache.getConfig(tenantId)（缓存命中则无网络调用）
            → OssTemplateFactory.create(category, props, rule)
            → template.presignedUrl(objectKey, expireSeconds)
              → 返回预签名 URL（纯 CPU 计算，微秒级）
```

#### 仅引入 blade-starter-oss 的服务（默认模式）

```
前端请求 API
  → Jackson 序列化触发 OssUrlSerializer
    → OssUrlSerializerHolder → OssPresignHandler
      → DefaultOssPresignHandler
        → Spring 管理的 OssTemplate（自动配置的单模板）
        → template.presignedUrl(objectKey, expireSeconds)
```

### 5.3 @ConditionalOnMissingBean 桥接机制

```
OssPresignConfiguration（blade-resource-api）
  @AutoConfigureBefore(OssConfiguration.class)
  @Bean OssPresignHandler → MultiTenantOssPresignHandler  ← 抢先注册

OssConfiguration（blade-starter-oss）
  @Bean @ConditionalOnMissingBean(OssPresignHandler.class)
  → DefaultOssPresignHandler                                ← API 模块已注册时跳过
```

### 5.4 降级策略

| 场景 | 行为 |
|------|------|
| 字段值为空 | 原样返回空值 |
| Holder 未初始化（OssPresignHandler 未注册） | 原样返回字段值 |
| Feign 调用失败（blade-resource 不可用） | 使用缓存数据，缓存也无则抛异常 |
| 签名生成异常 | 原样返回字段值，API 正常响应 |

---

## 六、常见问题

### Q1: 其他服务（如 blade-system）的 VO 能用 @OssUrl 吗？

**可以。** `@OssUrl` 注解定义在 `blade-starter-oss`（核心模块），所有微服务均可使用。只需确保服务引入了 `blade-resource-api` 依赖，即可获得多租户支持。未引入 `blade-resource-api` 的服务使用默认实现。

### Q2: 前端需要改动吗？

不需要。前端拿到的仍然是 URL 字符串，直接使用即可。但需注意：
- URL 有过期时间，过期后需重新请求接口
- 如果前端有缓存机制（如列表页缓存），缓存过期后 URL 可能失效，需重新获取

### Q3: 性能有影响吗？

预签名 URL 生成是纯 CPU 的 HMAC 计算和字符串拼接，不涉及网络 IO（缓存命中后），单次约微秒级。列表接口 100 条记录约增加 0.1ms，可忽略。

首次请求某租户时有一次 Feign 调用（获取 OSS 配置），后续请求命中缓存后无网络开销。

### Q4: 本地存储（LocalFile）模式下怎么工作？

本地存储没有签名机制，`presignedUrl` 返回的是后端代理 URL（如 `/oss/endpoint/view-file?fileName=xxx`），该接口经过网关认证后由 `OssEndpoint` 读取本地文件返回。安全性由网关认证保证。

### Q5: 租户使用了自定义 OSS 配置怎么办？

`MultiTenantOssPresignHandler` 通过 Feign 获取各租户独立的 OSS 配置，参照 `OssDataRule` 逻辑自动判断是否使用多租户路径模式（文件路径加 tenantId 前缀）。自定义 OSS 的租户使用独立凭证生成预签名 URL。

### Q6: 与 @Sensitive 注解同时使用会怎样？

`OssUrlSerializer` 实现了 `ContextualSerializer`，在 `createContextual()` 中检测到 `@Sensitive` 注解时，会委托给 `SensitiveSerializer` 处理（脱敏优先），不生成预签名 URL。

---

## 附录：版本变更记录

| 版本 | 日期 | 变更内容 |
|------|------|---------|
| v1.0 | 2026-06-03 | 初始版本，仅 blade-resource 服务可用 |
| v1.1 | 2026-06-04 | 升级为全服务可用，支持多租户独立 OSS 配置，Cache+Feign 架构 |
