from rest_framework.permissions import AllowAny from rest_framework_simplejwt.views import TokenRefreshView from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.exceptions import TokenError from drf_spectacular.utils import extend_schema from config.exceptions import AppError from apps.user.utils.jwt_redis import revoke_token, is_token_revoked, get_user_invalid_before @extend_schema(tags=['认证']) class CustomTokenRefreshView(TokenRefreshView): """U8 刷新 Token — 在 simplejwt 旋转前后加入 Redis 黑名单检查 + 旧 token 吊销""" permission_classes = [AllowAny] authentication_classes = () def post(self, request, *args, **kwargs): refresh_raw = request.data.get('refresh', '') if not refresh_raw: raise AppError('AUTH_TOKEN_INVALID', '请提供 refresh token', status_code=401) # ── 解析旧 token(必须在 super().post() 之前,因为 simplejwt 会 mutate) ── try: old_token = RefreshToken(refresh_raw) except TokenError: raise AppError('AUTH_TOKEN_INVALID', 'refresh token 无效或已过期', status_code=401) old_jti = old_token.payload.get('jti') old_exp = old_token.payload.get('exp') uid = old_token.payload.get('user_id') iat = old_token.payload.get('iat') # ── Redis 黑名单检查 ── if old_jti and is_token_revoked(old_jti): raise AppError('AUTH_TOKEN_INVALID', 'refresh token 已被吊销', status_code=401) # ── 用户级失效截止检查 ── if uid and iat is not None: invalid_before = get_user_invalid_before(uid) if invalid_before is not None and iat < invalid_before: raise AppError('AUTH_TOKEN_INVALID', 'token 已失效,请重新登录', status_code=401) # ── 交给 simplejwt 处理旋转 ── response = super().post(request, *args, **kwargs) # ── 吊销旧 refresh token ── if old_jti and old_exp: revoke_token(old_jti, old_exp) return response # 函数式引用,供 urls.py 保持一致风格 refresh_token = CustomTokenRefreshView.as_view()