重构: 切换存储至SQLite,启用INI配置与API Key校验

This commit is contained in:
2026-02-09 17:10:11 +08:00
parent d5edbc0723
commit b9828a1b13
30 changed files with 2721 additions and 612 deletions

155
src/gasflux/auth.py Normal file
View File

@ -0,0 +1,155 @@
"""
Authentication Module
Handles API key authentication and management.
"""
import hmac
import secrets
import hashlib
import os
from functools import wraps
from flask import request, abort, current_app, g
from .db import get_db
def hash_api_key(key: str, salt: str = None) -> tuple[str, str]:
"""Hash API key with salt for secure storage."""
if salt is None:
salt = secrets.token_hex(16)
# Use PBKDF2 for key derivation
key_hash = hashlib.pbkdf2_hmac(
'sha256',
key.encode('utf-8'),
salt.encode('utf-8'),
100000 # High iteration count for security
).hex()
return key_hash, salt
def verify_api_key(provided_key: str, stored_hash: str, salt: str) -> bool:
"""Verify provided API key against stored hash."""
expected_hash, _ = hash_api_key(provided_key, salt)
return hmac.compare_digest(expected_hash, stored_hash)
def require_api_key(f):
"""Decorator to require valid API key authentication."""
@wraps(f)
def decorated_function(*args, **kwargs):
# Check for bootstrap admin key (for initial key creation)
bootstrap_key = os.environ.get('ADMIN_BOOTSTRAP_KEY') or current_app.config.get('ADMIN_BOOTSTRAP_KEY')
if bootstrap_key:
provided_bootstrap = request.headers.get('X-Admin-Bootstrap-Key')
if provided_bootstrap and hmac.compare_digest(provided_bootstrap, bootstrap_key):
return f(*args, **kwargs)
# Check for regular API key
provided_key = request.headers.get('X-API-Key')
if not provided_key:
current_app.logger.warning(f"API key missing from request: {request.method} {request.path}")
abort(401, "API key required")
# Query database for key
db = get_db()
row = db.execute(
"SELECT key_hash, salt, revoked, scopes FROM api_keys WHERE key_id = ?",
(provided_key,)
).fetchone()
if not row:
current_app.logger.warning(f"Unknown API key used: {request.method} {request.path}")
abort(401, "Invalid API key")
if row['revoked']:
current_app.logger.warning(f"Revoked API key used: {request.method} {request.path}")
abort(401, "API key revoked")
# Verify key
if not verify_api_key(provided_key, row['key_hash'], row['salt']):
current_app.logger.warning(f"Invalid API key hash: {request.method} {request.path}")
abort(401, "Invalid API key")
# Update last used timestamp
db.execute(
"UPDATE api_keys SET last_used_at = datetime('now', '+8 hours') WHERE key_id = ?",
(provided_key,)
)
db.commit()
# Store key info in request context
g.api_key_id = provided_key
g.api_key_scopes = row['scopes'] or '*'
return f(*args, **kwargs)
return decorated_function
def create_api_key(description: str = "", scopes: str = "*") -> tuple[str, str]:
"""Create a new API key."""
key = secrets.token_urlsafe(32) # Generate secure random key
key_hash, salt = hash_api_key(key)
db = get_db()
db.execute(
"INSERT INTO api_keys (key_id, key_hash, salt, scopes, description) VALUES (?, ?, ?, ?, ?)",
(key, key_hash, salt, scopes, description)
)
db.commit()
current_app.logger.info(f"Created new API key: {key[:8]}... (description: {description})")
return key, key # Return both hashed and plain versions (plain only once)
def revoke_api_key(key_id: str) -> bool:
"""Revoke an API key."""
db = get_db()
result = db.execute(
"UPDATE api_keys SET revoked = 1 WHERE key_id = ?",
(key_id,)
)
db.commit()
if result.rowcount > 0:
current_app.logger.info(f"Revoked API key: {key_id[:8]}...")
return True
return False
def list_api_keys() -> list[dict]:
"""List all API keys (without sensitive data)."""
db = get_db()
rows = db.execute(
"SELECT key_id, scopes, description, created_at, last_used_at, revoked FROM api_keys ORDER BY created_at DESC"
).fetchall()
return [{
'key_id': row['key_id'],
'scopes': row['scopes'],
'description': row['description'],
'created_at': row['created_at'],
'last_used_at': row['last_used_at'],
'revoked': bool(row['revoked'])
} for row in rows]
def get_api_key_info(key_id: str) -> dict | None:
"""Get information about a specific API key."""
db = get_db()
row = db.execute(
"SELECT key_id, scopes, description, created_at, last_used_at, revoked FROM api_keys WHERE key_id = ?",
(key_id,)
).fetchone()
if row:
return {
'key_id': row['key_id'],
'scopes': row['scopes'],
'description': row['description'],
'created_at': row['created_at'],
'last_used_at': row['last_used_at'],
'revoked': bool(row['revoked'])
}
return None