API Key 인증 완전 제거

사내 운영 전제로 X-API-Key 인증을 전면 제거.
- app/core/auth.py 삭제
- routes.py에서 Depends(require_api_key) 모두 제거
- config.Settings에서 api_keys / api_key_set 제거
- .env / .env.example에서 API_KEYS 제거
- docker-compose.yml에서 API_KEYS 환경변수 제거 (KOSIMCSE_MODEL 추가)
- UI(index.html)에서 DEFAULT_API_KEY 상수 + X-API-Key 헤더 모두 제거
- scripts/sample_curl.sh, sample_python.py에서 키 헤더 제거
- tests: test_detect_requires_api_key → test_detect_no_auth_required로 갱신
- README: 인증 컬럼 제거, curl 예시에서 헤더 제거
main
hbyang 2026-05-14 08:58:28 +09:00
parent f31ef142e8
commit 4913bf3ecc
10 changed files with 27 additions and 78 deletions

View File

@ -4,7 +4,6 @@ PORT=8000
LOG_LEVEL=info
RELOAD=false
API_KEYS=combooks-key-change-me,baikal-key-change-me
ENGINE_VERSION=o2o-plagiarism-2.1.0-kosimcse
REFERENCE_CORPUS_DIR=./data/reference
TAXONOMY_DIR=./data/taxonomy

View File

@ -74,7 +74,6 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
# 탐지
curl -X POST http://localhost:8000/v1/plagiarism/detect \
-H "Content-Type: application/json" \
-H "X-API-Key: combooks-key-change-me" \
-d '{
"doc_id": "test-001",
"text": "검사할 본문 텍스트…",
@ -84,17 +83,15 @@ curl -X POST http://localhost:8000/v1/plagiarism/detect \
# 자서전 업로드
curl -X POST http://localhost:8000/v1/corpus \
-H "Content-Type: application/json" \
-H "X-API-Key: combooks-key-change-me" \
-d '{"title": "김OO 자서전", "text": "본문…"}'
# .txt 파일 업로드
curl -X POST http://localhost:8000/v1/corpus/file \
-H "X-API-Key: combooks-key-change-me" \
-F "title=김OO 자서전" \
-F "file=@autobiography.txt"
# 코퍼스 목록
curl -H "X-API-Key: combooks-key-change-me" http://localhost:8000/v1/corpus
curl http://localhost:8000/v1/corpus
# 분류체계 조회 (10종 태그 + 39 케이스)
curl http://localhost:8000/v1/taxonomy
@ -102,21 +99,19 @@ curl http://localhost:8000/v1/taxonomy
## 엔드포인트 요약
| Method | Path | 용도 | 인증 |
|---|---|---|---|
| GET | `/` | 검토 콘솔 (HTML) | - |
| GET | `/docs` | Swagger UI | - |
| GET | `/v1/health` | 헬스체크 + 엔진 정보 | - |
| GET | `/v1/taxonomy` | 분류체계 조회 | - |
| POST | `/v1/plagiarism/detect` | 단건 동기 탐지 | ✅ |
| POST | `/v1/plagiarism/batch` | 배치 잡 등록 (≤500건) | ✅ |
| GET | `/v1/plagiarism/batch/{job_id}` | 배치 결과 조회 | ✅ |
| GET | `/v1/corpus` | 코퍼스 목록 | ✅ |
| POST | `/v1/corpus` | JSON 업로드 | ✅ |
| POST | `/v1/corpus/file` | .txt 파일 업로드 | ✅ |
| DELETE | `/v1/corpus/{doc_id}` | 삭제 | ✅ |
인증: `X-API-Key` 헤더에 발급된 키 (기관별 분리 발급 가능, 콤마 구분).
| Method | Path | 용도 |
|---|---|---|
| GET | `/` | 검토 콘솔 (HTML) |
| GET | `/docs` | Swagger UI |
| GET | `/v1/health` | 헬스체크 + 엔진 정보 |
| GET | `/v1/taxonomy` | 분류체계 조회 |
| POST | `/v1/plagiarism/detect` | 단건 동기 탐지 |
| POST | `/v1/plagiarism/batch` | 배치 잡 등록 (≤500건) |
| GET | `/v1/plagiarism/batch/{job_id}` | 배치 결과 조회 |
| GET | `/v1/corpus` | 코퍼스 목록 |
| POST | `/v1/corpus` | JSON 업로드 |
| POST | `/v1/corpus/file` | .txt 파일 업로드 |
| DELETE | `/v1/corpus/{doc_id}` | 삭제 |
## 응답 스키마 (탐지 결과)
@ -159,7 +154,6 @@ curl http://localhost:8000/v1/taxonomy
| 변수 | 기본 | 설명 |
|---|---|---|
| `API_KEYS` | `combooks-key-…,baikal-key-…` | 콤마 구분 다중 키 |
| `SIMILARITY_THRESHOLD` | `0.85` | 침해 판정 임계값 (정밀도 우선) |
| `AUTOBIOGRAPHY_MODE` | `true` | 자서전 특화 전처리 (공통표현+NER) |
| `USE_LSH_FILTER` | `true` | MinHash+LSH 1차 필터 |

View File

@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import datetime, timezone
from fastapi import APIRouter, BackgroundTasks, Depends, File, Form, HTTPException, Request, UploadFile, status
from fastapi import APIRouter, BackgroundTasks, File, Form, HTTPException, Request, UploadFile, status
from app.api.schemas import (
BatchCreatedResponse,
@ -17,7 +17,6 @@ from app.api.schemas import (
HealthResponse,
TaxonomyResponse,
)
from app.core.auth import require_api_key
from app.core.config import get_settings
from app.engine.corpus import add_document, delete_document, list_documents
from app.engine.detector import PlagiarismDetector
@ -79,7 +78,6 @@ async def taxonomy(request: Request) -> TaxonomyResponse:
"/plagiarism/detect",
response_model=DetectResponse,
tags=["plagiarism"],
dependencies=[Depends(require_api_key)],
)
async def detect(req: DetectRequest, request: Request) -> DetectResponse:
return _detector(request).detect_request(req)
@ -90,7 +88,6 @@ async def detect(req: DetectRequest, request: Request) -> DetectResponse:
response_model=BatchCreatedResponse,
status_code=status.HTTP_202_ACCEPTED,
tags=["plagiarism"],
dependencies=[Depends(require_api_key)],
)
async def batch_create(
req: BatchRequest,
@ -113,7 +110,6 @@ async def batch_create(
"/plagiarism/batch/{job_id}",
response_model=BatchStatusResponse,
tags=["plagiarism"],
dependencies=[Depends(require_api_key)],
)
async def batch_status(job_id: str, request: Request) -> BatchStatusResponse:
job = _job_store(request).get(job_id)
@ -142,7 +138,6 @@ def _rebuild(request: Request) -> int:
"/corpus",
response_model=CorpusListResponse,
tags=["corpus"],
dependencies=[Depends(require_api_key)],
)
async def corpus_list(request: Request) -> CorpusListResponse:
settings = get_settings()
@ -158,7 +153,6 @@ async def corpus_list(request: Request) -> CorpusListResponse:
response_model=CorpusUploadResponse,
status_code=status.HTTP_201_CREATED,
tags=["corpus"],
dependencies=[Depends(require_api_key)],
)
async def corpus_upload_json(req: CorpusUploadRequest, request: Request) -> CorpusUploadResponse:
"""JSON으로 자서전 1건 업로드. 인덱스 자동 재빌드."""
@ -183,7 +177,6 @@ async def corpus_upload_json(req: CorpusUploadRequest, request: Request) -> Corp
response_model=CorpusUploadResponse,
status_code=status.HTTP_201_CREATED,
tags=["corpus"],
dependencies=[Depends(require_api_key)],
)
async def corpus_upload_file(
request: Request,
@ -218,7 +211,6 @@ async def corpus_upload_file(
"/corpus/{doc_id}",
status_code=status.HTTP_204_NO_CONTENT,
tags=["corpus"],
dependencies=[Depends(require_api_key)],
)
async def corpus_delete(doc_id: str, request: Request) -> None:
settings = get_settings()

View File

@ -1,13 +0,0 @@
from fastapi import Header, HTTPException, status
from app.core.config import get_settings
async def require_api_key(x_api_key: str | None = Header(default=None)) -> str:
settings = get_settings()
if not x_api_key or x_api_key not in settings.api_key_set:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or missing X-API-Key header",
)
return x_api_key

View File

@ -13,7 +13,6 @@ class Settings(BaseSettings):
log_level: str = "info" # debug / info / warning / error
reload: bool = False # 개발용 자동 재시작
api_keys: str = "combooks-key-change-me,baikal-key-change-me"
engine_version: str = "o2o-plagiarism-2.0.0-pdf-v1.2"
reference_corpus_dir: str = "./data/reference"
taxonomy_dir: str = "./data/taxonomy"
@ -49,10 +48,6 @@ class Settings(BaseSettings):
autobiography_mode: bool = True
enable_entity_masking: bool = True
@property
def api_key_set(self) -> set[str]:
return {k.strip() for k in self.api_keys.split(",") if k.strip()}
@property
def corpus_path(self) -> Path:
return Path(self.reference_corpus_dir).resolve()

View File

@ -358,9 +358,6 @@
</footer>
<script>
// 검토 콘솔 내부 호출에 자동 첨부할 기본 키 (사내 운영용)
const DEFAULT_API_KEY = "combooks-key-change-me";
const SAMPLES = {
copy: {
title: "어미만 변경한 표절 (홍길동전 기반)",
@ -423,7 +420,6 @@ async function runDetect() {
const docId = document.getElementById("doc-id").value.trim() || `web-${Date.now()}`;
const title = document.getElementById("title").value.trim();
const author = document.getElementById("author").value.trim();
const apiKey = DEFAULT_API_KEY;
const thresholdVal = parseFloat(document.getElementById("threshold-slider").value);
const autobioMode = document.querySelector('input[name="autobio-mode"]:checked').value;
@ -451,7 +447,7 @@ async function runDetect() {
try {
const resp = await fetch("/v1/plagiarism/detect", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!resp.ok) {
@ -614,11 +610,10 @@ document.querySelectorAll(".tab-btn").forEach((btn) => {
// ========== 코퍼스 관리 ==========
async function loadCorpus() {
const apiKey = DEFAULT_API_KEY;
const tbody = document.getElementById("corpus-tbody");
tbody.innerHTML = '<tr><td colspan="4" style="color: var(--muted); text-align: center; padding: 20px;">로딩 중…</td></tr>';
try {
const resp = await fetch("/v1/corpus", { headers: { "X-API-Key": apiKey } });
const resp = await fetch("/v1/corpus");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
document.getElementById("corpus-count").textContent = data.total;
@ -647,7 +642,6 @@ async function uploadCorpus() {
const title = document.getElementById("corpus-title").value.trim();
const text = document.getElementById("corpus-text").value.trim();
const file = document.getElementById("corpus-file").files[0];
const apiKey = DEFAULT_API_KEY;
const statusEl = document.getElementById("corpus-status");
if (!title) {
@ -670,11 +664,11 @@ async function uploadCorpus() {
fd.append("title", title);
if (docId) fd.append("doc_id", docId);
fd.append("file", file);
resp = await fetch("/v1/corpus/file", { method: "POST", headers: { "X-API-Key": apiKey }, body: fd });
resp = await fetch("/v1/corpus/file", { method: "POST", body: fd });
} else {
resp = await fetch("/v1/corpus", {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ doc_id: docId || null, title, text }),
});
}
@ -700,11 +694,9 @@ async function uploadCorpus() {
async function deleteDoc(docId) {
if (!confirm(`'${docId}' 문서를 삭제하시겠습니까? 인덱스가 재빌드됩니다.`)) return;
const apiKey = DEFAULT_API_KEY;
try {
const resp = await fetch(`/v1/corpus/${encodeURIComponent(docId)}`, {
method: "DELETE",
headers: { "X-API-Key": apiKey },
});
if (!resp.ok && resp.status !== 204) {
throw new Error(`HTTP ${resp.status}`);

View File

@ -12,6 +12,7 @@ services:
REFERENCE_CORPUS_DIR: /app/data/reference
TAXONOMY_DIR: /app/data/taxonomy
AUTOBIOGRAPHY_PATTERNS_PATH: /app/data/autobiography/common_patterns.txt
KOSIMCSE_MODEL: ${KOSIMCSE_MODEL:-BM-K/KoSimCSE-roberta-multitask}
volumes:
- ./data:/app/data
restart: unless-stopped

View File

@ -3,7 +3,6 @@
set -euo pipefail
API_HOST="${API_HOST:-http://localhost:8000}"
API_KEY="${API_KEY:-combooks-key-change-me}"
echo "--- 1) Health ---"
curl -sS "${API_HOST}/v1/health" | python3 -m json.tool
@ -12,7 +11,6 @@ echo
echo "--- 2) 단건 탐지: 어린왕자 유사 텍스트 ---"
curl -sS -X POST "${API_HOST}/v1/plagiarism/detect" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${API_KEY}" \
-d '{
"doc_id": "test-001",
"text": "어린왕자는 작은 별에서 온 소년이다. 그는 별을 떠나 여러 행성을 여행하며 다양한 어른들을 만난다. 마침내 지구에 도착해 여우를 만나고 길들임의 의미를 배운다.",
@ -23,7 +21,6 @@ echo
echo "--- 3) 배치 등록 ---"
JOB_RESPONSE=$(curl -sS -X POST "${API_HOST}/v1/plagiarism/batch" \
-H "Content-Type: application/json" \
-H "X-API-Key: ${API_KEY}" \
-d '{
"items": [
{"doc_id": "b-001", "text": "앤 셜리는 상상력이 풍부한 고아 소녀로 초록 지붕 집에 입양된다."},
@ -36,5 +33,4 @@ JOB_ID=$(echo "${JOB_RESPONSE}" | python3 -c 'import json,sys; print(json.load(s
echo
echo "--- 4) 배치 결과 조회 ---"
sleep 1
curl -sS "${API_HOST}/v1/plagiarism/batch/${JOB_ID}" \
-H "X-API-Key: ${API_KEY}" | python3 -m json.tool
curl -sS "${API_HOST}/v1/plagiarism/batch/${JOB_ID}" | python3 -m json.tool

View File

@ -10,14 +10,13 @@ import time
import urllib.request
API_HOST = os.environ.get("API_HOST", "http://localhost:8000")
API_KEY = os.environ.get("API_KEY", "combooks-key-change-me")
def _post(path: str, body: dict) -> dict:
req = urllib.request.Request(
f"{API_HOST}{path}",
data=json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json", "X-API-Key": API_KEY},
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req) as resp:
@ -25,7 +24,7 @@ def _post(path: str, body: dict) -> dict:
def _get(path: str) -> dict:
req = urllib.request.Request(f"{API_HOST}{path}", headers={"X-API-Key": API_KEY})
req = urllib.request.Request(f"{API_HOST}{path}")
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())

View File

@ -5,8 +5,6 @@ from fastapi.testclient import TestClient
from app.main import app
API_KEY = "combooks-key-change-me"
def test_health():
with TestClient(app) as client:
@ -17,13 +15,14 @@ def test_health():
assert body["corpus_size"] >= 0
def test_detect_requires_api_key():
def test_detect_no_auth_required():
"""인증 제거 - 키 없이도 200 응답."""
with TestClient(app) as client:
resp = client.post(
"/v1/plagiarism/detect",
json={"doc_id": "x", "text": "테스트 본문"},
)
assert resp.status_code == 401
assert resp.status_code == 200
def test_detect_returns_schema():
@ -34,7 +33,6 @@ def test_detect_returns_schema():
"doc_id": "t-1",
"text": "어린왕자는 작은 별에서 온 소년이다. 그는 여우를 만나 길들임을 배운다.",
},
headers={"X-API-Key": API_KEY},
)
assert resp.status_code == 200
body = resp.json()
@ -53,12 +51,8 @@ def test_batch_flow():
{"doc_id": "b-1", "text": "앤 셜리는 초록 지붕 집에 입양된 소녀다."},
]
},
headers={"X-API-Key": API_KEY},
)
assert resp.status_code == 202
job_id = resp.json()["job_id"]
status = client.get(
f"/v1/plagiarism/batch/{job_id}",
headers={"X-API-Key": API_KEY},
)
status = client.get(f"/v1/plagiarism/batch/{job_id}")
assert status.status_code == 200