728x90
반응형
BaaS(Backend as a Service)의 등장으로 프론트엔드에서 API를 거치지 않고 DB를 직접 쿼리하는 일이 잦아졌습니다. 이는 개발 속도를 비약적으로 높여주지만, 자칫하면 대형 보안 사고로 이어질 수 있습니다. 이를 방어하기 위해 Supabase의 핵심 기능인 RLS(Row Level Security)를 적극 활용한 경험을 공유합니다.

1. BaaS의 편리함 이면에 숨겨진 보안 취약점
'오늘가챠' 앱에서는 유저가 카드를 뽑으면 cards 테이블에 데이터가 저장됩니다.
만약 프론트엔드 클라이언트가 Supabase anon 키(익명 접속용 퍼블릭 키)를 이용해 직접 DB 쿼리를 날린다면 어떨까요?
// 프론트엔드에서 직접 DB를 찌르는 코드 (위험함)
const { data } = await supabase.from('cards').select('*')
악의적인 해커가 개발자 도구(Network 탭)에서 anon 키를 빼내어, 남의 user_id를 넣고 쿼리를 조작한다면 타인의 인벤토리를 들여다보거나 카드를 삭제해버릴 수 있습니다. 기존 백엔드(Spring)에서는 아래처럼 매번 권한 검증 로직을 짜야 했습니다.
// PhotoService.java — 기존의 수동 권한 검증 코드
public Photo getOwnedPhoto(Long userId, Long photoId) {
Photo up = photoMapper.findById(photoId);
if (up == null) throw new IllegalArgumentException("업로드를 찾을 수 없습니다.");
if (!up.getUserId().equals(userId)) throw new SecurityException("권한이 없습니다.");
return up;
}
2. RLS(Row Level Security)란?
PostgreSQL 9.5부터 도입된 RLS는 애플리케이션 코드가 아닌 데이터베이스 엔진 레벨에서 특정 유저가 테이블의 '어떤 행(Row)'에 접근할 수 있는지 권한을 제어하는 기능입니다.
Supabase는 자체 구축한 Auth(인증) 시스템의 JWT 토큰과 이 RLS를 기가 막히게 연동해 두었습니다.
3. RLS 정책(Policy) 작성 실전
Supabase 대시보드(SQL Editor)를 통해 cards 테이블에 다음과 같은 정책을 선언했습니다.
-- 1. 카드 조회 정책: 오직 자신이 뽑은 카드만 볼 수 있다.
CREATE POLICY "View own cards"
ON cards FOR SELECT
USING (auth.uid() = user_id);
-- 2. 카드 추가 정책: 카드 뽑기 성공 시 자기 계정에만 저장 가능.
CREATE POLICY "Insert own cards"
ON cards FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- 3. 사진 조회 정책: 공개(public) 사진은 모두가 볼 수 있지만,
-- 비공개(private) 사진은 소유자만 볼 수 있다.
CREATE POLICY "View photos"
ON photos FOR SELECT
USING (
status = 'public'
OR auth.uid() = user_id
);
동작 원리
- 프론트엔드나 백엔드에서 Supabase로 요청을 보낼 때, 헤더에 유저가 로그인하며 발급받은 JWT(
Authorization: Bearer <token>)를 담아 보냅니다. - DB 엔진 내부에서 이 토큰을 파싱해
auth.uid()함수를 통해 요청자의 고유 UUID를 알아냅니다. - 쿼리에 명시적
WHERE절이 없더라도, DB가 알아서 자신이 소유한 행(Row)만 필터링하여 응답을 내려주거나 접근을 차단합니다.
[요청 흐름]
프론트엔드 → JWT 토큰 포함 요청 → Supabase PostgREST
→ auth.uid() 추출 → RLS 정책 자동 적용
→ "WHERE user_id = '추출된_UUID'" 자동 주입
→ 안전한 결과만 반환
실제 프로젝트의 테이블 스키마와 RLS의 연계
-- photos 테이블은 user_id를 FK로 가지며, RLS가 이 컬럼을 기준으로 필터링
CREATE TABLE IF NOT EXISTS photos (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
image_url VARCHAR(500) NOT NULL,
status VARCHAR(10) NOT NULL DEFAULT 'DRAFT',
-- ...
);
-- cards 테이블도 동일하게 user_id 기반 RLS 적용
CREATE TABLE IF NOT EXISTS cards (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
photo_id BIGINT NOT NULL REFERENCES photos(id),
-- ...
);
4. 마무리
RLS를 적용함으로써, 백엔드 서버에서 길고 장황하게 작성해야 했던 유저 권한 검증 로직이 통째로 삭제되었습니다. 프론트엔드-BaaS 직결 아키텍처를 사용할 때 발생할 수 있는 보안 취약점을 DB 엔진 층에서 가장 안전하고 우아하게 틀어막은 베스트 프랙티스입니다.
728x90
반응형