后端 FastAPI 六层结构(api/services/repositories/models/schemas/core), 统一响应信封、全局异常、日志脱敏;6 张表经 Alembic 迁移建库,SQL 留存 db/migration;登录鉴权含图形验证码(Redis GETDEL 防重放)、JWT、登录限流、 登出黑名单(TTL=令牌剩余有效期)、登录日志。 前端 Vue3 + TS + Element Plus 骨架,含登录页、路由守卫、请求封装与 Vite 代理。 工程配置:uv 锁定 Python 3.12、ruff 检查与格式化、editorconfig、gitattributes。 测试 184 项(单元 155 / 集成 29)全绿,浏览器端到端 8 项通过。 Co-Authored-By: Claude Code <noreply@anthropic.com>
40 lines
1.5 KiB
Python
40 lines
1.5 KiB
Python
"""JWT 登出黑名单。
|
||
|
||
**TTL 必须等于令牌剩余有效期**:写死固定值会导致「登出 N 分钟后又活了」;
|
||
不设过期则 Redis 会随登出次数无限增长。两者都是错的。
|
||
|
||
**范围限定**:本模块只解决「用户主动登出」。
|
||
「禁用账号 / 改密 / 强制下线」由 `users.token_version` 承担 ——
|
||
那种场景按用户维度写入 N 个黑名单键既低效,又会在 Redis 故障时完全失效
|
||
(那时离职员工仍能用旧令牌查看全员薪酬)。鉴权时二者都要检查。
|
||
"""
|
||
|
||
from app.core.constants import REDIS_KEY_TOKEN_BLACKLIST
|
||
from app.core.redis_client import get_redis
|
||
|
||
|
||
def _key(jti: str) -> str:
|
||
return REDIS_KEY_TOKEN_BLACKLIST.format(jti=jti)
|
||
|
||
|
||
def revoke(jti: str, remaining_seconds: int) -> None:
|
||
"""把令牌加入黑名单。
|
||
|
||
:param jti: 令牌唯一标识
|
||
:param remaining_seconds: 令牌剩余有效期;**必须传真实剩余值**。
|
||
非正数表示令牌已自然过期,无需拉黑,本函数直接忽略。
|
||
"""
|
||
if remaining_seconds <= 0:
|
||
return
|
||
# 用 set(ex=…) 而非 setex(…):后者在 redis-py 8 已弃用(规范禁用弃用 API)
|
||
get_redis().set(_key(jti), "1", ex=remaining_seconds)
|
||
|
||
|
||
def is_revoked(jti: str) -> bool:
|
||
"""令牌是否已被拉黑。
|
||
|
||
Redis 不可用时本函数会抛异常 —— 这是**有意的 fail-closed**:
|
||
对能查看全员薪酬的系统,宁可拒绝请求也不能放行无法确定状态的令牌。
|
||
"""
|
||
return bool(get_redis().exists(_key(jti)))
|