auth_router.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. """
  2. 认证API路由
  3. 提供用户注册、登录、Token验证和刷新的API端点
  4. 需求: 3.1, 4.1, 4.2, 4.3
  5. """
  6. from decimal import Decimal
  7. from fastapi import APIRouter, Depends, HTTPException, status, Request, Form
  8. from pydantic import BaseModel
  9. from sqlalchemy.orm import Session
  10. from jose import JWTError
  11. from typing import Annotated, Optional
  12. from app.services.data_encryption_service import encryption_service
  13. from app.database import get_db
  14. from app.schemas.user_schema import UserCreate, UserResponse, TokenResponse, TokenVerifyResponse, UserLogin
  15. from app.services.auth_service import AuthService
  16. from app.services.user_service import UserService
  17. from app.services.system_config_manager import get_config_bool, get_config_float, get_config_string
  18. from app.dependencies.auth import get_current_user, oauth2_scheme
  19. from app.models.user import User
  20. from app.services.token_revocation_service import token_revocation_service
  21. router = APIRouter(prefix="/api/auth", tags=["认证"])
  22. def get_real_password(raw_password: str, encrypted: bool = True) -> str:
  23. """
  24. 获取真实密码
  25. Args:
  26. raw_password: 原始密码(可能是加密的或明文的)
  27. encrypted: 是否已加密
  28. Returns:
  29. 解密后的明文密码
  30. """
  31. if not encrypted:
  32. # 明文密码,直接返回
  33. return raw_password
  34. # 加密密码,解密后返回
  35. try:
  36. return encryption_service.decrypt(raw_password)
  37. except Exception:
  38. raise HTTPException(
  39. status_code=status.HTTP_401_UNAUTHORIZED,
  40. detail="账号或密码错误",
  41. headers={"WWW-Authenticate": "Bearer"}
  42. )
  43. @router.post("/register", response_model=UserResponse, summary="用户注册", description="创建新用户账号。前端调用时密码已加密,Swagger测试时使用明文密码需设置encrypted=false。")
  44. async def register(
  45. data: UserCreate,
  46. db: Session = Depends(get_db)
  47. ):
  48. """
  49. 用户注册
  50. **注意**: 非前端调用(如Swagger测试)时,请设置 `"encrypted": false` 并使用明文密码
  51. """
  52. # 有手机号时校验短信验证码(学校邮箱注册不需要)
  53. if data.phone:
  54. if not data.sms_code:
  55. raise HTTPException(
  56. status_code=status.HTTP_400_BAD_REQUEST,
  57. detail="请输入手机验证码"
  58. )
  59. from app.services.sms_service import sms_code_service
  60. ok = await sms_code_service.verify_code(data.phone, data.sms_code)
  61. if not ok:
  62. raise HTTPException(
  63. status_code=status.HTTP_400_BAD_REQUEST,
  64. detail="验证码错误或已过期"
  65. )
  66. # 有邮箱时校验邮箱验证码(如果提供了邮箱验证码)
  67. if data.email and data.email_code:
  68. from app.services.email_service import email_code_service
  69. ok = await email_code_service.verify_code(data.email.strip().lower(), data.email_code)
  70. if not ok:
  71. raise HTTPException(
  72. status_code=status.HTTP_400_BAD_REQUEST,
  73. detail="邮箱验证码错误或已过期"
  74. )
  75. data.password = get_real_password(data.password, data.encrypted)
  76. # 检查密码强度
  77. from app.services.password_strength_service import PasswordStrengthService
  78. from app.schemas.password_strength_schema import PasswordStrengthLevel
  79. strength_result = PasswordStrengthService.check_password_strength(data.password)
  80. if strength_result.strength == PasswordStrengthLevel.WEAK:
  81. raise HTTPException(
  82. status_code=status.HTTP_400_BAD_REQUEST,
  83. detail="密码强度太弱,请至少包含字母和数字两种字符"
  84. )
  85. # 检查注册开关
  86. if not get_config_bool("enable_registration", True):
  87. raise HTTPException(
  88. status_code=status.HTTP_403_FORBIDDEN,
  89. detail="系统暂未开放注册"
  90. )
  91. user_service = UserService(db)
  92. user = user_service.create_user(data)
  93. return UserResponse.model_validate(user)
  94. @router.post("/login", summary="用户登录", description="使用用户名/手机号和密码登录。前端调用时密码已加密,Swagger测试时使用明文密码需设置encrypted=false。")
  95. async def login(
  96. credentials: UserLogin,
  97. request: Request,
  98. db: Session = Depends(get_db)
  99. ):
  100. """
  101. 用户登录
  102. **注意**: 非前端调用(如Swagger测试)时,请设置 `"encrypted": false` 并使用明文密码
  103. """
  104. from app.services.log_service import LogService
  105. from app.services.auth_service import AuthService
  106. username = credentials.username
  107. real_password = get_real_password(credentials.password, credentials.encrypted)
  108. ip_address = request.client.host if request and request.client else None
  109. user_agent = request.headers.get("user-agent") if request else None
  110. log_service = LogService(db)
  111. auth_service = AuthService(db)
  112. user = auth_service.authenticate_user(username, real_password)
  113. if not user:
  114. log_service.log_login(
  115. user_id=username,
  116. user_type="user",
  117. login_result="failed",
  118. fail_reason="用户名或密码错误",
  119. ip_address=ip_address,
  120. user_agent=user_agent
  121. )
  122. raise HTTPException(
  123. status_code=status.HTTP_401_UNAUTHORIZED,
  124. detail="Invalid username or password",
  125. headers={"WWW-Authenticate": "Bearer"},
  126. )
  127. log_service.log_login(
  128. user_id=user.id,
  129. user_type="user",
  130. login_result="success",
  131. ip_address=ip_address,
  132. user_agent=user_agent
  133. )
  134. access_token = AuthService.create_access_token(user.id)
  135. return TokenResponse(
  136. access_token=access_token,
  137. user=UserResponse.model_validate(user)
  138. )
  139. @router.get("/verify", response_model=TokenVerifyResponse)
  140. def verify_token(
  141. current_user: User = Depends(get_current_user),
  142. db: Session = Depends(get_db)
  143. ):
  144. """
  145. 验证Token是否有效
  146. 如果Token有效,返回用户信息;如果无效,返回401错误
  147. """
  148. return TokenVerifyResponse(
  149. valid=True,
  150. user=UserResponse.model_validate(current_user)
  151. )
  152. @router.post("/refresh", response_model=TokenResponse)
  153. def refresh_token(
  154. current_user: User = Depends(get_current_user),
  155. db: Session = Depends(get_db)
  156. ):
  157. """
  158. 刷新Token
  159. 使用当前有效的Token获取新的Token(延长有效期)
  160. """
  161. # 生成新的Token
  162. new_access_token = AuthService.create_access_token(current_user.id)
  163. return TokenResponse(
  164. access_token=new_access_token,
  165. user=UserResponse.model_validate(current_user)
  166. )
  167. @router.post("/logout")
  168. def logout(
  169. current_user: User = Depends(get_current_user),
  170. token: str = Depends(oauth2_scheme),
  171. ):
  172. """
  173. 用户登出
  174. 注意:由于JWT是无状态的,服务端不存储Token,
  175. 实际的Token失效需要客户端删除本地存储的Token。
  176. 此接口主要用于记录登出日志和未来可能的Token黑名单功能。
  177. """
  178. # 撤销当前 token,并使该用户已签发 token 全部失效。
  179. try:
  180. payload = AuthService.verify_token(token)
  181. token_revocation_service.revoke_payload(payload)
  182. token_revocation_service.revoke_user_sessions(current_user.id)
  183. except JWTError:
  184. pass
  185. return {"message": "登出成功", "user_id": current_user.id}
  186. @router.get("/features", summary="获取公开功能开关", tags=["认证"])
  187. def get_feature_flags():
  188. """
  189. 返回前端需要的功能开关(无需登录)
  190. """
  191. return {
  192. "enable_openclaw": get_config_bool("enable_openclaw", True),
  193. "enable_openclaw_client": get_config_bool("enable_openclaw_client", True),
  194. "enable_openclaw_web": get_config_bool("enable_openclaw_web", True),
  195. "openclaw_client_url": get_config_string("openclaw_client_url", ""),
  196. }
  197. # ==================== 手机号验证码登录 ====================
  198. class PhoneLoginRequest(BaseModel):
  199. phone: str
  200. sms_code: str
  201. @router.post("/login/phone", summary="手机号验证码登录")
  202. async def login_by_phone(
  203. body: PhoneLoginRequest,
  204. request: Request,
  205. db: Session = Depends(get_db)
  206. ):
  207. """手机号 + 验证码登录"""
  208. from app.services.sms_service import sms_code_service
  209. from app.services.log_service import LogService
  210. ip = request.client.host if request.client else None
  211. ok = await sms_code_service.verify_code(body.phone, body.sms_code)
  212. if not ok:
  213. raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="验证码错误或已过期")
  214. user = db.query(User).filter(User.phone == body.phone).first()
  215. if not user:
  216. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="该手机号未注册")
  217. if user.status != "active":
  218. raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="账户已被禁用")
  219. LogService(db).log_login(user_id=user.id, user_type="user", login_result="success", ip_address=ip)
  220. access_token = AuthService.create_access_token(user.id)
  221. return TokenResponse(access_token=access_token, user=UserResponse.model_validate(user))
  222. # ==================== 手机验证码重置密码 ====================
  223. class ResetPasswordByPhoneRequest(BaseModel):
  224. phone: str
  225. sms_code: str
  226. new_password: str
  227. encrypted: bool = True
  228. @router.post("/reset-password/phone", summary="手机验证码修改密码")
  229. async def reset_password_by_phone(
  230. body: ResetPasswordByPhoneRequest,
  231. db: Session = Depends(get_db)
  232. ):
  233. """验证手机验证码后修改密码"""
  234. from app.services.sms_service import sms_code_service
  235. ok = await sms_code_service.verify_code(body.phone, body.sms_code)
  236. if not ok:
  237. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码错误或已过期")
  238. user = db.query(User).filter(User.phone == body.phone).first()
  239. if not user:
  240. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="该手机号未注册")
  241. real_password = get_real_password(body.new_password, body.encrypted)
  242. from app.services.password_strength_service import PasswordStrengthService
  243. from app.schemas.password_strength_schema import PasswordStrengthLevel
  244. if PasswordStrengthService.check_password_strength(real_password).strength == PasswordStrengthLevel.WEAK:
  245. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="密码强度太弱,请至少包含字母和数字两种字符")
  246. user.password_hash = AuthService.hash_password(real_password)
  247. db.commit()
  248. # 清除 bcrypt 登录缓存,防止旧密码在缓存过期前仍可登录
  249. try:
  250. from app.core.redis import redis_manager
  251. r = redis_manager.get_sync_client()
  252. if r:
  253. for key in r.keys(f"bcrypt_ok:{user.id}:*"):
  254. r.delete(key)
  255. except Exception:
  256. pass
  257. return {"code": 200, "message": "密码修改成功"}
  258. # ==================== 邮箱验证码重置密码 ====================
  259. class ResetPasswordByEmailRequest(BaseModel):
  260. email: str
  261. email_code: str
  262. new_password: str
  263. encrypted: bool = True
  264. @router.post("/reset-password/email", summary="邮箱验证码修改密码")
  265. async def reset_password_by_email(
  266. body: ResetPasswordByEmailRequest,
  267. db: Session = Depends(get_db)
  268. ):
  269. """验证邮箱验证码后修改密码"""
  270. from app.services.email_service import email_code_service
  271. email = body.email.strip().lower()
  272. ok = await email_code_service.verify_code(email, body.email_code)
  273. if not ok:
  274. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="验证码错误或已过期")
  275. user = db.query(User).filter(User.email == email).first()
  276. if not user:
  277. raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="该邮箱未绑定任何账户")
  278. real_password = get_real_password(body.new_password, body.encrypted)
  279. from app.services.password_strength_service import PasswordStrengthService
  280. from app.schemas.password_strength_schema import PasswordStrengthLevel
  281. if PasswordStrengthService.check_password_strength(real_password).strength == PasswordStrengthLevel.WEAK:
  282. raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="密码强度太弱,请至少包含字母和数字两种字符")
  283. user.password_hash = AuthService.hash_password(real_password)
  284. db.commit()
  285. # 清除 bcrypt 登录缓存
  286. try:
  287. from app.core.redis import redis_manager
  288. r = redis_manager.get_sync_client()
  289. if r:
  290. for key in r.keys(f"bcrypt_ok:{user.id}:*"):
  291. r.delete(key)
  292. except Exception:
  293. pass
  294. return {"code": 200, "message": "密码修改成功"}