mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat: OAuth 绑定安全增强及限流配置优化
- 新增一次性绑定令牌机制,避免在 URL 中暴露 access_token - 绑定流程改为新标签页打开,完成后自动刷新状态 - 放宽认证相关接口的 IP 限流配置 - 登录添加 429 限流错误的友好提示 - OAuth 绑定列表添加 Provider 图标显示 Close #106
This commit is contained in:
@@ -8,10 +8,14 @@ from typing import Any, Awaitable, Optional, cast
|
||||
|
||||
from redis.asyncio import Redis
|
||||
|
||||
|
||||
OAUTH_STATE_TTL_SECONDS = 600
|
||||
OAUTH_STATE_KEY_PREFIX = "oauth_state:"
|
||||
|
||||
# OAuth bind token: 用于安全地在浏览器跳转时传递用户身份
|
||||
# 短期有效(5分钟),一次性使用
|
||||
OAUTH_BIND_TOKEN_TTL_SECONDS = 300
|
||||
OAUTH_BIND_TOKEN_KEY_PREFIX = "oauth_bind_token:"
|
||||
|
||||
|
||||
CONSUME_STATE_SCRIPT = r"""
|
||||
local value = redis.call("GET", KEYS[1])
|
||||
@@ -76,3 +80,57 @@ async def consume_oauth_state(redis: Redis, nonce: str) -> Optional[OAuthStateDa
|
||||
return None
|
||||
|
||||
return OAuthStateData.from_dict(parsed)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OAuthBindTokenData:
|
||||
"""OAuth 绑定临时令牌数据,用于浏览器跳转场景的安全认证"""
|
||||
|
||||
token: str
|
||||
user_id: str
|
||||
provider_type: str
|
||||
created_at: int
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "OAuthBindTokenData":
|
||||
return cls(
|
||||
token=str(data.get("token") or ""),
|
||||
user_id=str(data.get("user_id") or ""),
|
||||
provider_type=str(data.get("provider_type") or ""),
|
||||
created_at=int(data.get("created_at") or 0),
|
||||
)
|
||||
|
||||
|
||||
def _bind_token_key(token: str) -> str:
|
||||
return f"{OAUTH_BIND_TOKEN_KEY_PREFIX}{token}"
|
||||
|
||||
|
||||
async def create_oauth_bind_token(redis: Redis, *, user_id: str, provider_type: str) -> str:
|
||||
"""创建一次性 OAuth 绑定令牌,用于浏览器跳转场景"""
|
||||
token = secrets.token_urlsafe(32)
|
||||
data = {
|
||||
"token": token,
|
||||
"user_id": user_id,
|
||||
"provider_type": provider_type,
|
||||
"created_at": int(time.time()),
|
||||
}
|
||||
await redis.setex(_bind_token_key(token), OAUTH_BIND_TOKEN_TTL_SECONDS, json.dumps(data))
|
||||
return token
|
||||
|
||||
|
||||
async def consume_oauth_bind_token(redis: Redis, token: str) -> Optional[OAuthBindTokenData]:
|
||||
"""消费(验证并删除)OAuth 绑定令牌,返回令牌数据或 None"""
|
||||
if not token:
|
||||
return None
|
||||
|
||||
key = _bind_token_key(token)
|
||||
raw = await cast(Awaitable[Optional[str]], redis.eval(CONSUME_STATE_SCRIPT, 1, key))
|
||||
if not raw:
|
||||
return None
|
||||
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
return OAuthBindTokenData.from_dict(parsed)
|
||||
|
||||
@@ -24,12 +24,12 @@ class IPRateLimiter:
|
||||
# 默认限制配置(每分钟)
|
||||
DEFAULT_LIMITS = {
|
||||
"default": 100, # 默认限制
|
||||
"login": 5, # 登录接口
|
||||
"register": 3, # 注册接口
|
||||
"login": 20, # 登录接口
|
||||
"register": 10, # 注册接口
|
||||
"api": 60, # API 接口
|
||||
"public": 60, # 公共接口
|
||||
"verification_send": 3, # 发送验证码接口
|
||||
"verification_verify": 10, # 验证验证码接口
|
||||
"verification_send": 5, # 发送验证码接口
|
||||
"verification_verify": 20, # 验证验证码接口
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user