jwt_utils.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. """JWT工具 - 标准RFC7519格式 + Redis持久化Refresh Token"""
  2. import os
  3. import secrets
  4. import jwt
  5. from datetime import datetime, timedelta, timezone
  6. _ISSUER = "trip-planner-pro"
  7. # 密钥(首次运行自动生成)
  8. _SECRET_KEY = None
  9. def _get_secret() -> str:
  10. global _SECRET_KEY
  11. if _SECRET_KEY is None:
  12. key = os.getenv("JWT_SECRET")
  13. if not key:
  14. key = os.urandom(32).hex()
  15. os.environ["JWT_SECRET"] = key
  16. _SECRET_KEY = key
  17. return _SECRET_KEY
  18. ALGORITHM = "HS256"
  19. # 过期时间
  20. ACCESS_TOKEN_EXPIRE_MINUTES = 30
  21. REFRESH_TOKEN_EXPIRE_DAYS = 7
  22. def _now() -> datetime:
  23. return datetime.now(timezone.utc)
  24. def create_access_token(user_id: int) -> str:
  25. """生成标准 Access Token(30分钟有效,HttpOnly Cookie传递)"""
  26. now = _now()
  27. payload = {
  28. "iss": _ISSUER,
  29. "sub": str(user_id),
  30. "aud": f"{_ISSUER}/api",
  31. "exp": now + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
  32. "iat": now,
  33. "jti": secrets.token_hex(16),
  34. "type": "access",
  35. }
  36. return jwt.encode(payload, _get_secret(), algorithm=ALGORITHM)
  37. def create_refresh_token(user_id: int) -> tuple:
  38. """
  39. 生成标准 Refresh Token(7天有效,jti存入Redis)
  40. 返回: (token_str, jti)
  41. """
  42. now = _now()
  43. jti = secrets.token_hex(16)
  44. payload = {
  45. "iss": _ISSUER,
  46. "sub": str(user_id),
  47. "aud": f"{_ISSUER}/auth/refresh",
  48. "exp": now + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS),
  49. "iat": now,
  50. "jti": jti,
  51. "type": "refresh",
  52. }
  53. token = jwt.encode(payload, _get_secret(), algorithm=ALGORITHM)
  54. return token, jti
  55. def verify_access_token(token: str) -> dict:
  56. """验证 Access Token,返回 {"id": user_id}"""
  57. payload = jwt.decode(
  58. token,
  59. _get_secret(),
  60. algorithms=[ALGORITHM],
  61. audience=f"{_ISSUER}/api",
  62. issuer=_ISSUER,
  63. options={"require": ["exp", "iat", "sub", "jti", "type"]},
  64. )
  65. if payload.get("type") != "access":
  66. raise jwt.InvalidTokenError("Token类型错误")
  67. return {"id": int(payload["sub"])}
  68. def verify_refresh_token(token: str) -> dict:
  69. """验证 Refresh Token(仅JWT签名验证),返回 {"id": user_id, "jti": jti}"""
  70. payload = jwt.decode(
  71. token,
  72. _get_secret(),
  73. algorithms=[ALGORITHM],
  74. audience=f"{_ISSUER}/auth/refresh",
  75. issuer=_ISSUER,
  76. options={"require": ["exp", "iat", "sub", "jti", "type"]},
  77. )
  78. if payload.get("type") != "refresh":
  79. raise jwt.InvalidTokenError("Token类型错误")
  80. return {"id": int(payload["sub"]), "jti": payload["jti"]}
  81. def get_token_jti(token: str) -> str:
  82. """解码token获取jti(不验证签名,仅用于找回jti)"""
  83. try:
  84. payload = jwt.decode(token, options={"verify_signature": False})
  85. return payload.get("jti", "")
  86. except Exception:
  87. return ""