NestJS-yritysbackendien API:t · Oppitunti

Suorat S3-lataukset allekirjoitetuilla URL-osoitteilla

Siirrä raskaat lataukset objektisäilöön luomalla API:sta lyhytikäisiä allekirjoitettuja URL-osoitteita.

Oppitunti 3/413 vaihetta

Suorat S3-lataukset allekirjoitetuilla URL-osoitteilla on ilmainen NestJS-yritysbackendien API:t-oppitunti CoddyKitissä. Tämä on oppitunti 3/4. Voit lukea koko oppitunnin alta ilmaiseksi ja harjoitella sen jälkeen käytännössä selaimessa sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla. Oppitunti kuuluu NestJS-yritysbackendien API:t-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. NestJS-yritysbackendien API:t-kurssilla on yhteensä 4 oppituntia.

Miksi latauksia ei välitetä proxyna API:n kautta?

Kun asiakas lataa suuren tiedoston, naiivi ratkaisu lähettää tavut NestJS-API:lle, joka välittää ne edelleen objektitallennukseen. Tällöin palvelimestasi tulee pullonkaula.

  • Muisti- ja suorittimen kuormitus — jokainen lataus käyttää pyyntösäiettä ja kulkee prosessisi puskureiden ja streamien kautta.
  • Kaksinkertainen kaistan käyttö — tavut kulkevat reittiä asiakas → API → S3, joten maksat samasta datasta kahdesti.
  • Pyyntöjen aikakatkaisut — kuormantasaajat (esimerkiksi ALB ja Nginx) rajoittavat pyynnön kestoa, joten usean gigatavun lataukset pysähtyvät.

Ratkaisu on antaa selaimen ladata tiedosto suoraan S3:een. API-palvelimesi luo vain lyhytikäisen allekirjoitetun URL-osoitteen, joka antaa luvan yhteen tiettyyn toimintoon.

Mikä on allekirjoitettu URL-osoite?

Allekirjoitettu URL-osoite on tavallinen S3-objektin URL-osoite, jonka kyselyparametrit sisältävät väliaikaisen, kryptografisesti allekirjoitetun luvan. URL-osoitteen haltija voi suorittaa täsmälleen yhden toiminnon (esimerkiksi PutObject) täsmälleen yhdelle avaimelle sen vanhenemiseen asti.

  • Allekirjoitetaan AWS-tunnistetiedoillasi, mutta tunnistetietoja ei koskaan paljasteta — vain allekirjoitus näkyy.
  • Rajoittuu yhteen HTTP-metodiin, säilöön ja objektiavaimeen.
  • Sillä on kiinteä voimassaoloaika (sekunteina), jonka jälkeen S3 hylkää sen virheellä 403.

Koska S3 tarkistaa allekirjoituksen itse, API-palvelimesi ei käsittele tiedoston tavuja lainkaan.

Latauksen kulku

Alusta loppuun ulottuvassa kulussa on kolme osapuolta: selain, NestJS-API-palvelimesi ja S3.

  • 1. Pyyntö: Selain pyytää API:lta: "Haluan ladata tiedoston avatar.png, 240 kt, image/png."
  • 2. Allekirjoitus: API tarkistaa pyynnön, luo yksilöllisen avaimen ja palauttaa allekirjoitetun PUT-URL-osoitteen.
  • 3. Lataus: Selain lähettää raakatavut PUT-pyynnöllä suoraan kyseiseen S3:n URL-osoitteeseen.
  • 4. Vahvistus: Selain ilmoittaa API:lle latauksen onnistuneen, ja API tallentaa avaimen tietokantaan.

API-palvelimesi pysyy nopeana ja tilattomana — se ei koskaan välitä hyötykuormaa proxyna.

S3-asiakkaan määrittäminen

Käytä AWS SDK v3:n modulaarisia paketteja. Luo yksi S3Client-instanssi ja jaa se NestJS-providerin kautta, jotta tunnistetiedot ja alue määritetään yhdessä paikassa.

Tunnistetiedot haetaan ympäristömuuttujista (tai tuotannossa IAM-roolista). Älä koskaan kirjoita niitä suoraan koodiin.

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { S3Client } from '@aws-sdk/client-s3';

@Injectable()
export class S3Provider {
  readonly client: S3Client;
  readonly bucket: string;

  constructor(private readonly config: ConfigService) {
    this.bucket = config.getOrThrow<string>('S3_BUCKET');
    this.client = new S3Client({
      region: config.getOrThrow<string>('AWS_REGION'),
      credentials: {
        accessKeyId: config.getOrThrow<string>('AWS_ACCESS_KEY_ID'),
        secretAccessKey: config.getOrThrow<string>('AWS_SECRET_ACCESS_KEY'),
      },
    });
  }
}

Allekirjoitetun PUT-URL-osoitteen luominen

@aws-sdk/s3-request-presigner-paketti allekirjoittaa komennon suorittamatta sitä. Muodostat PutObjectCommand-komennon, joka kuvaa kohdeavaimen ja sisällön tyypin, ja kutsut sitten getSignedUrl-funktiota yhdessä expiresIn-arvon kanssa.

  • Komennon ContentType pakotetaan: selaimen on lähetettävä vastaava Content-Type-otsake.
  • expiresIn ilmoitetaan sekunteina — pidä arvo pienenä (60–300 sekuntia), jotta vuotaneet URL-osoitteet vanhenevat nopeasti.
import { Injectable } from '@nestjs/common';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { S3Provider } from './s3.provider';

@Injectable()
export class UploadsService {
  constructor(private readonly s3: S3Provider) {}

  async createUploadUrl(key: string, contentType: string): Promise<string> {
    const command = new PutObjectCommand({
      Bucket: this.s3.bucket,
      Key: key,
      ContentType: contentType,
    });
    return getSignedUrl(this.s3.client, command, { expiresIn: 120 });
  }
}

Turvallisten ja yksilöllisten objektiavainten luominen

Älä koskaan luota asiakkaan tiedostonimeen S3-avaimena. Käyttäjä voisi lähettää arvon ../../etc/passwd tai käyttää samaa nimeä kuin toinen käyttäjä. Luo avain palvelimella.

  • Ryhmittele omistajan mukaan: uploads/{userId}/..., jotta käyttöoikeuksien tarkistaminen on helppo ymmärtää.
  • Käytä satunnaista UUID:tä yksilöllisyyden takaamiseksi.
  • Säilytä vain puhdistettu tiedostopääte sisällön tyypin vihjettä ja työkaluja varten.
import { randomUUID } from 'node:crypto';
import { extname } from 'node:path';

function buildObjectKey(userId: string, originalName: string): string {
  const ext = extname(originalName).toLowerCase().replace(/[^.a-z0-9]/g, '');
  const safeExt = /^\.[a-z0-9]{1,8}$/.test(ext) ? ext : '';
  return `uploads/${userId}/${randomUUID()}${safeExt}`;
}

console.log(buildObjectKey('user-42', 'My Vacation Photo.PNG'));
console.log(buildObjectKey('user-42', 'sneaky/../../etc/passwd'));

Controller-endpoint

Julkaise suojattu endpoint, joka vastaanottaa tiedoston metatiedot, tarkistaa ne DTO:n avulla ja palauttaa allekirjoitetun URL-osoitteen sekä lopullisen avaimen. Asiakas tarvitsee avainta myöhemmin latauksen vahvistamiseen tai julkisen/luku-URL-osoitteen muodostamiseen.

Todennus on tässä tärkeää: allekirjoitus-endpoint on käyttöoikeuksien hallintaporttisi. S3 luottaa jokaiseen kelvolliseen allekirjoitukseen, joten kaikki tarkistukset (kuka, mikä koko, mikä tyyppi) on tehtävä ennen allekirjoittamista.

import { Body, Controller, Post, UseGuards, Req } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { UploadsService } from './uploads.service';
import { CreateUploadDto } from './dto/create-upload.dto';

@UseGuards(JwtAuthGuard)
@Controller('uploads')
export class UploadsController {
  constructor(private readonly uploads: UploadsService) {}

  @Post('presign')
  async presign(@Req() req, @Body() dto: CreateUploadDto) {
    return this.uploads.presignForUser(req.user.id, dto);
  }
}

Latauspyynnön DTO:n tarkistaminen

Tarkista metatiedot ennen allekirjoittamista. Hylkää sallitsemattomat MIME-tyypit ja liian suuret tiedostot API-kerroksessa — mutta muista, että asiakas voi valehdella, joten tämä on ensimmäinen puolustuslinja, ei viimeinen.

  • Salli contentType-arvot vain sallittujen arvojen luettelosta.
  • Rajoita ilmoitettua size-arvoa, jotta ilmeisen valtavat lataukset hylätään nopeasti.
import { IsIn, IsInt, IsString, Max, Min } from 'class-validator';

const ALLOWED = ['image/png', 'image/jpeg', 'image/webp', 'application/pdf'] as const;

export class CreateUploadDto {
  @IsString()
  filename: string;

  @IsIn(ALLOWED)
  contentType: (typeof ALLOWED)[number];

  @IsInt()
  @Min(1)
  @Max(10 * 1024 * 1024) // 10 MB
  size: number;
}

Koon pakottaminen allekirjoitetulla Content-Length-arvolla

DTO:n kokotarkistus on vain suuntaa antava — selain hallitsee edelleen sitä, kuinka monta tavua se todellisuudessa lähettää PUT-pyynnöllä. Jotta S3 itse hylkäisi liian suuret lataukset, sido Content-Length-arvoalue allekirjoitukseen.

Yksittäisessä PUT-pyynnössä allekirjoita ContentLength, jotta S3 pakottaa tarkan tavumäärän. Joustavampia rajoja (minimi-/maksimialueen) varten käytä sen sijaan allekirjoitettua POST-käytäntöä, joka tukee content-length-range-ehtoja.

import { createPresignedPost } from '@aws-sdk/s3-presigned-post';

async function presignPost(client, bucket: string, key: string) {
  return createPresignedPost(client, {
    Bucket: bucket,
    Key: key,
    Conditions: [
      ['content-length-range', 1, 10 * 1024 * 1024], // 1 byte – 10 MB
      ['starts-with', '$Content-Type', 'image/'],
    ],
    Fields: { 'Content-Type': 'image/png' },
    Expires: 120,
  });
}

Lataus selaimesta

Kun allekirjoitettu PUT-URL-osoite on käytettävissä, selain lataa tiedoston tavallisella fetch-kutsulla. Asiakaspuolella ei tarvita SDK:ta tai AWS-tunnistetietoja — vain tavut ja vastaava Content-Type.

  • Otsakkeen on oltava sama kuin allekirjoituksessa käytetty ContentType, tai S3 palauttaa virheen 403 SignatureDoesNotMatch.
  • Älä lähetä Authorization-otsaketta — allekirjoitus on kyselymerkkijonossa.
async function uploadFile(presignedUrl: string, file: File): Promise<void> {
  const res = await fetch(presignedUrl, {
    method: 'PUT',
    headers: { 'Content-Type': file.type },
    body: file,
  });
  if (!res.ok) {
    throw new Error(`Upload failed: ${res.status} ${res.statusText}`);
  }
}

Latauksen vahvistaminen ja tiedoston lukeminen

Koska S3 ei ilmoita tapahtumasta API:lle, asiakas tekee onnistuneen PUT-pyynnön jälkeen uuden kutsun, jotta voit tallentaa avaimen. Lisäturvana API voi kutsua HeadObject-komentoa varmistaakseen, että objekti todella on olemassa, ja tarkistaa sen todellisen koon ja tyypin ennen kuin siihen luotetaan.

Kun tiedosto tarjotaan myöhemmin, voit joko pitää säilön yksityisenä ja luoda allekirjoitetun GET-URL-osoitteen tarvittaessa tai (julkisten resurssien tapauksessa) tallentaa ja palauttaa julkisen URL-osoitteen.

import { GetObjectCommand, HeadObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

async confirm(userId: string, key: string) {
  const head = await this.s3.client.send(
    new HeadObjectCommand({ Bucket: this.s3.bucket, Key: key }),
  );
  if ((head.ContentLength ?? 0) > 10 * 1024 * 1024) {
    throw new BadRequestException('Object exceeds size limit');
  }
  await this.files.save({ userId, key, size: head.ContentLength });
}

async downloadUrl(key: string) {
  return getSignedUrl(
    this.s3.client,
    new GetObjectCommand({ Bucket: this.s3.bucket, Key: key }),
    { expiresIn: 300 },
  );
}

Pikatarkistus

Testaa, ymmärrätkö, missä tämän mallin käyttöoikeuksien hallinta tapahtuu.

Kertaus

Opit, miten raskaat lataukset siirretään S3:n hoidettaviksi allekirjoitettujen URL-osoitteiden avulla:

  • Miksi: tavujen välittäminen API:n kautta tuhlaa kaistanleveyttä ja muistia sekä johtaa pyyntöjen aikakatkaisuihin.
  • Miten: API allekirjoittaa lyhytikäisen PutObjectCommand-komennon getSignedUrl-funktiolla, ja selain lähettää PUT-pyynnön suoraan S3:een.
  • Avaimet: luo ne aina palvelimella (UUID + käyttäjän mukaan ryhmitelty nimiavaruus); älä koskaan luota asiakkaan tiedostonimiin.
  • Turvallisuus: allekirjoitus-endpoint on käyttöoikeuksien hallintaportti — todenna käyttäjä, salli vain tietyt MIME-tyypit ja rajoita koko siellä. Käytä allekirjoitetun POST-pyynnön content-length-range-ehtoa, jotta S3 pakottaa kokorajoituksen.
  • Latauksen jälkeen: vahvista objekti HeadObject-komennolla, tallenna avain ja tarjoa tiedosto myöhemmin allekirjoitettujen GetObjectCommand-URL-osoitteiden kautta.
Aloita maksutta

Opi TypeScript tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
20
Oppitunnit
76

Usein kysytyt kysymykset

Onko oppitunti ”Suorat S3-lataukset allekirjoitetuilla URL-osoitteilla” ilmainen?

Kyllä – oppitunnin ”Suorat S3-lataukset allekirjoitetuilla URL-osoitteilla” koko tekstin voi lukea täällä verkossa ilmaiseksi. Jos haluat harjoitella interaktiivisesti sisäänrakennetulla koodieditorilla ja ympäri vuorokauden käytettävissä olevan tekoälytuutorin avulla sekä avata koko NestJS-yritysbackendien API:t-kurssin, päivitä CoddyKit PROhon. NestJS-yritysbackendien API:t-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Suorat S3-lataukset allekirjoitetuilla URL-osoitteilla”?

Siirrä raskaat lataukset objektisäilöön luomalla API:sta lyhytikäisiä allekirjoitettuja URL-osoitteita. Harjoittelet NestJS-yritysbackendien API:t-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni NestJS-yritysbackendien API:t-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin NestJS-yritysbackendien API:t-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 3/4.

Kuinka kauan ”Suorat S3-lataukset allekirjoitetuilla URL-osoitteilla”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä NestJS-yritysbackendien API:t-oppitunnilla?

Kyllä. Jokainen NestJS-yritysbackendien API:t-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Multipart-lataukset Multer-sieppaajilla
  2. Suurten vastausten suoratoisto StreamableFilella
  3. Suorat S3-lataukset allekirjoitetuilla URL-osoitteilla
  4. Kuvankäsittelyputket Sharpilla
← Takaisin: NestJS-yritysbackendien API:t