FineUploader для подписи amazon-s3 не работает с ошибкой 403 - PullRequest
0 голосов
/ 09 февраля 2019

Я создаю функцию загрузки изображений с помощью 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 будет подписана

...