jwt_tokens.py 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. """JWT 签发与严格校验。"""
  2. from __future__ import annotations
  3. from datetime import datetime, timedelta, timezone
  4. from uuid import uuid4
  5. import jwt
  6. from flask import current_app
  7. from dms.common.errors import AuthenticationError, InternalError, TokenExpiredError
  8. from dms.models import User
  9. ALGORITHM = "HS256"
  10. REQUIRED_CLAIMS = ("sub", "username", "roleCode", "authVersion", "iat", "exp", "jti", "iss")
  11. def _secret() -> str:
  12. secret = str(current_app.config.get("DMS_JWT_SECRET", "")).strip()
  13. if not secret:
  14. raise InternalError("JWT签名密钥未配置")
  15. return secret
  16. def issue_access_token(user: User, *, keep_signed_in: bool) -> tuple[str, int]:
  17. now = datetime.now(timezone.utc)
  18. duration_key = (
  19. "DMS_JWT_KEEP_SIGNED_IN_EXPIRES_SECONDS"
  20. if keep_signed_in
  21. else "DMS_JWT_EXPIRES_SECONDS"
  22. )
  23. expires_in = int(current_app.config[duration_key])
  24. claims = {
  25. "sub": str(user.id),
  26. "username": user.username,
  27. "roleCode": user.role_code,
  28. "authVersion": user.auth_version,
  29. "iat": now,
  30. "exp": now + timedelta(seconds=expires_in),
  31. "jti": str(uuid4()),
  32. "iss": current_app.config["DMS_JWT_ISSUER"],
  33. }
  34. return jwt.encode(claims, _secret(), algorithm=ALGORITHM), expires_in
  35. def decode_access_token(token: str) -> dict[str, object]:
  36. try:
  37. return jwt.decode(
  38. token,
  39. _secret(),
  40. algorithms=[ALGORITHM],
  41. issuer=current_app.config["DMS_JWT_ISSUER"],
  42. options={"require": list(REQUIRED_CLAIMS)},
  43. )
  44. except jwt.ExpiredSignatureError as exc:
  45. raise TokenExpiredError() from exc
  46. except jwt.PyJWTError as exc:
  47. raise AuthenticationError() from exc
  48. REAUTH_TOKEN_TYPE = "reauth"
  49. REAUTH_EXPIRES_SECONDS = 300
  50. REAUTH_REQUIRED_CLAIMS = ("sub", "type", "authVersion", "iat", "exp", "jti", "iss")
  51. def issue_reauth_token(user: User) -> tuple[str, int]:
  52. """敏感操作二次认证令牌:5 分钟有效,绑定 userId 与 authVersion(契约1.21)。"""
  53. now = datetime.now(timezone.utc)
  54. claims = {
  55. "sub": str(user.id),
  56. "type": REAUTH_TOKEN_TYPE,
  57. "authVersion": user.auth_version,
  58. "iat": now,
  59. "exp": now + timedelta(seconds=REAUTH_EXPIRES_SECONDS),
  60. "jti": str(uuid4()),
  61. "iss": current_app.config["DMS_JWT_ISSUER"],
  62. }
  63. return (
  64. jwt.encode(claims, _secret(), algorithm=ALGORITHM),
  65. REAUTH_EXPIRES_SECONDS,
  66. )
  67. def decode_reauth_token(token: str) -> dict[str, object]:
  68. try:
  69. return jwt.decode(
  70. token,
  71. _secret(),
  72. algorithms=[ALGORITHM],
  73. issuer=current_app.config["DMS_JWT_ISSUER"],
  74. options={"require": list(REAUTH_REQUIRED_CLAIMS)},
  75. )
  76. except jwt.ExpiredSignatureError as exc:
  77. raise TokenExpiredError() from exc
  78. except jwt.PyJWTError as exc:
  79. raise AuthenticationError() from exc