diff --git a/src/server/minio.ts b/src/server/minio.ts index 008a2d6..c89f66f 100644 --- a/src/server/minio.ts +++ b/src/server/minio.ts @@ -1,7 +1,7 @@ import 'server-only' import { Client } from 'minio'; -// 初始化 MinIO 客户端 +// 内部客户端:用于 statObject、removeObject 等服务端直连操作,以及 presignedPostPolicy export const minioClient = new Client({ endPoint: process.env.MINIO_ENDPOINT || 'localhost', port: parseInt(process.env.MINIO_API_PORT || '9000'), @@ -10,6 +10,32 @@ export const minioClient = new Client({ secretKey: process.env.MINIO_ROOT_PASSWORD || '', }); +/** + * 公网客户端:用于生成 presigned GET/PUT URL + * + * AWS Signature V4 会将 Host 头纳入签名计算,因此 presigned GET/PUT URL + * 必须使用与浏览器访问一致的公网地址生成,否则签名校验失败。 + * 而 POST Policy 不含 Host 签名,只需替换返回的 URL 地址即可。 + */ +function createPresignClient(): Client { + const serverUrl = process.env.MINIO_SERVER_URL; + if (!serverUrl) return minioClient; + + const parsed = new URL(serverUrl); + const useSSL = parsed.protocol === 'https:'; + const port = parsed.port ? parseInt(parsed.port) : (useSSL ? 443 : 80); + + return new Client({ + endPoint: parsed.hostname, + port, + useSSL, + accessKey: process.env.MINIO_ROOT_USER || '', + secretKey: process.env.MINIO_ROOT_PASSWORD || '', + }); +} + +const presignClient = createPresignClient(); + export const BUCKET_NAME = process.env.MINIO_BUCKET || 'app-files'; /** @@ -166,6 +192,9 @@ export async function generatePresignedPostPolicy( // 精确匹配 policy.setContentType(allowedContentType); } + } else { + // 未指定类型限制时,允许任意 Content-Type(客户端上传时会设置此字段,Policy 中必须声明) + policy.policy.conditions.push(['starts-with', '$Content-Type', '']); } if (allowOriginalFilename) { @@ -255,8 +284,8 @@ export async function generatePresignedGetObject( } } - // 生成预签名 URL - const url = await minioClient.presignedGetObject( + // 使用公网客户端生成预签名 URL,确保 V4 签名中的 Host 与浏览器访问地址一致 + const url = await presignClient.presignedGetObject( BUCKET_NAME, objectName, expirySeconds, @@ -264,7 +293,7 @@ export async function generatePresignedGetObject( ); return { - url: replaceUrlBase(url), + url, expiresIn: expirySeconds, }; } catch (error) {