Cloudflare Stream 도입기 2: 서명 URL과 재생 권한의 경계
Cloudflare Stream의 서명 URL을 붙이며 재생 권한의 경계를 다시 정리했습니다. 토큰을 발급할 수 있는 조건과 이미 발급된 토큰의 유효 범위는 따로 설계해야 했습니다.
앞선 글에서는 영상 전달을 외부 서비스에 맡기는 구조를 다뤘습니다. 이번 글은 로그인·수강권·서명 토큰이 어떻게 연결되는지에 관한 기록입니다. JWT와 HTTP 인증의 기본 개념을 알고 계신다고 가정하되, 서명 URL에서 달라지는 부분을 중심으로 설명합니다.
로그인 화면 안에 있다고 보호된 영상은 아닙니다
학생 전용 페이지에 플레이어를 넣어도, 그 안에서 사용하는 영상 주소가 공개되어 있다면 다른 경로로 접근할 가능성은 남습니다. 페이지 접근 권한과 영상 전달 권한을 따로 확인해야 하는 이유입니다. 이번에는 영상 자체에 서명을 요구하도록 설정하고, 우리 서버가 재생 토큰을 발급하는 구조를 선택했습니다.
Cloudflare Stream의 기본 영상은 ID를 아는 사람이 접근할 수 있습니다. requireSignedURLs를 켜면 ID만 사용하는 공개 접근을 제한하고 서명 토큰을 요구합니다. 허용 호스트 설정과 다운로드 제한 옵션도 제공됩니다. 이 동작의 기준은 Stream 보안 문서입니다. 이 글에서 설명하는 15분 만료와 권한 검사 순서는 우리 서비스의 구현 선택입니다.
인증 다음에는 요청한 영상에 대한 인가가 필요합니다
인증은 현재 요청을 보낸 학생이 누구인지 확인하는 단계입니다. 인가는 그 학생이 지금 이 강의를 볼 수 있는지 결정하는 단계입니다. 로그인에 성공했다는 이유만으로 다른 학생의 재생 세션이나 현재 유효한 수강권이 없는 강의의 토큰까지 발급해서는 안 됩니다.
백엔드는 학생 세션을 확인한 뒤, 요청한 playback이 그 학생의 것인지 확인합니다. 재생 세션의 유효기간, 강의 공개 상태, 영상 제공자, 영상 버전, 현재 수강권도 검사합니다. 학생이 과거에 결제했더라도 현재 유효한 수강권이 없거나, 요청한 playback의 영상 버전이 현재 강의 버전과 다르면 해당 토큰 발급 요청을 거부합니다.
학생 인증
→ 본인 재생 세션인지 확인
→ 세션 만료·강의 공개·영상 버전 확인
→ 현재 수강권 확인
→ 공급자 영상의 보호 설정 확인
→ 짧은 재생 토큰 발급
이 절차를 프론트 버튼의 표시 조건만으로 처리하지 않았습니다. 주소를 직접 호출하거나 오래 열린 화면에서 요청하더라도 서버가 같은 기준으로 판단해야 하기 때문입니다. 토큰 응답에는 캐시하지 않도록 설정하고, 발급 요청에도 사용량 제한을 적용했습니다.
서명은 암호화와 다릅니다
이번 토큰은 RS256으로 서명합니다. 서버가 가진 개인키로 서명하고 검증자는 대응하는 공개키로 변조 여부를 확인하는 방식입니다. JWT에 서명을 붙였다고 payload의 내용이 비밀이 되는 것은 아닙니다. 표현된 payload를 읽을 수 있다는 전제로 필요한 정보만 넣어야 합니다. JWT의 claim과 서명·암호화 구분은 RFC 7519를 참고했습니다.
| 필드 | 이번 구현의 값 | 목적 |
|---|---|---|
| sub | 영상 UID | 재생 대상 식별 |
| exp | 발급 시점 + 15분 | 유효기간 제한 |
| nbf | 현재 시각 − 30초 | 시계 차이 여유 |
| downloadable | false | MP4 다운로드 권한 미부여 |
토큰에는 학생 이름·이메일·학생 ID·재생 세션 ID를 넣지 않았습니다. 학생 권한은 발급 전에 우리 서버가 확인하고, 전달 서비스에는 재생에 필요한 정보만 보냅니다. 이 토큰은 소지한 사람이 사용할 수 있는 bearer token입니다. 특정 학생이나 특정 기기에 암호학적으로 묶인 주소라고 설명하면 실제 구현보다 강한 보장을 말하게 됩니다.
관리 키와 재생 토큰을 같은 비밀로 취급하지 않습니다
영상 설정을 바꾸는 관리 API 토큰과 재생 서명에 사용하는 개인키는 백엔드 런타임에만 둡니다. 브라우저에는 iframe 주소와 만료 시각만 반환합니다. 만료되는 재생 주소가 브라우저에 전달되는 것은 정상 동작이지만, 관리 키가 프론트 빌드에 들어가는 것은 전혀 다른 문제입니다.
관리자 화면의 URL 입력도 그대로 서버의 HTTP 요청 대상으로 사용하지 않습니다. 허용한 URL 형식에서 영상 UID만 추출하고, 백엔드는 정해진 공급자 API 주소를 호출합니다. 사용자 입력을 임의 외부 주소 조회로 연결하지 않으려는 선택입니다. 타임아웃과 리다이렉트 제한을 두고 공급자 오류 원문도 학생에게 전달하지 않습니다.
보호 설정이 풀렸다면 발급을 멈춥니다
등록 시에는 서버가 영상 인코딩 완료 여부와 길이를 확인합니다. 서명 필수 여부와 허용 호스트도 확인하고 필요한 설정을 적용합니다. 재생 토큰 발급 시점에도 설정과 영상 길이를 다시 확인합니다. 대시보드에서 영상이 공개 상태로 바뀌는 등 예상과 다른 상태를 만나면 발급하지 않는 보수적인 방식입니다.
대가도 있습니다. 서명 계산은 로컬에서 하지만, 현재 발급 경로는 메타데이터 조회를 위해 외부 API에 의존합니다. 공급자 API가 느리거나 장애가 나면 새 재생과 토큰 갱신이 영향을 받습니다. 조회를 캐시하면 의존성을 줄일 수 있지만 보호 설정 변경을 뒤늦게 발견할 수 있습니다. 지금은 규모보다 보호 설정 확인을 우선했으며, 캐시는 실제 사용량을 보고 판단할 과제로 남겨뒀습니다.
짧은 만료 시간에도 남는 범위가 있습니다
로그아웃한 세션으로는 새 토큰을 발급받을 수 없고, 유효한 수강권이 사라진 경우에도 이후 발급을 거부합니다. 하지만 이미 복사한 토큰을 학생별로 즉시 폐기하는 기능은 이번 구현에 없습니다. 발급된 토큰은 만료 전까지 접근 조건을 만족하는 요청에 재사용될 수 있습니다. 유효기간은 항상 발급 시점부터 15분이며, 수강권 잔여 시간과 별도로 계산합니다.
그래서 클라이언트는 만료 5분 전에 권한을 다시 확인하고 재연결합니다. 갱신이 거부되면 화면의 기존 플레이어를 제거합니다. 그렇더라도 이미 전달한 데이터를 회수하거나 화면 녹화를 막는 기능은 아닙니다. 허용 호스트와 서명 URL은 무단 접근을 어렵게 만드는 장치이며, 이 구현을 DRM이나 완전한 유포 방지라고 표현하지 않았습니다.
테스트에서 확인한 것과 아직 확인하지 못한 것
로컬 단위 테스트에서는 테스트용 RSA 키로 만든 서명을 검증했습니다. DB 통합 테스트에서는 타인의 playback, 취소된 수강권, 비공개 강의, 바뀐 영상 버전, 만료된 세션을 거부하는지 확인했습니다. 브라우저 테스트에서는 모의 플레이어를 이용해 토큰 갱신과 발급 거절을 재현했습니다.
실제 강의 영상으로 하는 검수는 아직 하지 않아, Cloudflare의 실제 인코딩과 네트워크 재생까지 검증한 것은 아닙니다. 구현한 접근 경계와 실제 영상 검증을 구분해서 기록하려고 합니다. 다음 글에서는 짧은 재생 토큰을 갱신하면서도 시청 기록을 잃지 않도록 만든 상태 관리와 진도 계산을 다룹니다.
관련 글
Cloudflare Stream 도입기 1: 영상 전달 구조와 시청 비용 계산
유료 온라인 강의를 Cloudflare Stream으로 전환하며 영상의 저장·인코딩·전달 경로를 구분했습니다. HLS와 적응형 스트리밍의 원리, 애플리케이션 서버와 CDN의 역할, 저장·전달 분량에 따른 비용 계산, 학습 진도와 과금 사용량이 달라지는 이유를 구현을 바탕으로 정리합니다.
Cloudflare R2 presigned 멀티파트 업로드 — mock 160개가 못 잡은 버그 2개
바이트가 서버를 통과하지 않는 순간, 서버는 파일 크기도 업로드 완료 여부도 스토리지에 남은 것도 잃습니다. R2 presigned 멀티파트를 붙이며 그 셋을 되찾은 과정과, mock 160개가 초록불인 채로 실물 레인 하나가 잡아낸 버그 두 개를 기록했습니다.
프로토타입 페이지를 숨기고 싶을 때, HTTP Basic Authentication
아직 공개하기 어려운 프로토타입 페이지를 쉽고 안전하게 숨기는 방법을 찾고 있나요? Next.js 미들웨어로 HTTP Basic 인증을 구현해 외부 접근과 검색 엔진 노출을 효과적으로 차단하세요.