- 📱 PWA 지원: 홈화면 추가 가능한 Progressive Web App - 🎨 M-Project 색상 스키마: 하늘색, 주황색, 회색, 흰색 일관된 디자인 - 📊 대시보드: 데스크톱 캘린더 뷰 + 모바일 일일 뷰 반응형 디자인 - 📥 분류 센터: Gmail 스타일 받은편지함으로 스마트 분류 시스템 - 🤖 AI 분류 제안: 키워드 기반 자동 분류 제안 및 일괄 처리 - 📷 업로드 모달: 데스크톱(파일 선택) + 모바일(카메라/갤러리) 최적화 - 🏷️ 3가지 분류: Todo(시작일), 캘린더(마감일), 체크리스트(무기한) - 📋 체크리스트: 진행률 표시 및 완료 토글 기능 - 🔄 시놀로지 연동 준비: 메일플러스 연동을 위한 구조 설계 - 📱 반응형 UI: 모든 페이지 모바일 최적화 완료
388 lines
12 KiB
Markdown
388 lines
12 KiB
Markdown
# 보안 가이드 (SECURITY.md)
|
|
|
|
## 🔐 보안 철학
|
|
|
|
Todo-Project는 **개인용 도구**로 설계되어 **편의성**과 **적절한 보안** 사이의 균형을 추구합니다.
|
|
|
|
### 보안 원칙
|
|
- **Trust but Verify**: 신뢰할 수 있는 기기에서는 간편하게, 의심스러운 접근은 차단
|
|
- **최소 권한**: 필요한 최소한의 권한만 부여
|
|
- **개인 최적화**: 개인 사용에 최적화된 보안 모델
|
|
|
|
## 🛡️ 보안 레벨
|
|
|
|
### 1. Minimal (개인용 권장)
|
|
```python
|
|
SECURITY_MINIMAL = {
|
|
"device_remember_days": 30, # 30일간 기기 기억
|
|
"require_password": False, # 기기 등록 후 비밀번호 불필요
|
|
"session_timeout": 0, # 무제한 세션
|
|
"biometric_optional": True, # 생체 인증 선택사항
|
|
"auto_login": True # 자동 로그인 활성화
|
|
}
|
|
```
|
|
|
|
**적합한 환경**: 개인 기기 (내 폰, 내 컴퓨터)에서만 사용
|
|
|
|
### 2. Balanced (일반 권장)
|
|
```python
|
|
SECURITY_BALANCED = {
|
|
"device_remember_days": 7, # 7일간 기기 기억
|
|
"require_password": True, # 주기적 비밀번호 확인
|
|
"session_timeout": 24*60, # 24시간 세션
|
|
"biometric_optional": True, # 생체 인증 선택사항
|
|
"auto_login": False # 수동 로그인
|
|
}
|
|
```
|
|
|
|
**적합한 환경**: 가끔 다른 기기에서도 접근하는 경우
|
|
|
|
### 3. Secure (높은 보안)
|
|
```python
|
|
SECURITY_SECURE = {
|
|
"device_remember_days": 1, # 1일간만 기기 기억
|
|
"require_password": True, # 매번 비밀번호 확인
|
|
"session_timeout": 60, # 1시간 세션
|
|
"biometric_required": True, # 생체 인증 필수
|
|
"auto_login": False # 수동 로그인
|
|
}
|
|
```
|
|
|
|
**적합한 환경**: 민감한 정보가 포함된 경우
|
|
|
|
## 🔑 인증 시스템
|
|
|
|
### 기기 등록 방식
|
|
|
|
#### 기기 식별
|
|
```python
|
|
class DeviceFingerprint:
|
|
"""기기 고유 식별자 생성"""
|
|
|
|
def generate_fingerprint(self, request):
|
|
"""브라우저 fingerprint 생성"""
|
|
components = [
|
|
request.headers.get('User-Agent', ''),
|
|
request.headers.get('Accept-Language', ''),
|
|
request.headers.get('Accept-Encoding', ''),
|
|
self.get_screen_resolution(), # JavaScript에서 전송
|
|
self.get_timezone(), # JavaScript에서 전송
|
|
self.get_platform_info() # JavaScript에서 전송
|
|
]
|
|
|
|
fingerprint = hashlib.sha256(
|
|
'|'.join(components).encode('utf-8')
|
|
).hexdigest()
|
|
|
|
return fingerprint[:16] # 16자리 축약
|
|
```
|
|
|
|
#### 기기 등록 프로세스
|
|
```python
|
|
class DeviceRegistration:
|
|
"""기기 등록 관리"""
|
|
|
|
async def register_device(self, user_id, device_info, user_confirmation):
|
|
"""새 기기 등록"""
|
|
|
|
# 1. 사용자 확인 (비밀번호 또는 기존 기기에서 승인)
|
|
if not await self.verify_user_identity(user_id, user_confirmation):
|
|
raise AuthenticationError("사용자 확인 실패")
|
|
|
|
# 2. 기기 정보 생성
|
|
device_id = self.generate_device_id(device_info)
|
|
device_name = device_info.get('name', '알 수 없는 기기')
|
|
|
|
# 3. 장기 토큰 생성 (30일 유효)
|
|
device_token = self.create_device_token(user_id, device_id)
|
|
|
|
# 4. 기기 정보 저장
|
|
device_record = {
|
|
"device_id": device_id,
|
|
"user_id": user_id,
|
|
"device_name": device_name,
|
|
"fingerprint": device_info['fingerprint'],
|
|
"registered_at": datetime.now(),
|
|
"last_used": datetime.now(),
|
|
"token": device_token,
|
|
"expires_at": datetime.now() + timedelta(days=30),
|
|
"is_trusted": True
|
|
}
|
|
|
|
await self.save_device_record(device_record)
|
|
return device_token
|
|
```
|
|
|
|
### 토큰 관리
|
|
|
|
#### JWT 토큰 구조
|
|
```python
|
|
class TokenManager:
|
|
"""토큰 생성 및 관리"""
|
|
|
|
def create_device_token(self, user_id, device_id):
|
|
"""장기간 유효한 기기 토큰 생성"""
|
|
payload = {
|
|
"user_id": str(user_id),
|
|
"device_id": device_id,
|
|
"token_type": "device",
|
|
"issued_at": datetime.utcnow(),
|
|
"expires_at": datetime.utcnow() + timedelta(days=30)
|
|
}
|
|
|
|
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
|
|
|
|
def create_session_token(self, user_id, device_id):
|
|
"""세션 토큰 생성"""
|
|
payload = {
|
|
"user_id": str(user_id),
|
|
"device_id": device_id,
|
|
"token_type": "session",
|
|
"issued_at": datetime.utcnow(),
|
|
"expires_at": datetime.utcnow() + timedelta(hours=24)
|
|
}
|
|
|
|
return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256")
|
|
```
|
|
|
|
#### 토큰 검증
|
|
```python
|
|
class TokenValidator:
|
|
"""토큰 검증"""
|
|
|
|
async def validate_device_token(self, token):
|
|
"""기기 토큰 검증"""
|
|
try:
|
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
|
|
|
# 토큰 타입 확인
|
|
if payload.get("token_type") != "device":
|
|
return None
|
|
|
|
# 만료 시간 확인
|
|
expires_at = datetime.fromisoformat(payload["expires_at"])
|
|
if datetime.utcnow() > expires_at:
|
|
return None
|
|
|
|
# 기기 정보 확인
|
|
device_record = await self.get_device_record(
|
|
payload["user_id"],
|
|
payload["device_id"]
|
|
)
|
|
|
|
if not device_record or not device_record["is_trusted"]:
|
|
return None
|
|
|
|
return payload
|
|
|
|
except jwt.JWTError:
|
|
return None
|
|
```
|
|
|
|
## 🔒 데이터 보안
|
|
|
|
### 데이터베이스 보안
|
|
|
|
#### 비밀번호 해싱
|
|
```python
|
|
from passlib.context import CryptContext
|
|
|
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
|
|
def hash_password(password: str) -> str:
|
|
"""비밀번호 해싱 (bcrypt)"""
|
|
return pwd_context.hash(password)
|
|
|
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
"""비밀번호 검증"""
|
|
return pwd_context.verify(plain_password, hashed_password)
|
|
```
|
|
|
|
#### 민감 정보 암호화
|
|
```python
|
|
from cryptography.fernet import Fernet
|
|
|
|
class DataEncryption:
|
|
"""민감 정보 암호화"""
|
|
|
|
def __init__(self):
|
|
self.key = settings.ENCRYPTION_KEY.encode()
|
|
self.cipher = Fernet(self.key)
|
|
|
|
def encrypt_sensitive_data(self, data: str) -> str:
|
|
"""민감 정보 암호화 (시놀로지 비밀번호 등)"""
|
|
return self.cipher.encrypt(data.encode()).decode()
|
|
|
|
def decrypt_sensitive_data(self, encrypted_data: str) -> str:
|
|
"""민감 정보 복호화"""
|
|
return self.cipher.decrypt(encrypted_data.encode()).decode()
|
|
```
|
|
|
|
### 네트워크 보안
|
|
|
|
#### HTTPS 강제
|
|
```python
|
|
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
|
|
|
|
# 프로덕션에서 HTTPS 강제
|
|
if not settings.DEBUG:
|
|
app.add_middleware(HTTPSRedirectMiddleware)
|
|
```
|
|
|
|
#### CORS 설정
|
|
```python
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.ALLOWED_ORIGINS, # 특정 도메인만 허용
|
|
allow_credentials=True,
|
|
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
|
allow_headers=["*"],
|
|
)
|
|
```
|
|
|
|
## 🚨 보안 모니터링
|
|
|
|
### 로그인 시도 모니터링
|
|
```python
|
|
class SecurityMonitor:
|
|
"""보안 모니터링"""
|
|
|
|
def __init__(self):
|
|
self.failed_attempts = {} # IP별 실패 횟수
|
|
self.blocked_ips = set() # 차단된 IP
|
|
|
|
async def record_login_attempt(self, ip_address, success):
|
|
"""로그인 시도 기록"""
|
|
if success:
|
|
# 성공 시 실패 횟수 초기화
|
|
self.failed_attempts.pop(ip_address, None)
|
|
else:
|
|
# 실패 시 횟수 증가
|
|
self.failed_attempts[ip_address] = \
|
|
self.failed_attempts.get(ip_address, 0) + 1
|
|
|
|
# 5회 실패 시 30분 차단
|
|
if self.failed_attempts[ip_address] >= 5:
|
|
self.block_ip(ip_address, minutes=30)
|
|
|
|
def block_ip(self, ip_address, minutes=30):
|
|
"""IP 주소 차단"""
|
|
self.blocked_ips.add(ip_address)
|
|
|
|
# 일정 시간 후 차단 해제
|
|
asyncio.create_task(
|
|
self.unblock_ip_after(ip_address, minutes)
|
|
)
|
|
```
|
|
|
|
### 의심스러운 활동 감지
|
|
```python
|
|
class AnomalyDetection:
|
|
"""이상 활동 감지"""
|
|
|
|
async def detect_suspicious_activity(self, user_id, activity):
|
|
"""의심스러운 활동 감지"""
|
|
|
|
# 1. 비정상적인 시간대 접근
|
|
if self.is_unusual_time(activity.timestamp):
|
|
await self.alert_unusual_time_access(user_id, activity)
|
|
|
|
# 2. 새로운 기기에서 접근
|
|
if not await self.is_known_device(user_id, activity.device_info):
|
|
await self.alert_new_device_access(user_id, activity)
|
|
|
|
# 3. 비정상적인 API 호출 패턴
|
|
if await self.is_unusual_api_pattern(user_id, activity):
|
|
await self.alert_unusual_api_pattern(user_id, activity)
|
|
```
|
|
|
|
## 🔧 보안 설정
|
|
|
|
### 환경 변수 보안
|
|
```bash
|
|
# .env 파일 보안 설정
|
|
SECRET_KEY=your-very-long-and-random-secret-key-here
|
|
ENCRYPTION_KEY=your-32-byte-encryption-key-here
|
|
|
|
# 시놀로지 인증 정보 (암호화 저장)
|
|
SYNOLOGY_USERNAME=encrypted_username
|
|
SYNOLOGY_PASSWORD=encrypted_password
|
|
|
|
# 보안 레벨 설정
|
|
SECURITY_LEVEL=minimal # minimal, balanced, secure
|
|
ENABLE_DEVICE_REGISTRATION=true
|
|
ENABLE_BIOMETRIC_AUTH=true
|
|
ENABLE_SECURITY_MONITORING=true
|
|
|
|
# 세션 설정
|
|
SESSION_TIMEOUT_MINUTES=1440 # 24시간
|
|
DEVICE_REMEMBER_DAYS=30
|
|
```
|
|
|
|
### 보안 헤더
|
|
```python
|
|
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
|
from fastapi.responses import Response
|
|
|
|
# 신뢰할 수 있는 호스트만 허용
|
|
app.add_middleware(
|
|
TrustedHostMiddleware,
|
|
allowed_hosts=["localhost", "127.0.0.1", "your-domain.com"]
|
|
)
|
|
|
|
@app.middleware("http")
|
|
async def add_security_headers(request, call_next):
|
|
"""보안 헤더 추가"""
|
|
response = await call_next(request)
|
|
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
response.headers["X-Frame-Options"] = "DENY"
|
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
|
|
|
return response
|
|
```
|
|
|
|
## 🛠️ 보안 체크리스트
|
|
|
|
### 개발 환경
|
|
- [ ] `.env` 파일이 `.gitignore`에 포함되어 있는가?
|
|
- [ ] 기본 비밀번호가 변경되었는가?
|
|
- [ ] 디버그 모드가 비활성화되어 있는가? (프로덕션)
|
|
- [ ] 로그에 민감 정보가 포함되지 않는가?
|
|
|
|
### 인증 시스템
|
|
- [ ] 비밀번호가 안전하게 해싱되어 있는가?
|
|
- [ ] JWT 토큰에 민감 정보가 포함되지 않는가?
|
|
- [ ] 토큰 만료 시간이 적절하게 설정되어 있는가?
|
|
- [ ] 기기 등록 프로세스가 안전한가?
|
|
|
|
### 네트워크 보안
|
|
- [ ] HTTPS가 활성화되어 있는가? (프로덕션)
|
|
- [ ] CORS 설정이 적절한가?
|
|
- [ ] 보안 헤더가 설정되어 있는가?
|
|
- [ ] 불필요한 포트가 차단되어 있는가?
|
|
|
|
### 데이터 보안
|
|
- [ ] 민감 정보가 암호화되어 있는가?
|
|
- [ ] 데이터베이스 접근이 제한되어 있는가?
|
|
- [ ] 백업 데이터가 안전하게 보관되어 있는가?
|
|
- [ ] 로그 파일이 안전하게 관리되어 있는가?
|
|
|
|
## 🚨 보안 사고 대응
|
|
|
|
### 사고 대응 절차
|
|
1. **즉시 조치**: 의심스러운 접근 차단
|
|
2. **영향 평가**: 피해 범위 확인
|
|
3. **복구 작업**: 시스템 정상화
|
|
4. **사후 분석**: 원인 분석 및 재발 방지
|
|
|
|
### 비상 연락처
|
|
- **시스템 관리자**: [연락처]
|
|
- **보안 담당자**: [연락처]
|
|
- **시놀로지 지원**: [연락처]
|
|
|
|
이 보안 가이드를 통해 안전하고 편리한 Todo 시스템을 구축할 수 있습니다.
|