NodeJS反向代理AWS S3预签名URL遇SignatureDoesNotMatch错误
我尝试用NodeJS脚本做反向代理,通过生成S3对象的预签名URL来服务静态站点:用户访问build.localhost:8080时,子域名build对应S3里的build文件夹,代理加载该文件夹下的index.html(该文件夹已上传npm run build生成的构建文件)。但运行代码后出现SignatureDoesNotMatch错误,附上代码和错误信息,请求排查。
代码
const express = require('express'); const httpProxy = require('http-proxy'); const AWS = require('aws-sdk'); var credentials = new AWS.SharedIniFileCredentials({profile: 'saml'}); AWS.config.credentials = credentials; const s3 = new AWS.S3(); function getPreSignedUrl(key){ const myBucket = 'test-react-bucket-dev'; const signedUrlExpireSeconds = 60 * 5; return new Promise((resolve, reject) => { s3.getSignedUrl('getObject', { Bucket: myBucket, Key: key, Expires: signedUrlExpireSeconds }, (err, url) => { if (err) reject(err); else resolve(url); }); }); } const app = express(); const PORT = 8080; const proxy = httpProxy.createProxyServer(); app.get('*', async (req, res) => { const hostname = req.hostname; const subdomain = hostname.split('.')[0]; let key = subdomain + (req.path === '/' ? '/index.html' : req.path); try { const resolvesTo = await getPreSignedUrl(key); proxy.web(req, res, { target: resolvesTo, changeOrigin: true }); } catch (error) { console.error("Error:", error); res.status(500).send("Error proxying the request"); } }); proxy.on('proxyReq', (proxyReq, req, res) => { const url = req.url; req.rawHeaders = [] if (url === '/') proxyReq.path += 'index.html'; console.log(req.rawHeaders) }); app.listen(PORT, () => console.log(`Reverse Proxy Running..${PORT}`));
错误信息
<Error class="hidden"> <link type="text/css" id="dark-mode" rel="stylesheet" href=""/> <style type="text/css" id="dark-mode-сustom-style"/> <style type="text/css" id="dark-mode-theme-changer-style"/> <Code>SignatureDoesNotMatch</Code> <Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message> <AWSAccessKeyId>ASIAR---REDACTED---EIYB</AWSAccessKeyId> <StringToSign>GET 1707814358 x-amz-security-token:FwoGZXIvYXdzEKf//////////wEaDDy3nYkyTaxu1hEt6iLnAcXSYnhu0Oq0K7AeEc/quuBjIyqMfZON6gbHFZ3/ZLi7yDwPtdlKsiacTDSKiav1XuQI1WP+aXePJ+RwgtJesC177Jmfg=/index.html /test-react-bucket-jk/build/index.html</StringToSign> <SignatureProvided>mlC4fkhNSMavJk8R45i6T+QRBr8=</SignatureProvided> <StringToSignBytes>47 45 54 0a 0a 0a 31 37 30........</StringToSignBytes> <RequestId>S3T66NPSHTKCDAQ7</RequestId> <HostId>7EPjXVhzo2S3fBt7fNivJjlnpzcM7Ii9lbsQG4rXYDsoI25+vYvCMS6Cfy1uvVpkHRQVBsqZZI8=</HostId> </Error>
核心原因
预签名URL已经包含了完整的请求路径和签名信息,但代理代码在proxyReq事件中修改了请求路径,导致实际发送给S3的请求路径和预签名URL中的路径不一致,签名验证失败。从错误信息的<StringToSign>可以看到,签名是基于/test-react-bucket-jk/build/index.html生成的,但代理后额外添加了/index.html,变成了/index.html/test-react-bucket-jk/build/index.html,完全不符合预期。同时,清空请求头的操作也可能导致S3验证签名时缺少必要信息。
具体修复步骤
删除
proxyReq事件中的所有逻辑
预签名URL已经包含了正确的对象路径,不需要手动修改路径或清空请求头。直接移除这段代码:proxy.on('proxyReq', (proxyReq, req, res) => { const url = req.url; req.rawHeaders = [] if (url === '/') proxyReq.path += 'index.html'; console.log(req.rawHeaders) });确认S3对象Key的拼接正确性
当前代码中key = subdomain + (req.path === '/' ? '/index.html' : req.path)的逻辑是正确的,但要确保S3中确实存在对应Key的对象(注意S3的Key区分大小写,且没有真正的文件夹结构,只是用/作为分隔符)。可选优化:改用重定向替代反向代理
不需要用反向代理,直接将预签名URL返回给客户端做重定向更简单可靠,避免代理层篡改请求:app.get('*', async (req, res) => { const hostname = req.hostname; const subdomain = hostname.split('.')[0]; let key = subdomain + (req.path === '/' ? '/index.html' : req.path); try { const signedUrl = await getPreSignedUrl(key); res.redirect(signedUrl); // 直接重定向到预签名URL } catch (error) { console.error("Error:", error); res.status(500).send("Error generating signed URL"); } });
验证修复
修复后重新运行脚本,访问build.localhost:8080,应该能正确加载S3中的build/index.html文件,不会再出现签名不匹配的错误。
内容的提问来源于stack exchange,提问作者iwrestledthebeartwice

