auth.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. """登录与注销接口。"""
  2. from __future__ import annotations
  3. from datetime import datetime, timezone
  4. from flask import request
  5. from sqlalchemy import select
  6. from dms.api.v1.blueprint import api_v1
  7. from dms.common.enums import AuditAction, AuditResult, EnabledStatus
  8. from dms.common.errors import (
  9. InvalidArgumentError,
  10. InvalidCredentialsError,
  11. LoginIpForbiddenError,
  12. UserDisabledError,
  13. )
  14. from dms.common.response import success_response
  15. from dms.database.transaction import for_update, transaction
  16. from dms.extensions import db
  17. from dms.models import User
  18. from dms.security.auth_context import get_auth_context
  19. from dms.security.decorators import bearer_auth_required
  20. from dms.security.jwt_tokens import issue_access_token
  21. from dms.security.passwords import verify_password
  22. from dms.services.audit_service import auth_audit, client_ip, record_login_failure
  23. from dms.services.ip_entry_service import verify_login_ip
  24. from dms.services.user_service import user_summary
  25. def _login_payload() -> tuple[str, str, bool]:
  26. if not request.is_json:
  27. raise InvalidArgumentError("请求体必须是JSON对象")
  28. body = request.get_json(silent=True)
  29. if not isinstance(body, dict):
  30. raise InvalidArgumentError("请求体必须是JSON对象")
  31. expected = {"username", "password", "keepSignedIn"}
  32. if set(body) != expected:
  33. raise InvalidArgumentError("请求体必须且只能包含username、password和keepSignedIn")
  34. username = body["username"]
  35. password = body["password"]
  36. keep_signed_in = body["keepSignedIn"]
  37. if not isinstance(username, str) or not username.strip():
  38. raise InvalidArgumentError("username不能为空")
  39. if not isinstance(password, str) or not password:
  40. raise InvalidArgumentError("password不能为空")
  41. if not isinstance(keep_signed_in, bool):
  42. raise InvalidArgumentError("keepSignedIn必须是布尔值")
  43. return username.strip(), password, keep_signed_in
  44. @api_v1.post("/auth/login")
  45. def login():
  46. username, password, keep_signed_in = _login_payload()
  47. user = db.session.scalar(
  48. select(User).where(User.username == username, User.is_deleted.is_(False))
  49. )
  50. password_valid = verify_password(
  51. user.password_hash if user is not None else None,
  52. password,
  53. )
  54. if user is None or not password_valid:
  55. record_login_failure(
  56. user=user,
  57. attempted_username=username,
  58. failure_reason="INVALID_CREDENTIALS",
  59. )
  60. raise InvalidCredentialsError()
  61. if user.status != EnabledStatus.ENABLED.value:
  62. record_login_failure(
  63. user=user,
  64. attempted_username=username,
  65. failure_reason="USER_DISABLED",
  66. )
  67. raise UserDisabledError()
  68. try:
  69. verify_login_ip(user)
  70. except LoginIpForbiddenError:
  71. record_login_failure(
  72. user=user,
  73. attempted_username=username,
  74. failure_reason="IP_NOT_ALLOWED",
  75. )
  76. raise
  77. token, expires_in = issue_access_token(user, keep_signed_in=keep_signed_in)
  78. with transaction() as session:
  79. user.last_login_at = datetime.now(timezone.utc).replace(tzinfo=None)
  80. user.last_login_ip = client_ip()
  81. user.row_version += 1
  82. session.add(
  83. auth_audit(
  84. action=AuditAction.LOGIN,
  85. result=AuditResult.SUCCESS,
  86. user=user,
  87. detail={"keepSignedIn": keep_signed_in},
  88. )
  89. )
  90. return success_response(
  91. {
  92. "accessToken": token,
  93. "tokenType": "Bearer",
  94. "expiresIn": expires_in,
  95. "user": user_summary(user),
  96. }
  97. )
  98. @api_v1.post("/auth/logout")
  99. @bearer_auth_required
  100. def logout():
  101. context = get_auth_context()
  102. with transaction() as session:
  103. user = session.scalar(for_update(select(User).where(User.id == context.user_id)))
  104. if user is None or user.is_deleted:
  105. raise InvalidCredentialsError()
  106. user.auth_version += 1
  107. user.row_version += 1
  108. session.add(
  109. auth_audit(
  110. action=AuditAction.LOGOUT,
  111. result=AuditResult.SUCCESS,
  112. user=user,
  113. detail={"authVersionIncremented": True},
  114. )
  115. )
  116. return success_response()