feat(auth): refresh token 先 Redis 后 MySQL 双写

登录 30 天会话 hash 持久化到 admin_refresh_tokens;启动从 MySQL 回填 Redis,
Redis 重启后会话仍可 refresh。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-07 21:56:30 +08:00
parent 69087988b1
commit 05006cb5df
12 changed files with 510 additions and 56 deletions
+9 -8
View File
@@ -277,8 +277,9 @@ async def change_password(
db: AsyncSession = Depends(get_db),
):
"""Change the current admin's password (requires current password verification)"""
from server.infrastructure.database.admin_repo import AdminRepositoryImpl
from server.infrastructure.database.admin_repo import AdminRepositoryImpl, LoginAttemptRepositoryImpl
from server.infrastructure.database.audit_log_repo import AuditLogRepositoryImpl
from server.infrastructure.database.refresh_token_repo import RefreshTokenRepositoryImpl
admin_repo = AdminRepositoryImpl(db)
current_admin = await admin_repo.get_by_id(admin.id)
@@ -305,13 +306,13 @@ async def change_password(
current_admin.token_version = (current_admin.token_version or 0) + 1
await admin_repo.update(current_admin)
# Clear all refresh tokens from Redis (all devices logged out)
try:
from server.infrastructure.redis.client import get_redis
redis = get_redis()
await redis.delete(f"refresh_tokens:{admin.id}")
except Exception:
logger.warning(f"Failed to clear refresh tokens from Redis for admin {admin.id}", exc_info=True)
auth_service = AuthService(
admin_repo=admin_repo,
attempt_repo=LoginAttemptRepositoryImpl(db),
audit_repo=audit_repo,
refresh_token_repo=RefreshTokenRepositoryImpl(db),
)
await auth_service._delete_all_refresh_tokens(admin.id)
# Audit log with client IP
ip_address = get_client_ip(request)