Files
aiagent/backend/app/core/security.py
renjianbo 21a06e8664 feat: complete P0-2 Token Refresh, P0-4 Android ProGuard, P0-5 App Update
- P0-2: Refresh token with Redis storage (UUID tokens, 30-day TTL, rotation on refresh)
- P0-2: Backend POST /auth/refresh and /auth/revoke endpoints
- P0-2: Android AuthInterceptor refresh-before-re-login flow
- P0-2: AuthRepository saves refresh_token on login, revokes on logout
- P0-4: Enable R8 minification (isMinifyEnabled=true) with comprehensive keep rules
- P0-5: Backend GET /api/v1/app/check-update with force_update support
- P0-5: Android AppUpdateManager with force/optional update dialogs on startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 22:20:47 +08:00

112 lines
3.3 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.
"""
安全相关功能密码加密、JWT、Refresh Token
"""
import json
import uuid
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
import bcrypt
from app.core.config import settings
# Redis client (lazy init)
_redis = None
def _get_redis():
global _redis
if _redis is None:
try:
from app.core.redis_client import get_redis_client
_redis = get_redis_client()
except Exception:
_redis = False
return _redis
# Refresh token TTL: 30 days
REFRESH_TOKEN_TTL_SEC = 30 * 24 * 3600 # 30 days
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证密码"""
try:
# bcrypt限制密码长度最多72字节
password_bytes = plain_password.encode('utf-8')
if len(password_bytes) > 72:
password_bytes = password_bytes[:72]
return bcrypt.checkpw(password_bytes, hashed_password.encode('utf-8'))
except Exception:
return False
def get_password_hash(password: str) -> str:
"""获取密码哈希"""
# bcrypt限制密码长度最多72字节
password_bytes = password.encode('utf-8')
if len(password_bytes) > 72:
password_bytes = password_bytes[:72]
# 生成盐并哈希密码
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8')
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""创建访问令牌"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY or settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
return encoded_jwt
def decode_access_token(token: str) -> Optional[dict]:
"""解码访问令牌"""
try:
payload = jwt.decode(token, settings.JWT_SECRET_KEY or settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
return payload
except JWTError:
return None
# ─── Refresh Token ──────────────────────────────────────────
def create_refresh_token(user_id: str, username: str, workspace_id: str = "") -> str:
"""创建 refresh token存储到 Redis返回 token 字符串。"""
token = str(uuid.uuid4())
data = {
"user_id": user_id,
"username": username,
"ws": workspace_id,
"created_at": datetime.utcnow().isoformat(),
}
r = _get_redis()
if r:
r.setex(f"refresh_token:{token}", REFRESH_TOKEN_TTL_SEC, json.dumps(data))
return token
def verify_refresh_token(token: str) -> Optional[dict]:
"""验证 refresh token返回存储的数据或 None。"""
r = _get_redis()
if not r:
return None
raw = r.get(f"refresh_token:{token}")
if not raw:
return None
try:
return json.loads(raw)
except Exception:
return None
def revoke_refresh_token(token: str):
"""撤销 refresh token。"""
r = _get_redis()
if r:
r.delete(f"refresh_token:{token}")