Cómo generar URL de Amazon S3 firmadas en Node.js

Acceda a contenido privado en un depósito de Amazon S3 mediante URL firmadas a corto plazo.

Primeros pasos

Para acelerar nuestro trabajo, vamos a utilizar CheatCode Node.js Boilerplate como punto de partida para nuestro trabajo. Para comenzar, clonemos una copia de ese proyecto:

Terminal

git clone https://github.com/cheatcode/nodejs-server-boilerplate.git

A continuación, necesitamos instalar las dependencias del repetitivo:

Terminal

cd nodejs-server-boilerplate && npm install

Después de esto, necesitamos instalar el aws-sdk paquete de NPM que nos dará acceso a la API de Amazon S3 para Node.js:

Terminal

npm i aws-sdk

Finalmente, inicie el servidor de desarrollo:

Terminal

npm run dev

Con eso en ejecución, estamos listos para comenzar.

Escribir una función para generar URL firmadas

Afortunadamente, el aws-sdk biblioteca nos da una función simple como parte del S3 constructor para generar URL firmadas. Lo que vamos a hacer es escribir una función que se ajuste a esto e inicialice nuestra conexión a Amazon S3.

/lib/getSignedS3URL.js

import AWS from "aws-sdk";
import settings from "./settings";

AWS.config = new AWS.Config({
  accessKeyId: settings?.aws?.akid,
  secretAccessKey: settings?.aws?.sak,
  region: "us-east-1",
  signatureVersion: "v4",
});

const s3 = new AWS.S3();

Después de haber importado aws-sdk arriba como AWS , configuramos el AWS.config global valor igual a una nueva instancia del AWS.Config clase (observa la sutil diferencia entre el cd minúsculo en el global que estamos configurando y la mayúscula C en la función constructora).

A esa clase, le pasamos un objeto con algunas configuraciones diferentes. Primero, queremos prestar atención al accessKeyId y secretAccessKey propiedades. Estos se establecen en las claves que obtenemos de AWS que asocian nuestras llamadas a S3 con nuestra cuenta de AWS.

Si bien la obtención de estas claves está fuera del alcance de este tutorial, si aún no las tiene, lea esta guía oficial sobre cómo crearlas a través de AWS IAM (Administración de acceso a la identidad).

Una vez que tengas tus llaves puedes continuar con el tutorial.

En el código anterior, no pegando nuestras claves directamente en nuestro código. En su lugar, estamos usando el settings característica que está integrada en el repetitivo que estamos usando. Está configurado para cargar la configuración de nuestra aplicación según el entorno (es decir, cargar diferentes claves para nuestro development entorno versus nuestro production ambiente).

El archivo que importamos aquí (ubicado en /lib/settings.js ) es responsable de decidir qué archivo de configuración debe cargarse cuando se inicia nuestra aplicación (el proceso iniciado por el npm run dev comando que ejecutamos anteriormente). De forma predeterminada, el texto modelo incluye un settings-development.json archivo en la raíz del proyecto que pretende contener nuestro desarrollo claves de entorno (mantener las claves separadas por entorno evita errores innecesarios y problemas de seguridad).

Al abrir ese archivo, queremos agregar las claves de AWS que obtuvo así:

/configuración-desarrollo.json

{
  [...]
  "aws": {
    "akid": "",
    "sak": ""
  },
  [...]
}

Aquí, agregamos una nueva propiedad alfabéticamente al objeto JSON en la raíz del archivo llamado aws (porque estamos en un .json archivo, necesitamos usar comillas dobles). Establecer en esa propiedad es otro objeto que contiene nuestras claves de AWS. Aquí, akid debe tener su valor establecido en su ID de clave de acceso para su usuario de IAM y sak debe tener su valor establecido en su clave de acceso secreta.

/lib/getSignedS3URL.js

import AWS from "aws-sdk";
import settings from "./settings";

AWS.config = new AWS.Config({
  accessKeyId: settings?.aws?.akid,
  secretAccessKey: settings?.aws?.sak,
  region: "us-east-1",
  signatureVersion: "v4",
});

const s3 = new AWS.S3();

De vuelta en nuestro archivo, con settings importado, ahora podemos apuntar a nuestras llaves con settings.aws.akid y settings.aws.sak . El ? entre cada propiedad anterior hay una técnica abreviada que nos ayuda a evitar escribir settings && settings.aws && settings.aws.akid (el settings?.aws?.akid que vemos arriba es equivalente a esto).

Con nuestras claves configuradas, a continuación, nos aseguramos de configurar el region donde vive nuestro depósito de Amazon S3. La creación de un depósito S3 también está fuera del alcance de este tutorial, por lo que si aún no ha configurado uno, lea esta guía de AWS y luego continúe con este tutorial una vez que lo haya completado. Asegúrese de anotar la región en la que crea su depósito (si no puede encontrar la versión discontinua de la región, consulte esta lista para encontrar el código adecuado para pasar a region encima de eso looks-like-this ).

Luego, con tu region conjunto, agregamos signatureVersion , estableciéndolo en v4 (esta es la última versión del protocolo de firma de AWS).

Finalmente, para completar el fragmento anterior, una vez que hayamos pasado todas nuestras configuraciones a AWS.Config , creamos una variable const s3 y establecerlo igual a una nueva instancia del AWS.S3() clase.

/lib/generarURL3SignedS3.js

import AWS from "aws-sdk";
import settings from "./settings";

AWS.config = new AWS.Config({ ... });

const s3 = new AWS.S3();

export default ({ bucket, key, expires }) => {
  const signedUrl = s3.getSignedUrl("getObject", {
    Key: key,
    Bucket: bucket,
    Expires: expires || 900, // S3 default is 900 seconds (15 minutes)
  });

  return signedUrl;
};

Como insinuamos anteriormente, el aws-sdk biblioteca hace que generar una URL firmada sea bastante simple. Aquí, hemos agregado una función que estamos configurando como predeterminada export . Esperamos que la función tome un único argumento como un objeto de JavaScript con tres propiedades:

  1. bucket - El depósito S3 que contiene el archivo ("objeto" en lenguaje AWS) para el que queremos recuperar una URL firmada.
  2. key - La ruta al archivo u "objeto" en nuestro depósito S3.
  3. expires - Cuánto tiempo en segundos queremos que la URL sea accesible (después de esta duración, los intentos posteriores de usar la URL fallarán).

Dentro de la función, creamos una nueva variable const signedUrl que esperamos que contenga nuestro signedUrl , aquí, lo que esperamos obtener al llamar a s3.getSignedUrl() . Algo que es único sobre el .getSignedUrl() El método aquí es que es sincrónico . Esto significa que cuando llamamos a la función, JavaScript esperará a que nos devuelva un valor antes de evaluar el resto de nuestro código.

A esa función, le pasamos dos argumentos:la operación de S3 que queremos realizar (ya sea getObject o putObject ) y un objeto de opciones que describe para qué archivo queremos recuperar una URL firmada.

La operación aquí debe ser explicada. Aquí, getObject dice que "queremos obtener una URL firmada para un objeto existente en nuestro depósito S3". Si tuviéramos que cambiar eso a putObject , podríamos simultáneamente crear un nuevo objeto y recuperar una URL firmada para ello. Esto es útil si siempre necesita recuperar una URL firmada (en lugar de obtener una después de que ya se haya cargado un archivo).

Para el objeto de opciones, aquí, simplemente copiamos las propiedades del argumento pasado a nuestra función contenedora. Notarás que las propiedades del objeto pasaron a .getSignedUrl() están en mayúscula, mientras que los pasados ​​a nuestra función contenedora están en minúsculas. En el aws-sdk , las letras mayúsculas se utilizan para las opciones que se pasan a las funciones de la biblioteca. Aquí, usamos minúsculas para nuestra función contenedora para simplificar las cosas.

Para estar seguro, para el Expires opción, si no hemos pasado un expires personalizado valor en nuestra función contenedora, recurrimos a 900 segundos o 15 minutos (esto significa que solo se podrá acceder a la URL que recibimos de Amazon durante 15 minutos antes de que sea un fracaso).

Finalmente, para concluir nuestra función, devolvemos signedUrl . A continuación, para probar esto, configuraremos una ruta Express.js simple donde podemos llamar a la función.

Conexión de una ruta Express para probar la generación de URL

Como parte de CheatCode Node.js Boilerplate que estamos usando para este tutorial, se nos proporciona un servidor Express.js preconfigurado. Ese servidor se crea dentro de /index.js en la raíz del proyecto. Allí, creamos el Express app y luego, para mantenerse organizado, pase ese app instancia en una serie de funciones donde definimos nuestras rutas reales (o extendemos el servidor Express HTTP).

/api/index.js

import getSignedS3URL from "../lib/getSignedS3URL";
import graphql from "./graphql/server";

export default (app) => {
  graphql(app);

  app.use("/s3/signed-url", (req, res) => {
    const signedUrl = getSignedS3URL({
      bucket: "cheatcode-tutorials",
      key: "panda.jpeg",
      expires: 5, // NOTE: Make this URL expire in five seconds.
    });

    res.send(`
      <html>
        <head>
          <title>AWS Signed URL Test</title>
        </head>
        <body>
          <p>URL on Amazon: ${signedUrl}</p>
          <img src="${signedUrl}" alt="AWS Signed URL Test" />
          <script>
            setTimeout(() => {
              location = "${signedUrl}";
            }, 6 * 1000);
          </script>
        </body>
      </html>
    `);
  });
};

Aquí, dentro del api() función que se llama desde el /index.js archivo que acabamos de discutir, tomamos el Express app ejemplo como argumento. De forma predeterminada, el modelo configura un servidor GraphQL para nosotros y aquí, separamos la creación de ese servidor en su propia función graphql() , pasando el app instancia para que pueda ser referenciada internamente.

A continuación, la parte que nos interesa en este tutorial, creamos una ruta de prueba en /s3/signed-url en nuestra aplicación (con nuestro servidor en ejecución, estará disponible en http://localhost:5001/s3/signed-url ). En la devolución de llamada para esa ruta, podemos ver que se realiza una llamada a nuestro getSignedS3URL() función (para ser claros, nuestra función contenedora). A él, le pasamos el objeto de opciones individuales que hemos anticipado con bucket , key y expires .

Aquí, como demostración, estamos pasando el cheatcode-tutorials depósito (usado para probar en nuestros tutoriales), un archivo que ya existe en nuestro depósito panda.jpeg como el key y expires establecido en 5 (es decir, expira la URL que recuperamos y almacenamos en const signedUrl aquí después de cinco segundos).

Establecemos esto bastante bajo para mostrar lo que sucede cuando se accede a una URL después de su tiempo de vencimiento (lo más probable es que desee configurar esto mucho más alto según su caso de uso). Para mostrar cómo funcionan estas URL, llamamos a res.send() para responder a cualquier solicitud de esta ruta con HTML ficticio, mostrando el signedUrl completo que recibimos de Amazon y, porque sabemos que es un .jpeg archivo:representar esa URL en un <img /> etiqueta.

Debajo de eso, hemos agregado una breve secuencia de comandos con un setTimeout() método que redirige el navegador a nuestra URL firmada después de seis segundos. Asumiendo nuestro expires se respeta el valor de 5 segundos, cuando visitamos esta URL, esperamos que sea inaccesible:

En nuestra demostración, podemos ver que cuando cargamos la página recuperamos nuestra URL (junto con nuestra imagen de panda). Después de seis segundos, redirigimos exactamente a la misma URL (sin cambios) y descubrimos que AWS arroja un error que nos dice que nuestra "solicitud ha caducado". Esto confirma que nuestra URL firmada se comportó como se esperaba y caducó cinco segundos después de su creación.

Terminando

En este tutorial, aprendimos cómo generar una URL temporal firmada para un objeto S3 usando el aws-sdk paquete. Aprendimos a escribir una función contenedora que establece una conexión con AWS y genera nuestra URL firmada.

Para demostrar nuestra función, finalmente, conectamos una ruta Express.js, devolviendo algo de HTML con una etiqueta de imagen que muestra nuestra URL firmada y luego redirigiendo después de unos segundos para verificar que la URL firmada caduque correctamente.