From ff9b5918fd2198cdb4653aa3ce56ddb17bb09d6f Mon Sep 17 00:00:00 2001 From: liuyh Date: Wed, 18 Mar 2026 11:27:52 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E5=86=B3MinIO=20=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF=E7=9B=B4=E4=BC=A0=E6=9E=B6=E6=9E=84=E5=9C=A8=E5=8F=8D?= =?UTF-8?q?=E5=90=91=E4=BB=A3=E7=90=86=E9=83=A8=E7=BD=B2=E4=B8=8B=E9=A2=84?= =?UTF-8?q?=E7=AD=BE=E5=90=8D=20URL=20=E5=A4=B1=E6=95=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/minio.ts | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) 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) {