Я создаю функцию загрузки изображений с помощью Angular и FineUploader, я хочу загружать изображения непосредственно в amazon s3.Я сталкиваюсь с проблемой, что подпись не совпадает с подписью s3 POST-запроса:
Рассчитанная нами подпись запроса не соответствует предоставленной вами подписи.Проверьте ваш ключ и метод подписи
Я не уверен, как установить 'process.env.CLIENT_SECRET_KEY', которая является основной причиной или нет.
`----ERROR Message-----
--->>>selected file: product-travel2.jpg
zone.js:3243 POST https://dcp-cp-dev-user-file-bucket.s3.amazonaws.com/ 403 (Forbidden)
scheduleTask @ zone.js:3243
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask @ zone.js:410
onScheduleTask @ zone.js:301
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask @ zone.js:404
push../node_modules/zone.js/dist/zone.js.Zone.scheduleTask @ zone.js:238
push../node_modules/zone.js/dist/zone.js.Zone.scheduleMacroTask @ zone.js:261
scheduleMacroTaskWithCurrentZone @ zone.js:1245
(anonymous) @ zone.js:3276
proto.(anonymous function) @ zone.js:1569
(anonymous) @ s3.fine-uploader.core.js:7539
(anonymous) @ s3.fine-uploader.core.js:727
push../node_modules/fine-uploader/s3.fine-uploader/s3.fine-uploader.core.js.qq.each @ s3.fine-uploader.core.js:428
success @ s3.fine-uploader.core.js:726
(anonymous) @ s3.fine-uploader.core.js:7549
(anonymous) @ s3.fine-uploader.core.js:727
push../node_modules/fine-uploader/s3.fine-uploader/s3.fine-uploader.core.js.qq.each @ s3.fine-uploader.core.js:428
success @ s3.fine-uploader.core.js:726
(anonymous) @ s3.fine-uploader.core.js:6153
(anonymous) @ s3.fine-uploader.core.js:727
push../node_modules/fine-uploader/s3.fine-uploader/s3.fine-uploader.core.js.qq.each @ s3.fine-uploader.core.js:428
success @ s3.fine-uploader.core.js:726
handleSignatureReceived @ s3.fine-uploader.core.js:6771
onComplete @ s3.fine-uploader.core.js:2763
(anonymous) @ s3.fine-uploader.core.js:2830
wrapFn @ zone.js:1332
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask @ zone.js:423
onInvokeTask @ core.js:3811
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask @ zone.js:422
push../node_modules/zone.js/dist/zone.js.Zone.runTask @ zone.js:195
push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask @ zone.js:498
invokeTask @ zone.js:1744
globalZoneAwareCallback @ zone.js:1770
XMLHttpRequest.send (async)
scheduleTask @ zone.js:3243
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask @ zone.js:410
onScheduleTask @ zone.js:301
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.scheduleTask @ zone.js:404
push../node_modules/zone.js/dist/zone.js.Zone.scheduleTask @ zone.js:238
push../node_modules/zone.js/dist/zone.js.Zone.scheduleMacroTask @ zone.js:261
scheduleMacroTaskWithCurrentZone @ zone.js:1245
(anonymous) @ zone.js:3276
proto.(anonymous function) @ zone.js:1569
sendRequest @ s3.fine-uploader.core.js:2808
prepareToSend @ s3.fine-uploader.core.js:2887
send @ s3.fine-uploader.core.js:2923
getSignature @ s3.fine-uploader.core.js:6929
(anonymous) @ s3.fine-uploader.core.js:460
generateAwsParams @ s3.fine-uploader.core.js:6140
initParams @ s3.fine-uploader.core.js:7513
setup @ s3.fine-uploader.core.js:7545
send @ s3.fine-uploader.core.js:7537
(anonymous) @ s3.fine-uploader.core.js:7672
then @ s3.fine-uploader.core.js:710
(anonymous) @ s3.fine-uploader.core.js:7670
then @ s3.fine-uploader.core.js:710
(anonymous) @ s3.fine-uploader.core.js:7669
then @ s3.fine-uploader.core.js:710
start @ s3.fine-uploader.core.js:7668
uploadFile @ s3.fine-uploader.core.js:7706
send @ s3.fine-uploader.core.js:3224
(anonymous) @ s3.fine-uploader.core.js:3351
then @ s3.fine-uploader.core.js:710
now @ s3.fine-uploader.core.js:3342
maybeSendDeferredFiles @ s3.fine-uploader.core.js:3317
maybeDefer @ s3.fine-uploader.core.js:3299
start @ s3.fine-uploader.core.js:3366
upload @ s3.fine-uploader.core.js:3379
_uploadFile @ s3.fine-uploader.core.js:2336
_onSubmitCallbackSuccess @ s3.fine-uploader.core.js:2146
(anonymous) @ s3.fine-uploader.core.js:460
_handleCheckedCallback @ s3.fine-uploader.core.js:1817
_upload @ s3.fine-uploader.core.js:2327
(anonymous) @ s3.fine-uploader.core.js:2223
then @ s3.fine-uploader.core.js:710
_onValidateCallbackSuccess @ s3.fine-uploader.core.js:2222
(anonymous) @ s3.fine-uploader.core.js:460
_handleCheckedCallback @ s3.fine-uploader.core.js:1817
_onValidateBatchCallbackSuccess @ s3.fine-uploader.core.js:2199
(anonymous) @ s3.fine-uploader.core.js:460
_handleCheckedCallback @ s3.fine-uploader.core.js:1817
_prepareItemsForUpload @ s3.fine-uploader.core.js:2235
addFiles @ s3.fine-uploader.core.js:1082
_onInputChange @ s3.fine-uploader.core.js:2128
onChange @ s3.fine-uploader.core.js:1536
(anonymous) @ s3.fine-uploader.core.js:808
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask @ zone.js:423
onInvokeTask @ core.js:3811
push../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask @ zone.js:422
push../node_modules/zone.js/dist/zone.js.Zone.runTask @ zone.js:195
push../node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask @ zone.js:498
invokeTask @ zone.js:1744
globalZoneAwareCallback @ zone.js:1770
image-upload.component.ts:64 --->>>error:The request signature we calculated does not match the signature you provided. Check your key and signing method.
на стороне сервера: пакет.json
{
"name": "fine-uploader-traditional-server",
"description": "Fine Uploader NodeJS example server for traditional server environments",
"dependencies": {
"body-parser": "^1.18.3",
"crypto-js": "^3.1.9-1",
"errorhandler": "^1.5.0",
"express": "^4.16.4",
"method-override": "^3.0.0",
"mkdirp": "^0.5.1",
"multiparty": "^4.2.1",
"rimraf": "^2.6.3"
},
"files": [
"nodejs.js"
],
"version": "3.0.3"
}
сторона сервера: s3handler.js
var express = require('express'),
CryptoJS = require('crypto-js'),
aws = require('aws-sdk'),
app = express(),
// clientSecretKey = process.env.CLIENT_SECRET_KEY,
// same with serverPublicKey
clientSecretKey = 'xxxxxxxxxx',
// These two keys are only needed if you plan on using the AWS SDK
serverPublicKey = 'xxxxxxxxxx',
serverSecretKey = 'yyyyyyyyyyyyyyyyy',
// Set these two values to match your environment
expectedBucket = 'dcp-cp-dev-user-file-bucket',
expectedHostname = 'dcp-cp-dev-user-file-bucket.s3.amazonaws.com',
// CHANGE TO INTEGERS TO ENABLE POLICY DOCUMENT VERIFICATION ON FILE SIZE
// recommended
expectedMinSize = 0,
expectedMaxSize = 5120000,
// EXAMPLES DIRECTLY BELOW:
//expectedMinSize = 0,
//expectedMaxSize = 15000000,
s3;
// Init S3, given your server-side keys. Only needed if using the AWS SDK.
aws.config.update({
accessKeyId: serverPublicKey,
secretAccessKey: serverSecretKey
});
s3 = new aws.S3({
signatureVersion: 'v4'
});
// app.use(express.bodyParser());
var bodyParser = require('body-parser');
app.use(
bodyParser.urlencoded({
extended: true
})
);
app.use(bodyParser.json());
app.use(express.static(__dirname)); //only needed if serving static content as well
// Crossを有効
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'http://localhost:4200');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');
res.header('Access-Control-Allow-Credentials', true);
next();
});
app.listen(8000, () => console.log('@@@ Listening on port 8000'));
// Optionsも必要
app.options('*', (req, res) => {
res.sendStatus(200);
});
app.get('/hello', (req, res) => res.send('Hello'));
// Handles all signature requests and the success request FU S3 sends after the file is in S3
// You will need to adjust these paths/conditions based on your setup.
app.post('/s3handler', function(req, res) {
if (typeof req.query.success !== 'undefined') {
verifyFileInS3(req, res);
} else {
signRequest(req, res);
}
});
// Handles the standard DELETE (file) request sent by Fine Uploader S3.
// Omit if you don't want to support this feature.
app.delete('/s3handler/*', function(req, res) {
deleteFile(req.query.bucket, req.query.key, function(err) {
if (err) {
console.log('Problem deleting file: ' + err);
res.status(500);
}
res.end();
});
});
// Signs any requests. Delegate to a more specific signer based on type of request.
function signRequest(req, res) {
if (req.body.headers) {
signRestRequest(req, res);
} else {
signPolicy(req, res);
}
}
// Signs multipart (chunked) requests. Omit if you don't want to support chunking.
function signRestRequest(req, res) {
var version = req.query.v4 ? 4 : 2,
stringToSign = req.body.headers,
signature = version === 4 ? signV4RestRequest(stringToSign) : signV2RestRequest(stringToSign);
var jsonResponse = {
signature: signature
};
res.setHeader('Content-Type', 'application/json');
if (isValidRestRequest(stringToSign, version)) {
res.end(JSON.stringify(jsonResponse));
} else {
res.status(400);
res.end(JSON.stringify({ invalid: true }));
}
}
function signV2RestRequest(headersStr) {
return getV2SignatureKey(clientSecretKey, headersStr);
}
function signV4RestRequest(headersStr) {
var matches = /.+\n.+\n(\d+)\/(.+)\/s3\/aws4_request\n([\s\S]+)/.exec(headersStr),
hashedCanonicalRequest = CryptoJS.SHA256(matches[3]),
stringToSign = headersStr.replace(/(.+s3\/aws4_request\n)[\s\S]+/, '$1' + hashedCanonicalRequest);
return getV4SignatureKey(clientSecretKey, matches[1], matches[2], 's3', stringToSign);
}
// Signs "simple" (non-chunked) upload requests.
function signPolicy(req, res) {
var policy = req.body,
base64Policy = new Buffer(JSON.stringify(policy)).toString('base64'),
signature = req.query.v4 ? signV4Policy(policy, base64Policy) : signV2Policy(base64Policy);
var jsonResponse = {
policy: base64Policy,
signature: signature
};
res.setHeader('Content-Type', 'application/json');
if (isPolicyValid(req.body)) {
res.end(JSON.stringify(jsonResponse));
} else {
res.status(400);
res.end(JSON.stringify({ invalid: true }));
}
}
function signV2Policy(base64Policy) {
return getV2SignatureKey(clientSecretKey, base64Policy);
}
function signV4Policy(policy, base64Policy) {
var conditions = policy.conditions,
credentialCondition;
for (var i = 0; i < conditions.length; i++) {
credentialCondition = conditions[i]['x-amz-credential'];
if (credentialCondition != null) {
break;
}
}
var matches = /.+\/(.+)\/(.+)\/s3\/aws4_request/.exec(credentialCondition);
return getV4SignatureKey(clientSecretKey, matches[1], matches[2], 's3', base64Policy);
}
// Ensures the REST request is targeting the correct bucket.
// Omit if you don't want to support chunking.
function isValidRestRequest(headerStr, version) {
if (version === 4) {
return new RegExp('host:' + expectedHostname).exec(headerStr) != null;
}
return new RegExp('/' + expectedBucket + '/.+$').exec(headerStr) != null;
}
// Ensures the policy document associated with a "simple" (non-chunked) request is
// targeting the correct bucket and the min/max-size is as expected.
// Comment out the expectedMaxSize and expectedMinSize variables near
// the top of this file to disable size validation on the policy document.
function isPolicyValid(policy) {
var bucket, parsedMaxSize, parsedMinSize, isValid;
policy.conditions.forEach(function(condition) {
if (condition.bucket) {
bucket = condition.bucket;
} else if (condition instanceof Array && condition[0] === 'content-length-range') {
parsedMinSize = condition[1];
parsedMaxSize = condition[2];
}
});
isValid = bucket === expectedBucket;
// If expectedMinSize and expectedMax size are not null (see above), then
// ensure that the client and server have agreed upon the exact same
// values.
if (expectedMinSize != null && expectedMaxSize != null) {
isValid = isValid && parsedMinSize === expectedMinSize.toString() && parsedMaxSize === expectedMaxSize.toString();
}
return isValid;
}
// After the file is in S3, make sure it isn't too big.
// Omit if you don't have a max file size, or add more logic as required.
function verifyFileInS3(req, res) {
function headReceived(err, data) {
if (err) {
res.status(500);
console.log(err);
res.end(JSON.stringify({ error: 'Problem querying S3!' }));
} else if (expectedMaxSize != null && data.ContentLength > expectedMaxSize) {
res.status(400);
res.write(JSON.stringify({ error: 'Too big!' }));
deleteFile(req.body.bucket, req.body.key, function(err) {
if (err) {
console.log("Couldn't delete invalid file!");
}
res.end();
});
} else {
res.end();
}
}
callS3(
'head',
{
bucket: req.body.bucket,
key: req.body.key
},
headReceived
);
}
function getV2SignatureKey(key, stringToSign) {
var words = CryptoJS.HmacSHA1(stringToSign, key);
return CryptoJS.enc.Base64.stringify(words);
}
function getV4SignatureKey(key, dateStamp, regionName, serviceName, stringToSign) {
var kDate = CryptoJS.HmacSHA256(dateStamp, 'AWS4' + key),
kRegion = CryptoJS.HmacSHA256(regionName, kDate),
kService = CryptoJS.HmacSHA256(serviceName, kRegion),
kSigning = CryptoJS.HmacSHA256('aws4_request', kService);
return CryptoJS.HmacSHA256(stringToSign, kSigning).toString();
}
function deleteFile(bucket, key, callback) {
callS3(
'delete',
{
bucket: bucket,
key: key
},
callback
);
}
function callS3(type, spec, callback) {
s3[type + 'Object'](
{
Bucket: spec.bucket,
Key: spec.key
},
callback
);
}
сторона клиента: package.json
"dependencies": {
"@angular/animations": "^6.1.0",
"@angular/common": "^6.1.0",
"@angular/compiler": "^6.1.0",
"@angular/core": "^6.1.0",
"@angular/forms": "^6.1.0",
"@angular/http": "^6.1.0",
"@angular/platform-browser": "^6.1.0",
"@angular/platform-browser-dynamic": "^6.1.0",
"@angular/router": "^6.1.0",
"core-js": "^2.5.4",
"fine-uploader": "^5.16.2",
"rxjs": "~6.2.0",
"zone.js": "~0.8.26"
},
сторона клиента: image-upload.component.ts
import { Component, AfterViewInit, OnInit } from '@angular/core';
import { s3 } from 'fine-uploader/lib/core/s3';
@Component({
selector: 'dcp-image-upload',
templateUrl: './image-upload.component.html',
styleUrls: ['./image-upload.component.scss']
})
export class ImageUploadComponent implements AfterViewInit {
bucketName = 'dcp-cp-dev-user-file-bucket';
uploader: any;
ngAfterViewInit() {
const instance = this;
this.uploader = new s3.FineUploaderBasic({
button: document.getElementById('upload_image'),
debug: false,
autoUpload: true,
multiple: true,
validation: {
allowedExtensions: ['jpeg', 'jpg', 'png', 'gif', 'svg'],
sizeLimit: 5120000 // 50 kB = 50 * 1024 bytes
},
region: 'ap-northeast-1',
request: {
endpoint: 'https://' + instance.bucketName + '.s3.amazonaws.com/',
// same with serverPublicKey
accessKey: 'xxxxxxxxxx',
params: { 'Cache-Control': 'private, max-age=31536000, must-revalidate' }
},
signature: {
endpoint: 'http://localhost:8000/s3handler'
},
iframeSupport: {
localBlankPagePath: '/somepage.html'
},
cors: {
expected: true,
sendCredentials: true
},
objectProperties: {
acl: 'public-read'
},
callbacks: {
onSubmit: function(id, fileName) {
console.log('--->>>selected file:', fileName);
},
// onSubmitted: function(id, name) { alert('onSubmitted');},
onComplete: function(id, name, responseJSON, maybeXhr) {
if (responseJSON.success) {
console.log('===>>>upload complete:', name);
console.log('===>>>uploaded image url:', 'https://' + instance.bucketName + '.s3.amazonaws.com/' + this.getKey(id));
}
},
// onAllComplete: function (successful, failed) { console.log(failed); },
// onCancel: function (id, name) {},
// onUpload: function(id, name) { alert('onUpload');},
// onUploadChunk: function(id, name, chunkData) { alert('onUploadChunk');},
// onUploadChunkSuccess: function(id, chunkData, responseJSON, xhr) { alert('onUploadChunkSuccess');},
// onResume: function(id, fileName, chunkData) { alert('onResume');},
// onProgress: function (id, name, loaded, total) {},
// onTotalProgress: function(loaded, total) { alert('onTotalProgress');},
onError: function(id, name, reason, maybeXhrOrXdr) {
console.log('--->>>error:' + reason);
}
// onSessionRequestComplete: function (response, success, xhrOrXdr) { }
}
});
}
}
на стороне клиента: image-upload.component.html
<div class='btn' id='upload_image'>Upload image</div>
на стороне клиента: app.componnet.html
<dcp-image-upload></dcp-image-upload>
<router-outlet></router-outlet>
aws s3 конфигурации:политика cors
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedMethod>DELETE</AllowedMethod>
<MaxAgeSeconds>3000</MaxAgeSeconds>
<ExposeHeader>ETag</ExposeHeader>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
aws s3 конфигурация шифрования
default encryption enables using AES-256.
aws конфигурация IAM
Я использую пользователя IAM с правами администратора и включенным MFA, указанные выше ключи являются временными ключамичерез скрипт sts token.
Я ожидаю, что загрузка изображения на S3 напрямую через мой сервер node.js будет подписана