# 技术设计 — 预签名URL方案修复文件路径未授权访问漏洞

> Spec: oss-presigned-url
> 版本: v2.0
> 更新时间: 2026-06-04
> 设计参考: docs/plans/2026-06-03-oss-presigned-url-design_v2.0.md
> 变更说明：从 blade-resource 单服务架构升级为多服务多租户架构

## 架构概述

### 整体思路

核心模块（`blade-starter-oss`）定义接口、注解、序列化器和默认实现。API 模块（`blade-resource-api`）通过 Cache+Feign 提供多租户实现，覆盖默认实现。消费服务只需依赖 `blade-resource-api`，即可在任意微服务中使用 `@OssUrl` 注解。参照 DataScope 模式（`ScopeModelHandler` → `DataScopeModelHandler`）。

```
所有微服务均可使用 @OssUrl
  → Jackson 序列化触发 OssUrlSerializer（OssUrlJacksonModule 注册）
    → OssUrlSerializerHolder.getPresignProvider()
      → OssPresignHandler（接口）
        ├── DefaultOssPresignHandler（默认实现，使用 Spring 管理的单 OssTemplate）
        │   ← @ConditionalOnMissingBean，无多租户需求时自动生效
        └── MultiTenantOssPresignHandler（多租户实现，在 blade-resource-api 中）
            ← @AutoConfigureBefore 抢先注册，覆盖默认实现
            → OssConfigCache.getConfig(tenantId)（Cache-Aside + Feign）
            → OssTemplateFactory.create(category, props, rule)（本地创建模板）
            → template.presignedUrl(objectKey, expires)
```

### 模块归属

| 层次 | 模块 | 职责 |
|------|------|------|
| 框架层 | blade-starter-oss | 注解、序列化器、OssPresignHandler 接口、默认实现、初始化器 |
| API 层 | blade-resource-api | Feign 接口、缓存、多租户实现、OssTemplateFactory |
| 服务层 | blade-resource | Feign 端点实现（OssPresignClient） |

### @ConditionalOnMissingBean 桥接

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

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

## 模块设计

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

#### 1.1 OssProperties 配置

**文件**: `blade-starter-oss/.../props/OssProperties.java`

```java
private Integer presignedExpires = 1800;  // 预签名URL默认过期时间（秒）
```

#### 1.2 OssTemplate 接口扩展

**文件**: `blade-starter-oss/.../OssTemplate.java`

```java
String presignedUrl(String fileName, int expireSeconds);
String presignedUrl(String bucketName, String fileName, int expireSeconds);
```

#### 1.3 各模板实现（7 种）

| 模板 | SDK 调用 |
|------|---------|
| MinioTemplate | `client.getPresignedObjectUrl()` |
| AliossTemplate | `ossClient.generatePresignedUrl()` |
| TencentCosTemplate | `cosClient.generatePresignedUrl()` |
| HuaweiObsTemplate | `obsClient.createTemporarySignature()` |
| S3Template | `client.generatePresignedUrl()` |
| QiniuTemplate | `auth.privateDownloadUrl()` |
| LocalFileTemplate | 返回后端代理 URL |

#### 1.4 OssEnum 扩展

**文件**: `blade-starter-oss/.../enums/OssEnum.java`

新增按 category 查找方法：
```java
public static OssEnum of(int category) { ... }
```

#### 1.5 注解与序列化器（5 个文件）

**包路径**: `org.springblade.core.oss.annotation`

| 文件 | 职责 |
|------|------|
| `OssUrl.java` | 注解，声明过期时间 |
| `OssUrlSerializer.java` | `@JacksonStdImpl` + `ContextualSerializer`，核心转换逻辑，兼容 @Sensitive |
| `OssUrlJacksonModule.java` | `SimpleModule`，注册 `OssUrlSerializer.instance` 拦截 String 类型 |
| `OssUrlSerializerHolder.java` | 静态持有者，桥接 Spring Bean（presignProvider + globalExpires） |
| `OssUrlPresignProvider.java` | `@FunctionalInterface`，生成预签名 URL |

**关键**：`OssUrlSerializer.createContextual()` 检测 `@OssUrl` 注解，同时兼容 `@Sensitive`（委托给 `SensitiveSerializer.instance.createContextual()`）。

#### 1.6 OssPresignHandler 接口 + DefaultOssPresignHandler

**包路径**: `org.springblade.core.oss.handler`

```java
public interface OssPresignHandler {
    String presignedUrl(String objectKey, int expireSeconds);
    int getGlobalExpires();
}
```

`DefaultOssPresignHandler`：使用 auto-configured `OssTemplate` 单模板，适用于共享 OSS 场景。

#### 1.7 OssPresignInitializer

**包路径**: `org.springblade.core.oss.config`

```java
public class OssPresignInitializer implements WebMvcConfigurer {
    @PostConstruct init() → 桥接 OssPresignHandler 到 OssUrlSerializerHolder
    extendMessageConverters() → 注册 OssUrlJacksonModule 到 write ObjectMapper
}
```

#### 1.8 OssConfiguration Bean 注册

```java
@Bean @ConditionalOnMissingBean(OssPresignHandler.class)
public OssPresignHandler ossPresignHandler(OssTemplate, OssProperties) {
    return new DefaultOssPresignHandler(ossTemplate, ossProperties);
}

@Bean @ConditionalOnBean(OssPresignHandler.class)
public OssPresignInitializer ossPresignInitializer(OssPresignHandler) { ... }
```

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

#### 2.1 OssPresignConfig DTO

**包路径**: `org.springblade.resource.pojo.dto`

DTO 字段：category, endpoint, transformEndpoint, accessKey, secretKey, bucketName, appId, region。

**关键**：不含 `@Sensitive` 注解，Feign 传输原始凭证值。

#### 2.2 Feign 接口 + 降级

**包路径**: `org.springblade.resource.feign`

- `IOssPresignClient` — `@FeignClient(APPLICATION_RESOURCE_NAME, fallback = IOssPresignClientFallback.class)`
  - `GET /feign/client/oss-presign/config` → `R<OssPresignConfig> getOssPresignConfig()`
- `IOssPresignClientFallback` — `@Component`，返回 `R.fail()`

#### 2.3 OssConfigCache

**包路径**: `org.springblade.resource.cache`

静态工具类，`SpringUtil.getBean(IOssPresignClient.class)` 延迟获取 Feign 客户端。Cache-Aside：`CacheUtils.get()` → 未命中 → Feign 调用 → `CacheUtils.put()`。

#### 2.4 MultiTenantOssPresignHandler

**包路径**: `org.springblade.resource.handler`

```java
presignedUrl(objectKey, expireSeconds):
  → AuthUtil.getTenantId()
  → ConcurrentHashMap<tenantId, OssTemplate> 缓存
  → OssConfigCache.getConfig(tenantId)（缓存命中则无网络调用）
  → OssTemplateFactory.create(category, props, new BladeOssRule(useTenantMode))
  → template.presignedUrl(objectKey, expireSeconds)
```

OssDataRule 判断逻辑：
```java
boolean useTenantMode = ossProperties.getTenantMode()
    && config.getEndpoint().equals(ossProperties.getEndpoint())
    && config.getAccessKey().equals(ossProperties.getAccessKey());
```

#### 2.5 OssTemplateFactory

**包路径**: `org.springblade.resource.handler`

根据 `OssEnum.category` + `OssProperties` 创建对应的 `OssTemplate`。SDK 依赖以 `provided` scope 引入（运行时由 blade-resource 提供）。

#### 2.6 OssPresignConfiguration

```java
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(OssConfiguration.class)
public class OssPresignConfiguration {
    @Bean
    public OssPresignHandler ossPresignHandler(OssProperties) {
        return new MultiTenantOssPresignHandler(ossProperties);
    }
}
```

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

#### 3.1 OssPresignClient（Feign 实现）

**包路径**: `org.springblade.resource.feign`

参照 `OssClient implements IOssClient` 模式：

```java
@NonDS @RestController @AllArgsConstructor
public class OssPresignClient implements IOssPresignClient {
    private final OssBuilder ossBuilder;

    @Override @GetMapping(GET_CONFIG)
    public R<OssPresignConfig> getOssPresignConfig() {
        Oss oss = ossBuilder.getOss(tenantId, StringPool.EMPTY);
        // 映射字段到 OssPresignConfig
        return R.data(config);
    }
}
```

注：`Oss` 实体的 `accessKey`/`secretKey` 不受 `@Sensitive` 影响（`@Sensitive` 仅作用于 Jackson 序列化，这里是直接读取实体字段值）。

## 数据流图

### 引入 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)
```

## 影响分析

### 新增文件

| 文件 | 模块 |
|------|------|
| `blade-starter-oss/.../annotation/OssUrl.java` | blade-starter-oss |
| `blade-starter-oss/.../annotation/OssUrlSerializer.java` | blade-starter-oss |
| `blade-starter-oss/.../annotation/OssUrlJacksonModule.java` | blade-starter-oss |
| `blade-starter-oss/.../annotation/OssUrlSerializerHolder.java` | blade-starter-oss |
| `blade-starter-oss/.../annotation/OssUrlPresignProvider.java` | blade-starter-oss |
| `blade-starter-oss/.../handler/OssPresignHandler.java` | blade-starter-oss |
| `blade-starter-oss/.../handler/DefaultOssPresignHandler.java` | blade-starter-oss |
| `blade-starter-oss/.../config/OssPresignInitializer.java` | blade-starter-oss |
| `blade-resource-api/.../pojo/dto/OssPresignConfig.java` | blade-resource-api |
| `blade-resource-api/.../feign/IOssPresignClient.java` | blade-resource-api |
| `blade-resource-api/.../feign/IOssPresignClientFallback.java` | blade-resource-api |
| `blade-resource-api/.../cache/OssConfigCache.java` | blade-resource-api |
| `blade-resource-api/.../handler/MultiTenantOssPresignHandler.java` | blade-resource-api |
| `blade-resource-api/.../handler/OssTemplateFactory.java` | blade-resource-api |
| `blade-resource-api/.../config/OssPresignConfiguration.java` | blade-resource-api |
| `blade-resource/.../feign/OssPresignClient.java` | blade-resource |

### 修改文件

| 文件 | 模块 | 改动 |
|------|------|------|
| `OssProperties.java` | blade-starter-oss | +presignedExpires 字段 |
| `OssTemplate.java` | blade-starter-oss | +presignedUrl 接口方法 |
| `OssEnum.java` | blade-starter-oss | +of(category) 方法 |
| `OssConfiguration.java` | blade-starter-oss | +2 个 Bean 注册 |
| `MinioTemplate~LocalFileTemplate` | blade-starter-oss | 各 +presignedUrl 实现 |
| `Attach.java` | blade-resource-api | import 路径变更 |
| `pom.xml` | blade-resource-api | +6 个 SDK provided 依赖 |

### 删除文件

| 文件 | 模块 | 原因 |
|------|------|------|
| `blade-resource-api/.../annotation/OssUrl.java` 等 5 个 | blade-resource-api | 迁入 blade-starter-oss |
| `blade-resource/.../config/OssUrlSerializerConfiguration.java` | blade-resource | 被 OssPresignInitializer 取代 |

### 不影响的范围

- 不修改任何 Controller、Service 业务逻辑
- 不修改数据库表结构或数据
- 不修改前端代码
- 不修改网关安全配置
- `@OssUrl` 注解按需标注到具体字段，不影响未标注的字段
