학습 노트

[학습 노트] ForenShield — 백엔드에서 구현한 것

sybear02 2026. 7. 29. 16:01

ForenShield — 백엔드에서 구현한 것

ForenShield 백엔드(backend-forensic)에서 코드로 붙인 기능을 중심으로 정리한 노트다. Spring Boot 쪽이고, AI GPU Worker와는 RabbitMQ·S3·결과 JSON으로 연결된다.


1. AI 분석 연동

프론트가 AI를 오래 붙잡지 않도록, 백엔드가 접수 → 큐 → 결과 저장 → 조회를 맡는다.

접수와 큐

분석 요청이 오면 영상·메타를 S3에 두고 RabbitMQ로 GPU job을 보낸다. 추론을 동기적으로 끝까지 돌리는 게 아니라, 일을 넘기고 상태를 관리하는 쪽에 가깝다. 분석용 사본을 만들고 원본과 SHA-256을 맞춰 본 뒤 큐에 넣고, 끝나면 사본을 지우는 흐름도 custody 로그와 같이 간다.

진행률

AI가 IN_PROGRESS랑 progressPercent를 올리면 AnalysisWorkerService가 받아 둔다. 진행률은 0~100으로 정규화하고, 완료 직전까지는 99를 넘기지 않는다. 메시지가 순서가 어긋나도 중간에 값이 줄어들지 않게, 이미 저장된 값과 비교해 더 큰 쪽만 반영한다.

Soft COMPLETED

얼굴이 없거나 너무 작으면 FAILED로 끊지 않고 COMPLETED에 errorCode·메시지를 붙인다. NO_HUMAN_FACE, FACE_TOO_SMALL, NO_FACE, FACE_GATE 같은 코드가 soft face-gate다. 상태 API에서는 완료로 보이되, 딥페이크 점수는 빼고 위변조 쪽만으로 risk를 잡도록 IntegratedRiskCalculator가 맞춰 둔다.

오버레이 job

모듈별 오버레이 URL은 결과와 같이 저장한다. 필요할 때만 도는 on-demand overlay API를 두고 RabbitMQ로 워커에 연결했다. 지원 모듈은 cnn, temporal, optical, forgery_spatial, forgery_temporal이다.

같은 분석 요청·같은 모듈에서 이미 끝난 job이 있으면 재사용한다. 재분석을 돌리면 예전 MP4를 물리지 않도록, 재사용 범위를 그 analysis에만 한정했다. forgery_spatial은 bbox 파이프라인이 바뀌면 예전 border 스타일 MP4를 쓰지 않고 다시 만들게 했다. overlay_jobs 테이블은 운영 RDS에서 ddl-auto=validate여도 없으면 스키마를 보강하는 initializer가 있다.

riskScore

증거 상세 API에는 TimeSformer·GMFlow 타임라인과 모듈 점수를 넣는다. Late Fusion 점수(F)와 위변조 레인 max(G)를 합친 동적 가중 riskScore는 저장할 때와 API로 읽을 때 다시 맞춘다.

둘 다 있으면 (F² + G²) / (F + G) 를 0~1로 구한 뒤 ×100이다. F만 있으면 딥페이크만, soft face-gate로 F를 못 쓰면 G만, 둘 다 없으면 LOW·0에 가깝게 둔다. AI 쪽 integrate_risk_score와 같은 식을 백엔드 util에 맞춰 두었다. 위험 구간은 대략 40 미만 LOW, 70 미만 MEDIUM, 그 위 HIGH로 나눈다.


2. 증거·사건 상세

결과가 사건·증거 화면에서 맞게 보이도록 손본 구간이다.

증거 상세 로직을 다듬고 타입별 메타데이터·DB 연동을 붙였다. 영상 증거 파이프라인도 맞춰 두었다. 케이스·마이페이지 라우팅을 손봐서 증거 상세로 바로 들어가도 동작하게 했다. 업로드 API에 caseName을 받게 했고, 분석 상태·통계 쪽 null·예외 처리도 보강했다. CoC 로그랑 상세 쪽 충돌도 정리했다.

대시보드 통계는 업로더별로 캐시하고, 분석이 끝나거나 요청이 바뀌면 invalidate해서 숫자가 오래 남지 않게 했다. S3에 둔 시각화 URL은 상세를 열 때 presigned로 다시 맞춰 주는 refresher가 있다.

Recovery Score

메타데이터 추출 상태를 바탕으로 0~100 복구 점수와 데이터 소실도를 낸다. 추출 실패면 30, 부분 성공이면 70, 성공이면 100에서 시작한다. width·height·duration·codec·fps·ffprobe JSON·추출 에러·오디오 스트림 유무처럼 빠진 항목마다 감점하고, CRITICAL·등급·factor 목록을 상세에 내려준다. CoC 체인 검증 결과와 같이 무결성·준비도 화면에 묶인다.


3. 무결성·보고서·비교

CoC 로그

증거·분석·보고서처럼 중요한 사건이 날 때마다 custody_logs에 한 줄씩 남긴다. 누가(actor), 무엇에(targetType·targetId), 무슨 행동(actionType)인지, 당시 해시·저장 경로·사유·payload·IP를 같이 적는다.

핵심은 해시 체인이다. 새 로그를 쓸 때 직전 로그의 currentLogHash를 previousLogHash로 가져오고, actor·action·해시·경로·payload·시각 등을 파이프로 이은 문자열을 SHA-256으로 돌려 currentLogHash에 넣는다. payload JSON은 DB가 공백을 바꿔도 해시가 안 깨지게 Jackson으로 정규화해서 넣었다. 시각도 밀리초 단위로 맞춰 둔다.

분석 쪽은 AnalysisCustodyLogService에서 이벤트를 나눈다. 분석용 사본 생성·원본과 SHA-256 일치 검증·사본 삭제, 큐 publish 실패·복사 실패 같은 ERROR_OCCURRED, ANALYSIS_REQUESTED·STARTED·COMPLETED·FAILED, 품질 경고 확인 등을 여기서 record 호출로 남긴다. 업로드·PDF 생성·다운로드·step-up 재인증·연장 같은 다른 구간도 같은 CustodyLogService.record로 이어진다.

검증은 해당 증거의 로그를 시간·logId 순으로 읽고, previousLogHash가 직전 행과 맞는지, currentLogHash를 다시 계산해 같은지 본다. 끊기거나 변조가 있으면 PREVIOUS_HASH_MISMATCH·HASH_MISMATCH 같은 이유로 실패한다. 이 결과를 CoC 체인 검증 API와 Recovery Score·상세 무결성 체크에서 같이 쓴다.

매니페스트·서명

증거 매니페스트를 JSON으로 만들고 PKCS#8 개인키·X.509 인증서로 서명한다. 예전에 쓰던 mock X.509 흐름을 실제 CA 연동 키·인증서 PEM(또는 Secrets Manager)으로 바꿨다. 서명 알고리즘·subject·인증서 SHA-256 지문·서명 시각을 저장하고, 보고서 무결성 스냅샷에서 VALID·INVALID·UNSIGNED를 다시 확인한다.

블록체인 앵커

보고서·매니페스트 해시를 앵커 테이블에 남기고, 모드에 따라 simulated 클라이언트나 HTTP 앵커 클라이언트로 보낸다. 하루 단위로 전날 merkle root를 올리는 스케줄러도 있다. 데모·운영에서 ledger에 찍힌 시각·tx 정보를 상세·PDF 쪽에 붙일 수 있게 해 둔 부분이다.

보고서·비교

비교 API로 두 증거를 묶어 검증 요약을 만들고, 비교·단일 증거용 PDF 리포트 생성·목록·다운로드를 연동했다. 게시 스냅샷 템플릿 버전을 두고, 리포트 custody 로그에도 compareId 등을 payload로 남긴다. 설정·알림 API와 스프린트 통합 때 BE–FE 맞춘 상세 시각화 API도 이 구간에 들어간다.


4. 보안·세션

인증·인가를 JWT·Redis·역할 가드로 나눈 구간이다. 로그인 실패 제한, 민감 조회용 step-up, 리프레시 세션, ORG_ADMIN과 일반 사용자 화면 분리가 핵심이다.

로그인 rate limit

LoginRateLimitService가 클라이언트 IP 기준으로 실패를 센다. 저장소는 프로세스 안 ConcurrentHashMap이다. IP가 비면 unknown으로 묶는다.

단계마다 실패 5회가 한도다. 첫 단계에서 5회가 차면 stage 1로 올리고 약 3분(FIRST_LOCK) 동안 막는다. 차단이 풀린 뒤 다시 5회가 차면 stage 2로 올리고 약 24시간(SECOND_LOCK) 막는다. 막혀 있는 동안 로그인 시도는 LoginRateLimitException으로 거절하고, retry-after 초를 내려준다. 3분 차단과 24시간 차단은 메시지가 다르다.

차단 시각이 지나면 blockedUntil을 지우고 failureCount를 0으로 돌린다. stage 2였으면 stage도 0으로 리셋한다. 로그인에 성공하면 그 IP 상태를 통째로 지워서, 정상 사용자에게 이전 실패가 남지 않게 한다.

Access JWT · Refresh

로그인이 성공하면 액세스 JWT와 리프레시 토큰을 발급한다. auth.refresh.enabled가 켜져 있으면 리프레시는 Redis에 유저별로 저장하고, HttpOnly 쿠키로 내려 XSS로 토큰을 읽기 어렵게 한다. Redis가 없으면 로컬·테스트용 인메모리 폴백이 있다. idle TTL은 마지막 refresh 이후 일정 분(설정값)이 지나면 세션이 끝나게 Access보다 길게 잡는 편이다.

/api/auth/refresh는 쿠키의 리프레시 JWT 검증 → Redis에 저장된 값과 일치 확인 → 새 액세스·리프레시 쌍 발급(rotation) 순서다. refresh가 꺼져 있으면 REFRESH_DISABLED로 거절하고 다시 로그인하라고 한다. 로그아웃 시 Redis에서 리프레시를 지운다.

프론트에서는 NEXT_PUBLIC_AUTH_REFRESH_ENABLED로 새로고침·직접 접속 시 쿠키로 세션을 복구할지 끈다. false면 직접 URL로 들어와도 refresh로 자동 로그인되지 않는다. 유휴 타임아웃이 끝나면 refresh 재발급 없이 로그인 화면으로 보낸다. Access는 만료 직전에 선제 refresh를 시도하는 흐름이다.

Step-up (비밀번호 재인증)

증거 상세·HLS처럼 민감한 조회 전에 일반 로그인만으로는 부족하다고 보고, 비밀번호를 한 번 더 확인한다. /api/auth/step-up/verify에서 PasswordEncoder로 맞추면 Redis에 step-up 토큰을 발급한다. 키 패턴은 STEPUP:{token} → userId이고, RefreshTokenRedisService와 같이 Redis + 인메모리 폴백이다.

프론트·API는 X-Step-Up-Token 헤더로 이 토큰을 실어 보낸다. requireValidStepUp은 토큰이 비었거나, Redis에 없거나, userId가 현재 사용자와 다르면 STEP_UP_REQUIRED(403)를 낸다.

연장은 /api/auth/step-up/extend다. 남은 TTL이 5분 이하일 때만 허용하고(STEP_UP_EXTEND_TOO_EARLY), 허용되면 설정상 step-up 만료 시간만큼 TTL을 다시 늘린다. 재인증 성공·연장은 custody에 STEP_UP_VERIFIED·STEP_UP_EXTENDED로 남긴다.

역할 가드 (ORG_ADMIN)

SecurityConfig에서 /api/v1/admin/**는 ADMIN·ORG_ADMIN만 통과한다. 반대로 일반 사용자 화면용 API인 대시보드·통계·마이페이지·비교(/api/v1/evidences/dashboard/**, stats, mypage, compare)는 INVESTIGATOR·USER·REVIEWER만 허용해서, ORG_ADMIN이 수사 화면 API를 쓰지 못하게 막았다. 그 외 evidences는 authenticated면 된다.

프론트 미들웨어도 맞춘다. UI 세션 쿠키가 없으면 로그인으로 보내고, ORG_ADMIN·ADMIN이 /main·/mypage·/compare 같은 사용자 앱 경로로 오면 /admin으로 돌린다. 일반 역할이 /admin으로 오면 /main으로 보낸다. 백엔드 API 가드와 화면 라우팅을 같이 막아 역할이 섞이지 않게 한 부분이다.


관련:
https://github.com/owhat02/backend-forensic · https://github.com/owhat02/ai-forensic · https://github.com/owhat02/frontend-deepfake

'학습 노트' 카테고리의 다른 글

[학습 노트] ForenShield 모델 원리  (0) 2026.08.02
[학습 노트] 최종 발표 FAQ  (0) 2026.07.28