This repository has been archived on 2026-10-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
NeteaseUrl/core/security.py
T
snowy-yang 9f56f68b66 chore(async-infra): 异步基础层 + 工具链 + 清理
异步基础层(core/):
- db.py   aiosqlite + WAL:读路径无锁并发、写路径 asyncio.Lock 串行,
          busy_timeout 自旋——实现「读写同时进行」(旧实现用 RLock 全读串行)
- http_client.py  aiohttp 连接池 + 有界重试(5xx 重试 / 4xx 不重试)+
          响应体上限 + 流式接口;按事件循环隔离 session(支持迁移期双 loop)

工具链:
- pyproject.toml   ruff(E/F/W/I/B/UP/C4/ASYNC/S/RUF) + pyright(basic) + pytest(asyncio auto)
- requirements-fastapi.txt  FastAPI/aiohttp/aiosqlite/aiofiles 等目标栈
- requirements-dev.txt      开发依赖(pytest/ruff/pyright)

清理:删除无用脚本 _csscheck.py 与产物标记 _css_ok.txt/_pkg_ok.txt,同步 .gitignore

验证:pytest 10/10(含「写 200 条同时 4 读协程零异常」并发用例);
ruff All checks passed;pyright 0 errors。
2026-09-26 00:24:00 +08:00

75 lines
2.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""音频直链多重防护(HMAC 签名 + 时效 + 代理流)——框架无关。
自 main.py 原样迁出,唯一改动是把密钥来源从 Flask 的 `app.secret_key`
改为**启动时注入**(`init_secret_key`),从而解除对本模块对 Flask app 的依赖;
签名/校验算法与口径保持逐字一致,迁移期新旧应用共用同一密钥即可互认 token。
"""
import base64
import hashlib
import hmac
import secrets
import time
from typing import Any
from core.config import AUDIO_HOST_MARKS, AUDIO_TOKEN_TTL, AUDIO_URL_KEYS
# 由应用启动时注入(来源:auth_system.get_secret_key() → app.secret_key)
_SECRET_KEY: bytes = b''
def init_secret_key(key) -> None:
"""注入签名密钥(str 或 bytes)。须在生成/校验音频 token 前调用一次。"""
global _SECRET_KEY
_SECRET_KEY = key.encode('utf-8') if isinstance(key, str) else key
def is_audio_url(v) -> bool:
return isinstance(v, str) and v.startswith('http') and any(m in v for m in AUDIO_HOST_MARKS)
def sign_payload(payload: str) -> str:
return hmac.new(_SECRET_KEY, payload.encode('utf-8'), hashlib.sha256).hexdigest()[:32]
def make_audio_token(url: str) -> str:
exp = int(time.time()) + AUDIO_TOKEN_TTL
nonce = secrets.token_hex(4)
payload = base64.urlsafe_b64encode(f'{exp}|{nonce}|{url}'.encode()).decode().rstrip('=')
return payload + '.' + sign_payload(payload)
def verify_audio_token(token: str) -> str | None:
try:
payload, sig = token.rsplit('.', 1)
if not hmac.compare_digest(sig, sign_payload(payload)):
return None
pad = '=' * (-len(payload) % 4)
exp, _nonce, url = base64.urlsafe_b64decode(payload + pad).decode('utf-8').split('|', 2)
if int(exp) < time.time():
return None
return url
except Exception:
return None
def protect_audio_urls(obj: Any) -> bool:
"""递归就地替换 dict/list 中的音频直链为签名代理地址,返回是否发生替换"""
hit = {'v': False}
def walk(o):
if isinstance(o, dict):
for k, v in list(o.items()):
if k in AUDIO_URL_KEYS and is_audio_url(v):
o[k] = '/audio_stream?token=' + make_audio_token(v)
hit['v'] = True
else:
walk(v)
elif isinstance(o, list):
for it in o:
walk(it)
walk(obj)
return hit['v']