Ayden's journal

06. AWS S3 이미지 가져오기 구현

우리는 앞선 포스트를 통해 S3에 이미지를 저장하는 두 가지 방법에 대해 알아보았다. 이렇게 저장된 자원들은 REST API를 통해 접근할 수 있다. 따라서 아래와 같은 방식으로 요청하면 특정 버킷으로부터 이미지 파일을 가져올 수 있다.

https://[버킷 이름].[리전 이름].amazonaws.com/[파일 이름]

 

그런데 이러한 주소를 직접 사용하는 것은 문제가 없을까? 이러한 주소를 직접 클라이언트에 건네준다는 것은 곧 내가 사용하고 있는 S3가 어느 리전에 위치해있는지, 버킷 이름은 무엇인지와 같은 정보가 무방비하게 노출되어버리고 만다는 의미가 된다. 따라서 이번에도 백엔드 서버가 S3와 클라이언트 사이를 중개해주어야 하는데, 나는 여기서 presigned URL을 사용하고 있다. pURL은 그저 '권한이 미리 서명된 URL'에 지나지 않기 때문에 이를 put 요청이 아니라 get 요청에도 사용할 수 있는 것이다.

imageRoutes.get('/:imageName', async (req: Request, res: Response) => {
const { imageName } = req.params;
// presigned URL 생성
const params = new GetObjectCommand({
Bucket: process.env.S3_BUCKET_NAME as string,
Key: imageName,
});
try {
const url = await getSignedUrl(s3, params, { expiresIn: 60 });
// presigned URL을 사용하여 이미지를 가져옴
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error fetching image: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// 이미지 콘텐츠를 클라이언트에 전달
res.set(
'Content-Type',
response.headers.get('Content-Type') || 'image/jpeg',
);
res.send(buffer);
} catch (error) {
console.error('Error fetching image:', error);
res.status(500).send('Error fetching image');
}
});

 

이렇게 하면 버킷과 리전을 노출하지 않고도 클라이언트에 이미지를 제공할 수 있다. 앞으로는 클라이언트가 S3가 아닌 백엔드에게 이미지를 요청할 수 있도록, 이미지 업로드의 결과로 백엔드 주소를 보내주어야 할 것이다. 아래의 예시 코드에서는 localhost:3000라고 했지만, 실제로는 백엔드 서버의 주소를 적어주면 된다

imageRoutes.post(
'/upload',
upload.array('images', 10),
(req: Request, res: Response) => {
try {
const files = req.files as Express.MulterS3.File[];
const locations = files.map((file) => {
const fileName = file.location.split("amazonaws.com/")[1]
const imagesURL = "localhost:3000/images/"
return imagesURL + fileName
});
res
.status(200)
.send({ message: 'Files uploaded successfully!', locations });
} catch (error) {
res.status(500).send({ error: 'Failed to upload files.' });
}
},
);

 

pURL을 사용해 이미지를 서빙하는 것은 크게 두 가지 장점이 있다. 하나는 앞서 살펴본 보안 문제인데, 버킷 이름과 리전 이름을 숨기고 백엔드 서버의 주소만을 노출하기 때문에 S3 주소를 직접 보내주는 것보다는 훨씬 안전하다. 다른 하나의 장점은 인가를 적용하기가 수월하다는 점이다.

 

만약 /images/pro/000000.jpg라는 주소로 get 요청이 들어왔다고 해보자. 이 주소는 pro사용자만이 접근이 가능하다. 따라서 우리는 접근하는 유저의 인가 정보를 확인하고, 등급이 pro일 때만 이미지를 제공할 수 있게 된다.

그런데 누군가 똑같은 이름의 이미지를 경로만 다르게하여 /images/common/000000.jpg 주소로 요청한다면 어떻게 될까. 이 경로는 common 사용자도 접근이 가능하니까, pro 이미지가 common 사용자에게도 공개되어버리고 마는 것일까?

 

당연하게도 pro에게만 공개된 이미지가 common에게 공개되는 것은 비즈니스 정책상의 실패 ─ 혹은 그 이상의 문제 ─ 라고 할 수 있겠다. 문제는 S3 버킷이 하나 뿐일 경우 위와 같은 방식으로 경로 이름만 바꿔서 common 사용자가 pro 이미지에 접근할 수도 있다는 것이다. 따라서 등급에 따라 서로 다른 이미지를 제공하고자 했다면, 등급에 따라 서로 다른 버킷을 사용하는 것이 옳다. 위에서 살펴봤던 코드를 조금만 수정하면 아주 간단하게 등급별 이미지 제공 정책을 추가할 수 있다.

imageRoutes.get('/:level/:imageName', isEnough, async (req: Request, res: Response) => {
const { imageName, level } = req.params;
// presigned URL 생성
const params = new GetObjectCommand({
Bucket: process.env.S3_BUCKET_NAME as string + "-" + level,
Key: imageName,
});
...
});

나는 isEnough 미들웨어를 사용하여 인가를 처리하였는데, 해당 내용은 조만간 다른 포스트에서 다루려 한다.

블로그의 프로필 사진

블로그의 정보

Ayden's journal

Beard Weard Ayden

활동하기