实现数据库的连接串加密

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())" # 生成方式: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
ENCRYPTION_KEY= ENCRYPTION_KEY=
DATABASE_ENCRYPTION_KEY="rJC2VqLg4KrlSq+DGJcYm869q4v5KB2dFAeuQTe0I50="
# ============================================ # ============================================
# 数据库配置 (PostgreSQL) # 数据库配置 (PostgreSQL)

View File

@@ -19,4 +19,5 @@ METADATA_DB_NAME="system_hub"
METADATA_DB_HOST="192.168.1.114" METADATA_DB_HOST="192.168.1.114"
METADATA_DB_PORT="5432" METADATA_DB_PORT="5432"
METADATA_DB_USER="tjwater" 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" status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user"
) )
membership_role = await metadata_repo.get_membership_role( membership_role = await metadata_repo.get_membership_role(project_uuid, user.id)
project_uuid, user.id
)
if not membership_role: if not membership_role:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="No access to project" 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: except ValueError as exc:
logger.error( logger.error(
"Missing ENCRYPTION_KEY while resolving project PostgreSQL routing", "Invalid project PostgreSQL routing DSN configuration",
exc_info=True, exc_info=True,
) )
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, 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 ) from exc
if not routing: if not routing:
raise HTTPException( raise HTTPException(
@@ -143,12 +141,12 @@ async def get_project_pg_connection(
) )
except ValueError as exc: except ValueError as exc:
logger.error( logger.error(
"Missing ENCRYPTION_KEY while resolving project PostgreSQL routing", "Invalid project PostgreSQL routing DSN configuration",
exc_info=True, exc_info=True,
) )
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, 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 ) from exc
if not routing: if not routing:
raise HTTPException( raise HTTPException(
@@ -184,12 +182,12 @@ async def get_project_timescale_connection(
) )
except ValueError as exc: except ValueError as exc:
logger.error( logger.error(
"Missing ENCRYPTION_KEY while resolving project TimescaleDB routing", "Invalid project TimescaleDB routing DSN configuration",
exc_info=True, exc_info=True,
) )
raise HTTPException( raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, 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 ) from exc
if not routing: if not routing:
raise HTTPException( raise HTTPException(

View File

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

View File

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

View File

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