实现数据库的连接串加密

This commit is contained in:
2026-02-25 16:36:53 +08:00
parent 0bc4058f23
commit 52ccb8abf1
6 changed files with 75 additions and 31 deletions

View File

@@ -12,6 +12,7 @@ SECRET_KEY=your-secret-key-here-change-in-production-use-openssl-rand-hex-32
# 数据加密密钥 - 用于敏感数据加密
# 生成方式: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
ENCRYPTION_KEY=
DATABASE_ENCRYPTION_KEY="rJC2VqLg4KrlSq+DGJcYm869q4v5KB2dFAeuQTe0I50="
# ============================================
# 数据库配置 (PostgreSQL)

View File

@@ -19,4 +19,5 @@ METADATA_DB_NAME="system_hub"
METADATA_DB_HOST="192.168.1.114"
METADATA_DB_PORT="5432"
METADATA_DB_USER="tjwater"
METADATA_DB_PASSWORD="Tjwater@123456"
METADATA_DB_PASSWORD="Tjwater@123456"
DATABASE_ENCRYPTION_KEY="rJC2VqLg4KrlSq+DGJcYm869q4v5KB2dFAeuQTe0I50="

View File

@@ -68,9 +68,7 @@ async def get_project_context(
status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
)
membership_role = await metadata_repo.get_membership_role(
project_uuid, user.id
)
membership_role = await metadata_repo.get_membership_role(project_uuid, user.id)
if not membership_role:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="No access to project"
@@ -102,12 +100,12 @@ async def get_project_pg_session(
)
except ValueError as exc:
logger.error(
"Missing ENCRYPTION_KEY while resolving project PostgreSQL routing",
"Invalid project PostgreSQL routing DSN configuration",
exc_info=True,
)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="ENCRYPTION_KEY is not configured for project PostgreSQL access",
detail=f"Project PostgreSQL routing DSN is invalid: {exc}",
) from exc
if not routing:
raise HTTPException(
@@ -143,12 +141,12 @@ async def get_project_pg_connection(
)
except ValueError as exc:
logger.error(
"Missing ENCRYPTION_KEY while resolving project PostgreSQL routing",
"Invalid project PostgreSQL routing DSN configuration",
exc_info=True,
)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="ENCRYPTION_KEY is not configured for project PostgreSQL access",
detail=f"Project PostgreSQL routing DSN is invalid: {exc}",
) from exc
if not routing:
raise HTTPException(
@@ -184,12 +182,12 @@ async def get_project_timescale_connection(
)
except ValueError as exc:
logger.error(
"Missing ENCRYPTION_KEY while resolving project TimescaleDB routing",
"Invalid project TimescaleDB routing DSN configuration",
exc_info=True,
)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="ENCRYPTION_KEY is not configured for project TimescaleDB access",
detail=f"Project TimescaleDB routing DSN is invalid: {exc}",
) from exc
if not routing:
raise HTTPException(

View File

@@ -17,6 +17,7 @@ class Settings(BaseSettings):
# 数据加密密钥 (使用 Fernet)
ENCRYPTION_KEY: str = "" # 必须从环境变量设置
DATABASE_ENCRYPTION_KEY: str = "" # project_databases.dsn_encrypted 专用
# Database Config (PostgreSQL)
DB_NAME: str = "tjwater"

View File

@@ -5,16 +5,17 @@ import os
from app.core.config import settings
class Encryptor:
"""
使用 Fernet (对称加密) 实现数据加密/解密
适用于加密敏感配置、用户数据等
"""
def __init__(self, key: Optional[bytes] = None):
"""
初始化加密器
Args:
key: 加密密钥,如果为 None 则从环境变量读取
"""
@@ -26,58 +27,71 @@ class Encryptor:
"Generate one using: Encryptor.generate_key()"
)
key = key_str.encode()
self.fernet = Fernet(key)
def encrypt(self, data: str) -> str:
"""
加密字符串
Args:
data: 待加密的明文字符串
Returns:
Base64 编码的加密字符串
"""
if not data:
return data
encrypted_bytes = self.fernet.encrypt(data.encode())
return encrypted_bytes.decode()
def decrypt(self, data: str) -> str:
"""
解密字符串
Args:
data: Base64 编码的加密字符串
Returns:
解密后的明文字符串
"""
if not data:
return data
decrypted_bytes = self.fernet.decrypt(data.encode())
return decrypted_bytes.decode()
@staticmethod
def generate_key() -> str:
"""
生成新的 Fernet 加密密钥
Returns:
Base64 编码的密钥字符串
"""
key = Fernet.generate_key()
return key.decode()
# 全局加密器实例(懒加载)
_encryptor: Optional[Encryptor] = None
_database_encryptor: Optional[Encryptor] = None
def is_encryption_configured() -> bool:
return bool(os.getenv("ENCRYPTION_KEY") or settings.ENCRYPTION_KEY)
def is_database_encryption_configured() -> bool:
return bool(
os.getenv("DATABASE_ENCRYPTION_KEY")
or settings.DATABASE_ENCRYPTION_KEY
or os.getenv("ENCRYPTION_KEY")
or settings.ENCRYPTION_KEY
)
def get_encryptor() -> Encryptor:
"""获取全局加密器实例"""
global _encryptor
@@ -85,6 +99,26 @@ def get_encryptor() -> Encryptor:
_encryptor = Encryptor()
return _encryptor
def get_database_encryptor() -> Encryptor:
"""获取 project DB DSN 专用加密器实例"""
global _database_encryptor
if _database_encryptor is None:
key_str = (
os.getenv("DATABASE_ENCRYPTION_KEY")
or settings.DATABASE_ENCRYPTION_KEY
or os.getenv("ENCRYPTION_KEY")
or settings.ENCRYPTION_KEY
)
if not key_str:
raise ValueError(
"DATABASE_ENCRYPTION_KEY not found in environment variables or .env. "
"Generate one using: Encryptor.generate_key()"
)
_database_encryptor = Encryptor(key=key_str.encode())
return _database_encryptor
# 向后兼容(延迟加载)
def __getattr__(name):
if name == "encryptor":

View File

@@ -2,10 +2,16 @@ from dataclasses import dataclass
from typing import Optional, List
from uuid import UUID
from cryptography.fernet import InvalidToken
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.encryption import get_encryptor, is_encryption_configured
from app.core.encryption import (
get_database_encryptor,
get_encryptor,
is_database_encryption_configured,
is_encryption_configured,
)
from app.infra.db.metadata import models
@@ -65,9 +71,7 @@ class MetadataRepository:
def __init__(self, session: AsyncSession):
self.session = session
async def get_user_by_keycloak_id(
self, keycloak_id: UUID
) -> Optional[models.User]:
async def get_user_by_keycloak_id(self, keycloak_id: UUID) -> Optional[models.User]:
result = await self.session.execute(
select(models.User).where(models.User.keycloak_id == keycloak_id)
)
@@ -102,11 +106,16 @@ class MetadataRepository:
record = result.scalar_one_or_none()
if not record:
return None
if is_encryption_configured():
encryptor = get_encryptor()
if not is_database_encryption_configured():
raise ValueError("DATABASE_ENCRYPTION_KEY is not configured")
encryptor = get_database_encryptor()
try:
dsn = encryptor.decrypt(record.dsn_encrypted)
else:
dsn = record.dsn_encrypted
except InvalidToken:
raise ValueError(
"Failed to decrypt project DB DSN: DATABASE_ENCRYPTION_KEY mismatch "
"or invalid dsn_encrypted value"
)
dsn = _normalize_postgres_dsn(dsn)
return ProjectDbRouting(
project_id=record.project_id,