feat: 대시보드 로그인을 아이디/비밀번호 방식으로 전환 (SECRET_KEY 직접 입력 제거)

- auth.py 신규: POST /api/v1/auth/login → JWT 세션 토큰(HS256, SECRET_KEY 서명, 24h)
  GET /api/v1/auth/me 자격 확인. IP당 5회 실패 시 300초 잠금(인메모리)
- config: ADMIN_USERNAME / ADMIN_PASSWORD / ADMIN_PASSWORD_HASH(bcrypt) /
  SESSION_TOKEN_EXPIRE_HOURS / LOGIN_MAX_FAILURES / LOGIN_LOCKOUT_SECONDS
- security: create_session_token / verify_session_token / is_valid_credential 추가.
  require_api_key·optional_api_key 는 시그니처 유지, X-API-Key 값으로 API 키 또는
  세션 토큰 모두 수용 → 프론트 api.ts·useWebSocket·MCP 서버 호출 경로 무변경
- main.py WebSocket 인증도 is_valid_credential 로 통일
- bcrypt 검증은 passlib 대신 bcrypt 직접 사용 (passlib 1.7.4 ↔ bcrypt 5.0 비호환)
- Login.tsx: 아이디/비밀번호 폼, 401/429/503 별 안내 메시지
- .env.example 에 관리자 계정 항목 추가

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-20 00:06:23 +09:00
parent f8c4612650
commit 9063a9d01e
8 changed files with 269 additions and 70 deletions

View File

@@ -610,13 +610,11 @@ async def websocket_endpoint(websocket: WebSocket, client_id: str):
→ {"type": "chart_data", ...}
그 외 메시지 → echo
"""
# WebSocket 인증: 운영 환경에서는 토큰 검증 (API_KEY 또는 SECRET_KEY 허용)
# WebSocket 인증: 운영 환경에서는 토큰 검증 (API_KEY/SECRET_KEY 또는 로그인 세션 토큰 허용)
if not settings.DEBUG:
import hmac
from app.core.security import _get_api_key
from app.core.security import is_valid_credential
token = websocket.query_params.get("token", "")
expected = _get_api_key()
if not token or not hmac.compare_digest(token.encode(), expected.encode()):
if not token or not is_valid_credential(token):
await websocket.close(code=4001, reason="Unauthorized")
return