full_server.py 78 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283
  1. #!/usr/bin/env python3
  2. """
  3. 完整的SSO服务器 - 包含认证API
  4. """
  5. import sys
  6. import os
  7. import socket
  8. import json
  9. import uuid
  10. # 添加src目录到Python路径
  11. sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
  12. # 加载环境变量
  13. from dotenv import load_dotenv
  14. load_dotenv()
  15. from fastapi import FastAPI, HTTPException, Depends, Request
  16. from fastapi.middleware.cors import CORSMiddleware
  17. from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
  18. from pydantic import BaseModel
  19. from typing import Optional
  20. import hashlib
  21. import secrets
  22. # 修复JWT导入 - 确保使用正确的JWT库
  23. try:
  24. # 首先尝试使用PyJWT
  25. import jwt as pyjwt
  26. # 测试是否有encode方法
  27. test_token = pyjwt.encode({"test": "data"}, "secret", algorithm="HS256")
  28. jwt = pyjwt
  29. print("✅ 使用PyJWT库")
  30. except (ImportError, AttributeError, TypeError) as e:
  31. print(f"PyJWT导入失败: {e}")
  32. try:
  33. # 尝试使用python-jose
  34. from jose import jwt
  35. print("✅ 使用python-jose库")
  36. except ImportError as e:
  37. print(f"python-jose导入失败: {e}")
  38. # 最后尝试安装PyJWT
  39. print("尝试安装PyJWT...")
  40. import subprocess
  41. import sys
  42. try:
  43. subprocess.check_call([sys.executable, "-m", "pip", "install", "PyJWT"])
  44. import jwt
  45. print("✅ PyJWT安装成功")
  46. except Exception as install_error:
  47. print(f"❌ PyJWT安装失败: {install_error}")
  48. raise ImportError("无法导入JWT库,请手动安装: pip install PyJWT")
  49. from datetime import datetime, timedelta, timezone
  50. import pymysql
  51. from urllib.parse import urlparse
  52. # 数据模型
  53. class LoginRequest(BaseModel):
  54. username: str
  55. password: str
  56. remember_me: bool = False
  57. class TokenResponse(BaseModel):
  58. access_token: str
  59. refresh_token: Optional[str] = None
  60. token_type: str = "Bearer"
  61. expires_in: int
  62. scope: Optional[str] = None
  63. class UserInfo(BaseModel):
  64. id: str
  65. username: str
  66. email: str
  67. phone: Optional[str] = None
  68. avatar_url: Optional[str] = None
  69. is_active: bool
  70. is_superuser: bool = False
  71. roles: list = []
  72. permissions: list = []
  73. class ApiResponse(BaseModel):
  74. code: int
  75. message: str
  76. data: Optional[dict] = None
  77. timestamp: str
  78. # 配置
  79. JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "dev-jwt-secret-key-12345")
  80. ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
  81. def check_port(port):
  82. """检查端口是否可用"""
  83. with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
  84. try:
  85. s.bind(('localhost', port))
  86. return True
  87. except OSError:
  88. return False
  89. def find_available_port(start_port=8000, max_port=8010):
  90. """查找可用端口"""
  91. for port in range(start_port, max_port + 1):
  92. if check_port(port):
  93. return port
  94. return None
  95. def get_db_connection():
  96. """获取数据库连接"""
  97. try:
  98. database_url = os.getenv('DATABASE_URL', '')
  99. if not database_url:
  100. return None
  101. parsed = urlparse(database_url)
  102. config = {
  103. 'host': parsed.hostname or 'localhost',
  104. 'port': parsed.port or 3306,
  105. 'user': parsed.username or 'root',
  106. 'password': parsed.password or '',
  107. 'database': parsed.path[1:] if parsed.path else 'sso_db',
  108. 'charset': 'utf8mb4'
  109. }
  110. return pymysql.connect(**config)
  111. except Exception as e:
  112. print(f"数据库连接失败: {e}")
  113. return None
  114. def verify_password_simple(password: str, stored_hash: str) -> bool:
  115. """验证密码(简化版)"""
  116. if stored_hash.startswith("sha256$"):
  117. parts = stored_hash.split("$")
  118. if len(parts) == 3:
  119. salt = parts[1]
  120. expected_hash = parts[2]
  121. actual_hash = hashlib.sha256((password + salt).encode()).hexdigest()
  122. return actual_hash == expected_hash
  123. return False
  124. def create_access_token(data: dict) -> str:
  125. """创建访问令牌"""
  126. to_encode = data.copy()
  127. expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
  128. to_encode.update({"exp": expire, "iat": datetime.now(timezone.utc)})
  129. encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm="HS256")
  130. return encoded_jwt
  131. def verify_token(token: str) -> Optional[dict]:
  132. """验证令牌"""
  133. try:
  134. payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=["HS256"])
  135. return payload
  136. except jwt.PyJWTError:
  137. return None
  138. # 创建FastAPI应用
  139. app = FastAPI(
  140. title="SSO认证中心",
  141. version="1.0.0",
  142. description="OAuth2单点登录认证中心",
  143. docs_url="/docs",
  144. redoc_url="/redoc"
  145. )
  146. # 配置CORS
  147. app.add_middleware(
  148. CORSMiddleware,
  149. allow_origins=["*"],
  150. allow_credentials=True,
  151. allow_methods=["*"],
  152. allow_headers=["*"],
  153. )
  154. security = HTTPBearer()
  155. @app.get("/")
  156. async def root():
  157. """根路径"""
  158. return ApiResponse(
  159. code=0,
  160. message="欢迎使用SSO认证中心",
  161. data={
  162. "name": "SSO认证中心",
  163. "version": "1.0.0",
  164. "docs": "/docs"
  165. },
  166. timestamp=datetime.now(timezone.utc).isoformat()
  167. ).model_dump()
  168. @app.get("/health")
  169. async def health_check():
  170. """健康检查"""
  171. return ApiResponse(
  172. code=0,
  173. message="服务正常运行",
  174. data={
  175. "status": "healthy",
  176. "version": "1.0.0",
  177. "timestamp": datetime.now(timezone.utc).isoformat()
  178. },
  179. timestamp=datetime.now(timezone.utc).isoformat()
  180. ).model_dump()
  181. @app.post("/api/v1/auth/login")
  182. async def login(request: Request, login_data: LoginRequest):
  183. """用户登录"""
  184. print(f"🔐 收到登录请求: username={login_data.username}")
  185. try:
  186. # 获取数据库连接
  187. print("📊 尝试连接数据库...")
  188. conn = get_db_connection()
  189. if not conn:
  190. print("❌ 数据库连接失败")
  191. return ApiResponse(
  192. code=500001,
  193. message="数据库连接失败",
  194. timestamp=datetime.now(timezone.utc).isoformat()
  195. ).model_dump()
  196. print("✅ 数据库连接成功")
  197. cursor = conn.cursor()
  198. # 查找用户
  199. print(f"🔍 查找用户: {login_data.username}")
  200. cursor.execute(
  201. "SELECT id, username, email, password_hash, is_active, is_superuser FROM users WHERE username = %s OR email = %s",
  202. (login_data.username, login_data.username)
  203. )
  204. user_data = cursor.fetchone()
  205. print(f"👤 用户查询结果: {user_data is not None}")
  206. cursor.close()
  207. conn.close()
  208. if not user_data:
  209. print("❌ 用户不存在")
  210. return ApiResponse(
  211. code=200001,
  212. message="用户名或密码错误",
  213. timestamp=datetime.now(timezone.utc).isoformat()
  214. ).model_dump()
  215. user_id, username, email, password_hash, is_active, is_superuser = user_data
  216. print(f"✅ 找到用户: {username}, 激活状态: {is_active}")
  217. # 检查用户状态
  218. if not is_active:
  219. print("❌ 用户已被禁用")
  220. return ApiResponse(
  221. code=200002,
  222. message="用户已被禁用",
  223. timestamp=datetime.now(timezone.utc).isoformat()
  224. ).model_dump()
  225. # 验证密码
  226. print(f"🔑 验证密码,哈希格式: {password_hash[:20]}...")
  227. password_valid = verify_password_simple(login_data.password, password_hash)
  228. print(f"🔑 密码验证结果: {password_valid}")
  229. if not password_valid:
  230. print("❌ 密码验证失败")
  231. return ApiResponse(
  232. code=200001,
  233. message="用户名或密码错误",
  234. timestamp=datetime.now(timezone.utc).isoformat()
  235. ).model_dump()
  236. # 生成令牌
  237. print("🎫 生成访问令牌...")
  238. token_data = {
  239. "sub": user_id,
  240. "username": username,
  241. "email": email,
  242. "is_superuser": is_superuser
  243. }
  244. access_token = create_access_token(token_data)
  245. print(f"✅ 令牌生成成功: {access_token[:50]}...")
  246. token_response = TokenResponse(
  247. access_token=access_token,
  248. token_type="Bearer",
  249. expires_in=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
  250. scope="profile email"
  251. )
  252. print("🎉 登录成功")
  253. return ApiResponse(
  254. code=0,
  255. message="登录成功",
  256. data=token_response.model_dump(),
  257. timestamp=datetime.now(timezone.utc).isoformat()
  258. ).model_dump()
  259. except Exception as e:
  260. print(f"❌ 登录错误详情: {type(e).__name__}: {str(e)}")
  261. import traceback
  262. print(f"❌ 错误堆栈: {traceback.format_exc()}")
  263. return ApiResponse(
  264. code=500001,
  265. message="服务器内部错误",
  266. timestamp=datetime.now(timezone.utc).isoformat()
  267. ).model_dump()
  268. @app.get("/api/v1/users/profile")
  269. async def get_user_profile(credentials: HTTPAuthorizationCredentials = Depends(security)):
  270. """获取用户资料"""
  271. try:
  272. # 验证令牌
  273. payload = verify_token(credentials.credentials)
  274. if not payload:
  275. return ApiResponse(
  276. code=200002,
  277. message="无效的访问令牌",
  278. timestamp=datetime.now(timezone.utc).isoformat()
  279. ).model_dump()
  280. user_id = payload.get("sub")
  281. if not user_id:
  282. return ApiResponse(
  283. code=200002,
  284. message="无效的访问令牌",
  285. timestamp=datetime.now(timezone.utc).isoformat()
  286. ).model_dump()
  287. # 获取数据库连接
  288. conn = get_db_connection()
  289. if not conn:
  290. return ApiResponse(
  291. code=500001,
  292. message="数据库连接失败",
  293. timestamp=datetime.now(timezone.utc).isoformat()
  294. ).model_dump()
  295. cursor = conn.cursor()
  296. # 查找用户详细信息
  297. cursor.execute("""
  298. SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active, u.is_superuser,
  299. u.last_login_at, u.created_at, u.updated_at,
  300. p.real_name, p.company, p.department, p.position
  301. FROM users u
  302. LEFT JOIN user_profiles p ON u.id = p.user_id
  303. WHERE u.id = %s
  304. """, (user_id,))
  305. user_data = cursor.fetchone()
  306. cursor.close()
  307. conn.close()
  308. if not user_data:
  309. return ApiResponse(
  310. code=200001,
  311. message="用户不存在",
  312. timestamp=datetime.now(timezone.utc).isoformat()
  313. ).model_dump()
  314. # 构建用户信息
  315. user_info = {
  316. "id": user_data[0],
  317. "username": user_data[1],
  318. "email": user_data[2],
  319. "phone": user_data[3],
  320. "avatar_url": user_data[4],
  321. "is_active": user_data[5],
  322. "is_superuser": user_data[6],
  323. "last_login_at": user_data[7].isoformat() if user_data[7] else None,
  324. "created_at": user_data[8].isoformat() if user_data[8] else None,
  325. "updated_at": user_data[9].isoformat() if user_data[9] else None,
  326. "real_name": user_data[10],
  327. "company": user_data[11],
  328. "department": user_data[12],
  329. "position": user_data[13]
  330. }
  331. return ApiResponse(
  332. code=0,
  333. message="获取用户资料成功",
  334. data=user_info,
  335. timestamp=datetime.now(timezone.utc).isoformat()
  336. ).model_dump()
  337. except Exception as e:
  338. print(f"获取用户资料错误: {e}")
  339. return ApiResponse(
  340. code=500001,
  341. message="服务器内部错误",
  342. timestamp=datetime.now(timezone.utc).isoformat()
  343. ).model_dump()
  344. @app.put("/api/v1/users/profile")
  345. async def update_user_profile(
  346. request: Request,
  347. profile_data: dict,
  348. credentials: HTTPAuthorizationCredentials = Depends(security)
  349. ):
  350. """更新用户资料"""
  351. try:
  352. # 验证令牌
  353. payload = verify_token(credentials.credentials)
  354. if not payload:
  355. return ApiResponse(
  356. code=200002,
  357. message="无效的访问令牌",
  358. timestamp=datetime.now(timezone.utc).isoformat()
  359. ).model_dump()
  360. user_id = payload.get("sub")
  361. # 获取数据库连接
  362. conn = get_db_connection()
  363. if not conn:
  364. return ApiResponse(
  365. code=500001,
  366. message="数据库连接失败",
  367. timestamp=datetime.now(timezone.utc).isoformat()
  368. ).model_dump()
  369. cursor = conn.cursor()
  370. # 更新用户基本信息
  371. update_fields = []
  372. update_values = []
  373. if 'email' in profile_data:
  374. update_fields.append('email = %s')
  375. update_values.append(profile_data['email'])
  376. if 'phone' in profile_data:
  377. update_fields.append('phone = %s')
  378. update_values.append(profile_data['phone'])
  379. if update_fields:
  380. update_values.append(user_id)
  381. cursor.execute(f"""
  382. UPDATE users
  383. SET {', '.join(update_fields)}, updated_at = NOW()
  384. WHERE id = %s
  385. """, update_values)
  386. # 更新或插入用户详情
  387. profile_fields = ['real_name', 'company', 'department', 'position']
  388. profile_updates = {k: v for k, v in profile_data.items() if k in profile_fields}
  389. if profile_updates:
  390. # 检查是否已有记录
  391. cursor.execute("SELECT id FROM user_profiles WHERE user_id = %s", (user_id,))
  392. profile_exists = cursor.fetchone()
  393. if profile_exists:
  394. # 更新现有记录
  395. update_fields = []
  396. update_values = []
  397. for field, value in profile_updates.items():
  398. update_fields.append(f'{field} = %s')
  399. update_values.append(value)
  400. update_values.append(user_id)
  401. cursor.execute(f"""
  402. UPDATE user_profiles
  403. SET {', '.join(update_fields)}, updated_at = NOW()
  404. WHERE user_id = %s
  405. """, update_values)
  406. else:
  407. # 插入新记录
  408. fields = ['user_id'] + list(profile_updates.keys())
  409. values = [user_id] + list(profile_updates.values())
  410. placeholders = ', '.join(['%s'] * len(values))
  411. cursor.execute(f"""
  412. INSERT INTO user_profiles ({', '.join(fields)}, created_at, updated_at)
  413. VALUES ({placeholders}, NOW(), NOW())
  414. """, values)
  415. conn.commit()
  416. cursor.close()
  417. conn.close()
  418. return ApiResponse(
  419. code=0,
  420. message="用户资料更新成功",
  421. timestamp=datetime.now(timezone.utc).isoformat()
  422. ).model_dump()
  423. except Exception as e:
  424. print(f"更新用户资料错误: {e}")
  425. return ApiResponse(
  426. code=500001,
  427. message="服务器内部错误",
  428. timestamp=datetime.now(timezone.utc).isoformat()
  429. ).model_dump()
  430. @app.put("/api/v1/users/password")
  431. async def change_user_password(
  432. request: Request,
  433. password_data: dict,
  434. credentials: HTTPAuthorizationCredentials = Depends(security)
  435. ):
  436. """修改用户密码"""
  437. try:
  438. # 验证令牌
  439. payload = verify_token(credentials.credentials)
  440. if not payload:
  441. return ApiResponse(
  442. code=200002,
  443. message="无效的访问令牌",
  444. timestamp=datetime.now(timezone.utc).isoformat()
  445. ).model_dump()
  446. user_id = payload.get("sub")
  447. old_password = password_data.get('old_password')
  448. new_password = password_data.get('new_password')
  449. if not old_password or not new_password:
  450. return ApiResponse(
  451. code=100001,
  452. message="缺少必要参数",
  453. timestamp=datetime.now(timezone.utc).isoformat()
  454. ).model_dump()
  455. # 获取数据库连接
  456. conn = get_db_connection()
  457. if not conn:
  458. return ApiResponse(
  459. code=500001,
  460. message="数据库连接失败",
  461. timestamp=datetime.now(timezone.utc).isoformat()
  462. ).model_dump()
  463. cursor = conn.cursor()
  464. # 验证当前密码
  465. cursor.execute("SELECT password_hash FROM users WHERE id = %s", (user_id,))
  466. result = cursor.fetchone()
  467. if not result or not verify_password_simple(old_password, result[0]):
  468. cursor.close()
  469. conn.close()
  470. return ApiResponse(
  471. code=200001,
  472. message="当前密码错误",
  473. timestamp=datetime.now(timezone.utc).isoformat()
  474. ).model_dump()
  475. # 生成新密码哈希
  476. new_password_hash = hash_password_simple(new_password)
  477. # 更新密码
  478. cursor.execute("""
  479. UPDATE users
  480. SET password_hash = %s, updated_at = NOW()
  481. WHERE id = %s
  482. """, (new_password_hash, user_id))
  483. conn.commit()
  484. cursor.close()
  485. conn.close()
  486. return ApiResponse(
  487. code=0,
  488. message="密码修改成功",
  489. timestamp=datetime.now(timezone.utc).isoformat()
  490. ).model_dump()
  491. except Exception as e:
  492. print(f"修改密码错误: {e}")
  493. return ApiResponse(
  494. code=500001,
  495. message="服务器内部错误",
  496. timestamp=datetime.now(timezone.utc).isoformat()
  497. ).model_dump()
  498. def hash_password_simple(password):
  499. """简单的密码哈希"""
  500. import hashlib
  501. import secrets
  502. # 生成盐值
  503. salt = secrets.token_hex(16)
  504. # 使用SHA256哈希
  505. password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
  506. return f"sha256${salt}${password_hash}"
  507. @app.post("/api/v1/auth/logout")
  508. async def logout():
  509. """用户登出"""
  510. return ApiResponse(
  511. code=0,
  512. message="登出成功",
  513. timestamp=datetime.now(timezone.utc).isoformat()
  514. ).model_dump()
  515. # OAuth2 授权端点
  516. @app.get("/oauth/authorize")
  517. async def oauth_authorize(
  518. response_type: str,
  519. client_id: str,
  520. redirect_uri: str,
  521. scope: str = "profile",
  522. state: str = None
  523. ):
  524. """OAuth2授权端点"""
  525. try:
  526. print(f"🔐 OAuth授权请求: client_id={client_id}, redirect_uri={redirect_uri}, scope={scope}")
  527. # 验证必要参数
  528. if not response_type or not client_id or not redirect_uri:
  529. error_url = f"{redirect_uri}?error=invalid_request&error_description=Missing required parameters"
  530. if state:
  531. error_url += f"&state={state}"
  532. return {"error": "invalid_request", "redirect_url": error_url}
  533. # 验证response_type
  534. if response_type != "code":
  535. error_url = f"{redirect_uri}?error=unsupported_response_type&error_description=Only authorization code flow is supported"
  536. if state:
  537. error_url += f"&state={state}"
  538. return {"error": "unsupported_response_type", "redirect_url": error_url}
  539. # 获取数据库连接
  540. conn = get_db_connection()
  541. if not conn:
  542. error_url = f"{redirect_uri}?error=server_error&error_description=Database connection failed"
  543. if state:
  544. error_url += f"&state={state}"
  545. return {"error": "server_error", "redirect_url": error_url}
  546. cursor = conn.cursor()
  547. # 验证client_id和redirect_uri
  548. cursor.execute("""
  549. SELECT id, name, redirect_uris, scope, is_active, is_trusted
  550. FROM apps
  551. WHERE app_key = %s AND is_active = 1
  552. """, (client_id,))
  553. app_data = cursor.fetchone()
  554. cursor.close()
  555. conn.close()
  556. if not app_data:
  557. error_url = f"{redirect_uri}?error=invalid_client&error_description=Invalid client_id"
  558. if state:
  559. error_url += f"&state={state}"
  560. return {"error": "invalid_client", "redirect_url": error_url}
  561. app_id, app_name, redirect_uris_json, app_scope_json, is_active, is_trusted = app_data
  562. # 验证redirect_uri
  563. redirect_uris = json.loads(redirect_uris_json) if redirect_uris_json else []
  564. if redirect_uri not in redirect_uris:
  565. error_url = f"{redirect_uri}?error=invalid_request&error_description=Invalid redirect_uri"
  566. if state:
  567. error_url += f"&state={state}"
  568. return {"error": "invalid_request", "redirect_url": error_url}
  569. # 验证scope
  570. app_scopes = json.loads(app_scope_json) if app_scope_json else []
  571. requested_scopes = scope.split() if scope else []
  572. invalid_scopes = [s for s in requested_scopes if s not in app_scopes]
  573. if invalid_scopes:
  574. error_url = f"{redirect_uri}?error=invalid_scope&error_description=Invalid scope: {' '.join(invalid_scopes)}"
  575. if state:
  576. error_url += f"&state={state}"
  577. return {"error": "invalid_scope", "redirect_url": error_url}
  578. # TODO: 检查用户登录状态
  579. # 这里应该检查用户是否已登录(通过session或cookie)
  580. # 如果未登录,应该重定向到登录页面
  581. # 临时方案:返回登录页面,让用户先登录
  582. # 生产环境应该使用session管理
  583. # 构建登录页面URL,登录后返回授权页面
  584. login_page_url = f"/oauth/login?response_type={response_type}&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}"
  585. if state:
  586. login_page_url += f"&state={state}"
  587. print(f"🔐 需要用户登录,重定向到登录页面: {login_page_url}")
  588. from fastapi.responses import RedirectResponse
  589. return RedirectResponse(url=login_page_url, status_code=302)
  590. # 非受信任应用需要用户授权确认
  591. # 这里返回授权页面HTML
  592. authorization_html = f"""
  593. <!DOCTYPE html>
  594. <html>
  595. <head>
  596. <title>授权确认 - SSO认证中心</title>
  597. <meta charset="utf-8">
  598. <meta name="viewport" content="width=device-width, initial-scale=1">
  599. <style>
  600. body {{
  601. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  602. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  603. margin: 0;
  604. padding: 20px;
  605. min-height: 100vh;
  606. display: flex;
  607. align-items: center;
  608. justify-content: center;
  609. }}
  610. .auth-container {{
  611. background: white;
  612. border-radius: 10px;
  613. padding: 40px;
  614. box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
  615. max-width: 400px;
  616. width: 100%;
  617. }}
  618. .auth-header {{
  619. text-align: center;
  620. margin-bottom: 30px;
  621. }}
  622. .auth-header h1 {{
  623. color: #333;
  624. margin-bottom: 10px;
  625. }}
  626. .app-info {{
  627. background: #f8f9fa;
  628. padding: 20px;
  629. border-radius: 8px;
  630. margin-bottom: 20px;
  631. }}
  632. .scope-list {{
  633. list-style: none;
  634. padding: 0;
  635. margin: 10px 0;
  636. }}
  637. .scope-list li {{
  638. padding: 5px 0;
  639. color: #666;
  640. }}
  641. .scope-list li:before {{
  642. content: "✓ ";
  643. color: #28a745;
  644. font-weight: bold;
  645. }}
  646. .auth-buttons {{
  647. display: flex;
  648. gap: 10px;
  649. margin-top: 20px;
  650. }}
  651. .btn {{
  652. flex: 1;
  653. padding: 12px 20px;
  654. border: none;
  655. border-radius: 6px;
  656. font-size: 16px;
  657. cursor: pointer;
  658. text-decoration: none;
  659. text-align: center;
  660. display: inline-block;
  661. }}
  662. .btn-primary {{
  663. background: #007bff;
  664. color: white;
  665. }}
  666. .btn-secondary {{
  667. background: #6c757d;
  668. color: white;
  669. }}
  670. .btn:hover {{
  671. opacity: 0.9;
  672. }}
  673. </style>
  674. </head>
  675. <body>
  676. <div class="auth-container">
  677. <div class="auth-header">
  678. <h1>授权确认</h1>
  679. <p>应用请求访问您的账户</p>
  680. </div>
  681. <div class="app-info">
  682. <h3>{app_name}</h3>
  683. <p>该应用请求以下权限:</p>
  684. <ul class="scope-list">
  685. """
  686. # 添加权限列表
  687. scope_descriptions = {
  688. "profile": "访问您的基本信息(用户名、头像等)",
  689. "email": "访问您的邮箱地址",
  690. "phone": "访问您的手机号码",
  691. "roles": "访问您的角色和权限信息"
  692. }
  693. for scope_item in requested_scopes:
  694. description = scope_descriptions.get(scope_item, f"访问 {scope_item} 信息")
  695. authorization_html += f"<li>{description}</li>"
  696. authorization_html += f"""
  697. </ul>
  698. </div>
  699. <div class="auth-buttons">
  700. <a href="/oauth/authorize/deny?client_id={client_id}&redirect_uri={redirect_uri}&state={state or ''}" class="btn btn-secondary">拒绝</a>
  701. <a href="/oauth/authorize/approve?client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state or ''}" class="btn btn-primary">授权</a>
  702. </div>
  703. </div>
  704. </body>
  705. </html>
  706. """
  707. from fastapi.responses import HTMLResponse
  708. return HTMLResponse(content=authorization_html)
  709. except Exception as e:
  710. print(f"❌ OAuth授权错误: {e}")
  711. error_url = f"{redirect_uri}?error=server_error&error_description=Internal server error"
  712. if state:
  713. error_url += f"&state={state}"
  714. return {"error": "server_error", "redirect_url": error_url}
  715. @app.get("/oauth/login")
  716. async def oauth_login_page(
  717. response_type: str,
  718. client_id: str,
  719. redirect_uri: str,
  720. scope: str = "profile",
  721. state: str = None
  722. ):
  723. """OAuth2登录页面"""
  724. try:
  725. print(f"🔐 显示OAuth登录页面: client_id={client_id}")
  726. # 获取应用信息
  727. conn = get_db_connection()
  728. if not conn:
  729. return {"error": "server_error", "message": "数据库连接失败"}
  730. cursor = conn.cursor()
  731. cursor.execute("SELECT name FROM apps WHERE app_key = %s", (client_id,))
  732. app_data = cursor.fetchone()
  733. cursor.close()
  734. conn.close()
  735. app_name = app_data[0] if app_data else "未知应用"
  736. # 构建登录页面HTML
  737. login_html = f"""
  738. <!DOCTYPE html>
  739. <html lang="zh-CN">
  740. <head>
  741. <meta charset="UTF-8">
  742. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  743. <title>SSO登录 - {app_name}</title>
  744. <style>
  745. body {{
  746. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  747. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  748. margin: 0;
  749. padding: 20px;
  750. min-height: 100vh;
  751. display: flex;
  752. align-items: center;
  753. justify-content: center;
  754. }}
  755. .login-container {{
  756. background: white;
  757. border-radius: 15px;
  758. padding: 40px;
  759. box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
  760. max-width: 400px;
  761. width: 100%;
  762. }}
  763. .login-header {{
  764. text-align: center;
  765. margin-bottom: 30px;
  766. }}
  767. .login-header h1 {{
  768. color: #333;
  769. margin-bottom: 10px;
  770. }}
  771. .app-info {{
  772. background: #f8f9fa;
  773. padding: 15px;
  774. border-radius: 8px;
  775. margin-bottom: 20px;
  776. text-align: center;
  777. }}
  778. .form-group {{
  779. margin-bottom: 20px;
  780. }}
  781. .form-group label {{
  782. display: block;
  783. margin-bottom: 5px;
  784. font-weight: 500;
  785. color: #333;
  786. }}
  787. .form-group input {{
  788. width: 100%;
  789. padding: 12px;
  790. border: 1px solid #ddd;
  791. border-radius: 6px;
  792. font-size: 16px;
  793. box-sizing: border-box;
  794. }}
  795. .form-group input:focus {{
  796. outline: none;
  797. border-color: #007bff;
  798. box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
  799. }}
  800. .btn {{
  801. width: 100%;
  802. padding: 12px;
  803. background: #007bff;
  804. color: white;
  805. border: none;
  806. border-radius: 6px;
  807. font-size: 16px;
  808. font-weight: 500;
  809. cursor: pointer;
  810. transition: background 0.3s;
  811. }}
  812. .btn:hover {{
  813. background: #0056b3;
  814. }}
  815. .btn:disabled {{
  816. background: #6c757d;
  817. cursor: not-allowed;
  818. }}
  819. .error-message {{
  820. color: #dc3545;
  821. font-size: 14px;
  822. margin-top: 10px;
  823. text-align: center;
  824. }}
  825. .success-message {{
  826. color: #28a745;
  827. font-size: 14px;
  828. margin-top: 10px;
  829. text-align: center;
  830. }}
  831. </style>
  832. </head>
  833. <body>
  834. <div class="login-container">
  835. <div class="login-header">
  836. <h1>🔐 SSO登录</h1>
  837. <p>请登录以继续访问应用</p>
  838. </div>
  839. <div class="app-info">
  840. <strong>{app_name}</strong> 请求访问您的账户
  841. </div>
  842. <form id="loginForm" onsubmit="handleLogin(event)">
  843. <div class="form-group">
  844. <label for="username">用户名或邮箱</label>
  845. <input type="text" id="username" name="username" required>
  846. </div>
  847. <div class="form-group">
  848. <label for="password">密码</label>
  849. <input type="password" id="password" name="password" required>
  850. </div>
  851. <button type="submit" class="btn" id="loginBtn">登录</button>
  852. <div id="message"></div>
  853. </form>
  854. <div style="margin-top: 20px; text-align: center; font-size: 14px; color: #666;">
  855. <p>测试账号: admin / Admin123456</p>
  856. </div>
  857. </div>
  858. <script>
  859. async function handleLogin(event) {{
  860. event.preventDefault();
  861. const loginBtn = document.getElementById('loginBtn');
  862. const messageDiv = document.getElementById('message');
  863. loginBtn.disabled = true;
  864. loginBtn.textContent = '登录中...';
  865. messageDiv.innerHTML = '';
  866. const formData = new FormData(event.target);
  867. const loginData = {{
  868. username: formData.get('username'),
  869. password: formData.get('password'),
  870. remember_me: false
  871. }};
  872. try {{
  873. // 调用登录API
  874. const response = await fetch('/api/v1/auth/login', {{
  875. method: 'POST',
  876. headers: {{
  877. 'Content-Type': 'application/json'
  878. }},
  879. body: JSON.stringify(loginData)
  880. }});
  881. const result = await response.json();
  882. if (result.code === 0) {{
  883. messageDiv.innerHTML = '<div class="success-message">登录成功,正在跳转...</div>';
  884. // 登录成功后,重定向到授权页面
  885. const authUrl = `/oauth/authorize/authenticated?response_type={response_type}&client_id={client_id}&redirect_uri={redirect_uri}&scope={scope}&state={state or ''}&access_token=${{result.data.access_token}}`;
  886. setTimeout(() => {{
  887. window.location.href = authUrl;
  888. }}, 1000);
  889. }} else {{
  890. messageDiv.innerHTML = `<div class="error-message">${{result.message}}</div>`;
  891. }}
  892. }} catch (error) {{
  893. messageDiv.innerHTML = '<div class="error-message">登录失败,请重试</div>';
  894. }} finally {{
  895. loginBtn.disabled = false;
  896. loginBtn.textContent = '登录';
  897. }}
  898. }}
  899. </script>
  900. </body>
  901. </html>
  902. """
  903. from fastapi.responses import HTMLResponse
  904. return HTMLResponse(content=login_html)
  905. except Exception as e:
  906. print(f"❌ OAuth登录页面错误: {e}")
  907. return {"error": "server_error", "message": "服务器内部错误"}
  908. @app.get("/oauth/authorize/authenticated")
  909. async def oauth_authorize_authenticated(
  910. response_type: str,
  911. client_id: str,
  912. redirect_uri: str,
  913. access_token: str,
  914. scope: str = "profile",
  915. state: str = None
  916. ):
  917. """用户已登录后的授权处理"""
  918. try:
  919. print(f"🔐 用户已登录,处理授权: client_id={client_id}")
  920. # 验证访问令牌
  921. payload = verify_token(access_token)
  922. if not payload:
  923. error_url = f"{redirect_uri}?error=invalid_token&error_description=Invalid access token"
  924. if state:
  925. error_url += f"&state={state}"
  926. from fastapi.responses import RedirectResponse
  927. return RedirectResponse(url=error_url, status_code=302)
  928. user_id = payload.get("sub")
  929. username = payload.get("username", "")
  930. print(f"✅ 用户已验证: {username} ({user_id})")
  931. # 获取应用信息
  932. conn = get_db_connection()
  933. if not conn:
  934. error_url = f"{redirect_uri}?error=server_error&error_description=Database connection failed"
  935. if state:
  936. error_url += f"&state={state}"
  937. from fastapi.responses import RedirectResponse
  938. return RedirectResponse(url=error_url, status_code=302)
  939. cursor = conn.cursor()
  940. cursor.execute("SELECT name, is_trusted FROM apps WHERE app_key = %s", (client_id,))
  941. app_data = cursor.fetchone()
  942. cursor.close()
  943. conn.close()
  944. if not app_data:
  945. error_url = f"{redirect_uri}?error=invalid_client&error_description=Invalid client"
  946. if state:
  947. error_url += f"&state={state}"
  948. from fastapi.responses import RedirectResponse
  949. return RedirectResponse(url=error_url, status_code=302)
  950. app_name, is_trusted = app_data
  951. # 如果是受信任应用,直接授权
  952. if is_trusted:
  953. # 生成授权码
  954. auth_code = secrets.token_urlsafe(32)
  955. # TODO: 将授权码存储到数据库,关联用户和应用
  956. # 这里简化处理,实际应该存储到数据库
  957. # 重定向回应用
  958. callback_url = f"{redirect_uri}?code={auth_code}"
  959. if state:
  960. callback_url += f"&state={state}"
  961. print(f"✅ 受信任应用自动授权: {callback_url}")
  962. from fastapi.responses import RedirectResponse
  963. return RedirectResponse(url=callback_url, status_code=302)
  964. # 非受信任应用,显示授权确认页面
  965. # 这里可以返回授权确认页面的HTML
  966. # 为简化,暂时也直接授权
  967. auth_code = secrets.token_urlsafe(32)
  968. callback_url = f"{redirect_uri}?code={auth_code}"
  969. if state:
  970. callback_url += f"&state={state}"
  971. print(f"✅ 用户授权完成: {callback_url}")
  972. from fastapi.responses import RedirectResponse
  973. return RedirectResponse(url=callback_url, status_code=302)
  974. except Exception as e:
  975. print(f"❌ 授权处理错误: {e}")
  976. error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
  977. if state:
  978. error_url += f"&state={state}"
  979. from fastapi.responses import RedirectResponse
  980. return RedirectResponse(url=error_url, status_code=302)
  981. async def oauth_approve(
  982. client_id: str,
  983. redirect_uri: str,
  984. scope: str = "profile",
  985. state: str = None
  986. ):
  987. """用户同意授权"""
  988. try:
  989. print(f"✅ 用户同意授权: client_id={client_id}")
  990. # 生成授权码
  991. auth_code = secrets.token_urlsafe(32)
  992. # TODO: 将授权码存储到数据库,关联用户和应用
  993. # 这里简化处理,实际应该:
  994. # 1. 验证用户登录状态
  995. # 2. 将授权码存储到数据库
  996. # 3. 设置过期时间(通常10分钟)
  997. # 构建回调URL
  998. callback_url = f"{redirect_uri}?code={auth_code}"
  999. if state:
  1000. callback_url += f"&state={state}"
  1001. print(f"🔄 重定向到: {callback_url}")
  1002. from fastapi.responses import RedirectResponse
  1003. return RedirectResponse(url=callback_url, status_code=302)
  1004. except Exception as e:
  1005. print(f"❌ 授权确认错误: {e}")
  1006. error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
  1007. if state:
  1008. error_url += f"&state={state}"
  1009. from fastapi.responses import RedirectResponse
  1010. return RedirectResponse(url=error_url, status_code=302)
  1011. @app.get("/oauth/authorize/deny")
  1012. async def oauth_deny(
  1013. client_id: str,
  1014. redirect_uri: str,
  1015. state: str = None
  1016. ):
  1017. """用户拒绝授权"""
  1018. try:
  1019. print(f"❌ 用户拒绝授权: client_id={client_id}")
  1020. # 构建错误回调URL
  1021. error_url = f"{redirect_uri}?error=access_denied&error_description=User denied authorization"
  1022. if state:
  1023. error_url += f"&state={state}"
  1024. from fastapi.responses import RedirectResponse
  1025. return RedirectResponse(url=error_url, status_code=302)
  1026. except Exception as e:
  1027. print(f"❌ 拒绝授权错误: {e}")
  1028. error_url = f"{redirect_uri}?error=server_error&error_description=Authorization failed"
  1029. if state:
  1030. error_url += f"&state={state}"
  1031. from fastapi.responses import RedirectResponse
  1032. return RedirectResponse(url=error_url, status_code=302)
  1033. @app.post("/oauth/token")
  1034. async def oauth_token(request: Request):
  1035. """OAuth2令牌端点"""
  1036. try:
  1037. # 获取请求数据
  1038. form_data = await request.form()
  1039. grant_type = form_data.get("grant_type")
  1040. code = form_data.get("code")
  1041. redirect_uri = form_data.get("redirect_uri")
  1042. client_id = form_data.get("client_id")
  1043. client_secret = form_data.get("client_secret")
  1044. print(f"🎫 令牌请求: grant_type={grant_type}, client_id={client_id}")
  1045. # 验证grant_type
  1046. if grant_type != "authorization_code":
  1047. return {
  1048. "error": "unsupported_grant_type",
  1049. "error_description": "Only authorization_code grant type is supported"
  1050. }
  1051. # 验证必要参数
  1052. if not code or not redirect_uri or not client_id:
  1053. return {
  1054. "error": "invalid_request",
  1055. "error_description": "Missing required parameters"
  1056. }
  1057. # 获取数据库连接
  1058. conn = get_db_connection()
  1059. if not conn:
  1060. return {
  1061. "error": "server_error",
  1062. "error_description": "Database connection failed"
  1063. }
  1064. cursor = conn.cursor()
  1065. # 验证客户端
  1066. cursor.execute("""
  1067. SELECT id, name, app_secret, redirect_uris, scope, is_active
  1068. FROM apps
  1069. WHERE app_key = %s AND is_active = 1
  1070. """, (client_id,))
  1071. app_data = cursor.fetchone()
  1072. if not app_data:
  1073. cursor.close()
  1074. conn.close()
  1075. return {
  1076. "error": "invalid_client",
  1077. "error_description": "Invalid client credentials"
  1078. }
  1079. app_id, app_name, stored_secret, redirect_uris_json, scope_json, is_active = app_data
  1080. # 验证客户端密钥(如果提供了)
  1081. if client_secret and client_secret != stored_secret:
  1082. cursor.close()
  1083. conn.close()
  1084. return {
  1085. "error": "invalid_client",
  1086. "error_description": "Invalid client credentials"
  1087. }
  1088. # 验证redirect_uri
  1089. redirect_uris = json.loads(redirect_uris_json) if redirect_uris_json else []
  1090. if redirect_uri not in redirect_uris:
  1091. cursor.close()
  1092. conn.close()
  1093. return {
  1094. "error": "invalid_grant",
  1095. "error_description": "Invalid redirect_uri"
  1096. }
  1097. # TODO: 验证授权码
  1098. # 这里简化处理,实际应该:
  1099. # 1. 从数据库查找授权码
  1100. # 2. 验证授权码是否有效且未过期
  1101. # 3. 验证授权码是否已被使用
  1102. # 4. 获取关联的用户ID
  1103. # 模拟用户ID(实际应该从授权码记录中获取)
  1104. user_id = "ed6a79d3-0083-4d81-8b48-fc522f686f74" # admin用户ID
  1105. # 生成访问令牌
  1106. token_data = {
  1107. "sub": user_id,
  1108. "client_id": client_id,
  1109. "scope": "profile email"
  1110. }
  1111. access_token = create_access_token(token_data)
  1112. refresh_token = secrets.token_urlsafe(32)
  1113. # TODO: 将令牌存储到数据库
  1114. cursor.close()
  1115. conn.close()
  1116. # 返回令牌响应
  1117. token_response = {
  1118. "access_token": access_token,
  1119. "token_type": "Bearer",
  1120. "expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60,
  1121. "refresh_token": refresh_token,
  1122. "scope": "profile email"
  1123. }
  1124. print(f"✅ 令牌生成成功: {access_token[:50]}...")
  1125. return token_response
  1126. except Exception as e:
  1127. print(f"❌ 令牌生成错误: {e}")
  1128. return {
  1129. "error": "server_error",
  1130. "error_description": "Internal server error"
  1131. }
  1132. @app.get("/oauth/userinfo")
  1133. async def oauth_userinfo(credentials: HTTPAuthorizationCredentials = Depends(security)):
  1134. """OAuth2用户信息端点"""
  1135. try:
  1136. # 验证令牌
  1137. payload = verify_token(credentials.credentials)
  1138. if not payload:
  1139. return {
  1140. "error": "invalid_token",
  1141. "error_description": "Invalid or expired access token"
  1142. }
  1143. user_id = payload.get("sub")
  1144. client_id = payload.get("client_id")
  1145. scope = payload.get("scope", "").split()
  1146. print(f"👤 用户信息请求: user_id={user_id}, client_id={client_id}, scope={scope}")
  1147. # 获取数据库连接
  1148. conn = get_db_connection()
  1149. if not conn:
  1150. return {
  1151. "error": "server_error",
  1152. "error_description": "Database connection failed"
  1153. }
  1154. cursor = conn.cursor()
  1155. # 查找用户信息
  1156. cursor.execute("""
  1157. SELECT u.id, u.username, u.email, u.phone, u.avatar_url, u.is_active,
  1158. p.real_name, p.company, p.department, p.position
  1159. FROM users u
  1160. LEFT JOIN user_profiles p ON u.id = p.user_id
  1161. WHERE u.id = %s AND u.is_active = 1
  1162. """, (user_id,))
  1163. user_data = cursor.fetchone()
  1164. cursor.close()
  1165. conn.close()
  1166. if not user_data:
  1167. return {
  1168. "error": "invalid_token",
  1169. "error_description": "User not found or inactive"
  1170. }
  1171. # 构建用户信息响应(根据scope过滤)
  1172. user_info = {"sub": user_data[0]}
  1173. if "profile" in scope:
  1174. user_info.update({
  1175. "username": user_data[1],
  1176. "avatar_url": user_data[4],
  1177. "real_name": user_data[6],
  1178. "company": user_data[7],
  1179. "department": user_data[8],
  1180. "position": user_data[9]
  1181. })
  1182. if "email" in scope:
  1183. user_info["email"] = user_data[2]
  1184. if "phone" in scope:
  1185. user_info["phone"] = user_data[3]
  1186. print(f"✅ 返回用户信息: {user_info}")
  1187. return user_info
  1188. except Exception as e:
  1189. print(f"❌ 获取用户信息错误: {e}")
  1190. return {
  1191. "error": "server_error",
  1192. "error_description": "Internal server error"
  1193. }
  1194. @app.get("/api/v1/apps")
  1195. async def get_apps(
  1196. page: int = 1,
  1197. page_size: int = 20,
  1198. keyword: str = "",
  1199. status: str = "",
  1200. credentials: HTTPAuthorizationCredentials = Depends(security)
  1201. ):
  1202. """获取应用列表"""
  1203. try:
  1204. # 验证令牌
  1205. payload = verify_token(credentials.credentials)
  1206. if not payload:
  1207. return ApiResponse(
  1208. code=200002,
  1209. message="无效的访问令牌",
  1210. timestamp=datetime.now(timezone.utc).isoformat()
  1211. ).model_dump()
  1212. user_id = payload.get("sub")
  1213. # 获取数据库连接
  1214. conn = get_db_connection()
  1215. if not conn:
  1216. return ApiResponse(
  1217. code=500001,
  1218. message="数据库连接失败",
  1219. timestamp=datetime.now(timezone.utc).isoformat()
  1220. ).model_dump()
  1221. cursor = conn.cursor()
  1222. # 构建查询条件
  1223. where_conditions = ["created_by = %s"]
  1224. params = [user_id]
  1225. if keyword:
  1226. where_conditions.append("(name LIKE %s OR description LIKE %s)")
  1227. params.extend([f"%{keyword}%", f"%{keyword}%"])
  1228. if status == "active":
  1229. where_conditions.append("is_active = 1")
  1230. elif status == "inactive":
  1231. where_conditions.append("is_active = 0")
  1232. where_clause = " AND ".join(where_conditions)
  1233. # 查询总数
  1234. cursor.execute(f"SELECT COUNT(*) FROM apps WHERE {where_clause}", params)
  1235. total = cursor.fetchone()[0]
  1236. # 查询应用列表
  1237. offset = (page - 1) * page_size
  1238. cursor.execute(f"""
  1239. SELECT id, name, app_key, description, icon_url, redirect_uris, scope,
  1240. is_active, is_trusted, access_token_expires, refresh_token_expires,
  1241. created_at, updated_at
  1242. FROM apps
  1243. WHERE {where_clause}
  1244. ORDER BY created_at DESC
  1245. LIMIT %s OFFSET %s
  1246. """, params + [page_size, offset])
  1247. apps = []
  1248. for row in cursor.fetchall():
  1249. app = {
  1250. "id": row[0],
  1251. "name": row[1],
  1252. "app_key": row[2],
  1253. "description": row[3],
  1254. "icon_url": row[4],
  1255. "redirect_uris": json.loads(row[5]) if row[5] else [],
  1256. "scope": json.loads(row[6]) if row[6] else [],
  1257. "is_active": bool(row[7]),
  1258. "is_trusted": bool(row[8]),
  1259. "access_token_expires": row[9],
  1260. "refresh_token_expires": row[10],
  1261. "created_at": row[11].isoformat() if row[11] else None,
  1262. "updated_at": row[12].isoformat() if row[12] else None,
  1263. # 模拟统计数据
  1264. "today_requests": secrets.randbelow(1000),
  1265. "active_users": secrets.randbelow(100)
  1266. }
  1267. apps.append(app)
  1268. cursor.close()
  1269. conn.close()
  1270. return ApiResponse(
  1271. code=0,
  1272. message="获取应用列表成功",
  1273. data={
  1274. "items": apps,
  1275. "total": total,
  1276. "page": page,
  1277. "page_size": page_size
  1278. },
  1279. timestamp=datetime.now(timezone.utc).isoformat()
  1280. ).model_dump()
  1281. except Exception as e:
  1282. print(f"获取应用列表错误: {e}")
  1283. return ApiResponse(
  1284. code=500001,
  1285. message="服务器内部错误",
  1286. timestamp=datetime.now(timezone.utc).isoformat()
  1287. ).model_dump()
  1288. @app.get("/api/v1/apps/{app_id}")
  1289. async def get_app_detail(
  1290. app_id: str,
  1291. credentials: HTTPAuthorizationCredentials = Depends(security)
  1292. ):
  1293. """获取应用详情(包含密钥)"""
  1294. try:
  1295. # 验证令牌
  1296. payload = verify_token(credentials.credentials)
  1297. if not payload:
  1298. return ApiResponse(
  1299. code=200002,
  1300. message="无效的访问令牌",
  1301. timestamp=datetime.now(timezone.utc).isoformat()
  1302. ).model_dump()
  1303. user_id = payload.get("sub")
  1304. # 获取数据库连接
  1305. conn = get_db_connection()
  1306. if not conn:
  1307. return ApiResponse(
  1308. code=500001,
  1309. message="数据库连接失败",
  1310. timestamp=datetime.now(timezone.utc).isoformat()
  1311. ).model_dump()
  1312. cursor = conn.cursor()
  1313. # 查询应用详情(包含密钥)
  1314. cursor.execute("""
  1315. SELECT id, name, app_key, app_secret, description, icon_url,
  1316. redirect_uris, scope, is_active, is_trusted,
  1317. access_token_expires, refresh_token_expires,
  1318. created_at, updated_at
  1319. FROM apps
  1320. WHERE id = %s AND created_by = %s
  1321. """, (app_id, user_id))
  1322. app_data = cursor.fetchone()
  1323. cursor.close()
  1324. conn.close()
  1325. if not app_data:
  1326. return ApiResponse(
  1327. code=200001,
  1328. message="应用不存在或无权限",
  1329. timestamp=datetime.now(timezone.utc).isoformat()
  1330. ).model_dump()
  1331. app_detail = {
  1332. "id": app_data[0],
  1333. "name": app_data[1],
  1334. "app_key": app_data[2],
  1335. "app_secret": app_data[3],
  1336. "description": app_data[4],
  1337. "icon_url": app_data[5],
  1338. "redirect_uris": json.loads(app_data[6]) if app_data[6] else [],
  1339. "scope": json.loads(app_data[7]) if app_data[7] else [],
  1340. "is_active": bool(app_data[8]),
  1341. "is_trusted": bool(app_data[9]),
  1342. "access_token_expires": app_data[10],
  1343. "refresh_token_expires": app_data[11],
  1344. "created_at": app_data[12].isoformat() if app_data[12] else None,
  1345. "updated_at": app_data[13].isoformat() if app_data[13] else None
  1346. }
  1347. return ApiResponse(
  1348. code=0,
  1349. message="获取应用详情成功",
  1350. data=app_detail,
  1351. timestamp=datetime.now(timezone.utc).isoformat()
  1352. ).model_dump()
  1353. except Exception as e:
  1354. print(f"获取应用详情错误: {e}")
  1355. return ApiResponse(
  1356. code=500001,
  1357. message="服务器内部错误",
  1358. timestamp=datetime.now(timezone.utc).isoformat()
  1359. ).model_dump()
  1360. @app.post("/api/v1/apps")
  1361. async def create_app(
  1362. request: Request,
  1363. app_data: dict,
  1364. credentials: HTTPAuthorizationCredentials = Depends(security)
  1365. ):
  1366. """创建应用"""
  1367. try:
  1368. # 验证令牌
  1369. payload = verify_token(credentials.credentials)
  1370. if not payload:
  1371. return ApiResponse(
  1372. code=200002,
  1373. message="无效的访问令牌",
  1374. timestamp=datetime.now(timezone.utc).isoformat()
  1375. ).model_dump()
  1376. user_id = payload.get("sub")
  1377. # 验证必要字段
  1378. if not app_data.get('name') or not app_data.get('redirect_uris'):
  1379. return ApiResponse(
  1380. code=100001,
  1381. message="缺少必要参数",
  1382. timestamp=datetime.now(timezone.utc).isoformat()
  1383. ).model_dump()
  1384. # 获取数据库连接
  1385. conn = get_db_connection()
  1386. if not conn:
  1387. return ApiResponse(
  1388. code=500001,
  1389. message="数据库连接失败",
  1390. timestamp=datetime.now(timezone.utc).isoformat()
  1391. ).model_dump()
  1392. cursor = conn.cursor()
  1393. # 生成应用ID和密钥
  1394. app_id = str(uuid.uuid4())
  1395. app_key = generate_random_string(32)
  1396. app_secret = generate_random_string(64)
  1397. # 插入应用记录
  1398. cursor.execute("""
  1399. INSERT INTO apps (
  1400. id, name, app_key, app_secret, description, icon_url,
  1401. redirect_uris, scope, is_active, is_trusted,
  1402. access_token_expires, refresh_token_expires, created_by,
  1403. created_at, updated_at
  1404. ) VALUES (
  1405. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW()
  1406. )
  1407. """, (
  1408. app_id,
  1409. app_data['name'],
  1410. app_key,
  1411. app_secret,
  1412. app_data.get('description', ''),
  1413. app_data.get('icon_url', ''),
  1414. json.dumps(app_data['redirect_uris']),
  1415. json.dumps(app_data.get('scope', ['profile'])),
  1416. True,
  1417. app_data.get('is_trusted', False),
  1418. app_data.get('access_token_expires', 7200),
  1419. app_data.get('refresh_token_expires', 2592000),
  1420. user_id
  1421. ))
  1422. conn.commit()
  1423. cursor.close()
  1424. conn.close()
  1425. # 返回创建的应用信息
  1426. app_info = {
  1427. "id": app_id,
  1428. "name": app_data['name'],
  1429. "app_key": app_key,
  1430. "app_secret": app_secret,
  1431. "description": app_data.get('description', ''),
  1432. "redirect_uris": app_data['redirect_uris'],
  1433. "scope": app_data.get('scope', ['profile']),
  1434. "is_active": True,
  1435. "is_trusted": app_data.get('is_trusted', False)
  1436. }
  1437. return ApiResponse(
  1438. code=0,
  1439. message="应用创建成功",
  1440. data=app_info,
  1441. timestamp=datetime.now(timezone.utc).isoformat()
  1442. ).model_dump()
  1443. except Exception as e:
  1444. print(f"创建应用错误: {e}")
  1445. return ApiResponse(
  1446. code=500001,
  1447. message="服务器内部错误",
  1448. timestamp=datetime.now(timezone.utc).isoformat()
  1449. ).model_dump()
  1450. @app.put("/api/v1/apps/{app_id}/status")
  1451. async def toggle_app_status(
  1452. app_id: str,
  1453. status_data: dict,
  1454. credentials: HTTPAuthorizationCredentials = Depends(security)
  1455. ):
  1456. """切换应用状态"""
  1457. try:
  1458. # 验证令牌
  1459. payload = verify_token(credentials.credentials)
  1460. if not payload:
  1461. return ApiResponse(
  1462. code=200002,
  1463. message="无效的访问令牌",
  1464. timestamp=datetime.now(timezone.utc).isoformat()
  1465. ).model_dump()
  1466. user_id = payload.get("sub")
  1467. is_active = status_data.get('is_active')
  1468. if is_active is None:
  1469. return ApiResponse(
  1470. code=100001,
  1471. message="缺少必要参数",
  1472. timestamp=datetime.now(timezone.utc).isoformat()
  1473. ).model_dump()
  1474. # 获取数据库连接
  1475. conn = get_db_connection()
  1476. if not conn:
  1477. return ApiResponse(
  1478. code=500001,
  1479. message="数据库连接失败",
  1480. timestamp=datetime.now(timezone.utc).isoformat()
  1481. ).model_dump()
  1482. cursor = conn.cursor()
  1483. # 检查应用是否存在且属于当前用户
  1484. cursor.execute("""
  1485. SELECT id, name FROM apps
  1486. WHERE id = %s AND created_by = %s
  1487. """, (app_id, user_id))
  1488. app_data = cursor.fetchone()
  1489. if not app_data:
  1490. cursor.close()
  1491. conn.close()
  1492. return ApiResponse(
  1493. code=200001,
  1494. message="应用不存在或无权限",
  1495. timestamp=datetime.now(timezone.utc).isoformat()
  1496. ).model_dump()
  1497. # 更新应用状态
  1498. cursor.execute("""
  1499. UPDATE apps
  1500. SET is_active = %s, updated_at = NOW()
  1501. WHERE id = %s
  1502. """, (is_active, app_id))
  1503. conn.commit()
  1504. cursor.close()
  1505. conn.close()
  1506. action = "启用" if is_active else "禁用"
  1507. print(f"✅ 应用状态已更新: {app_data[1]} -> {action}")
  1508. return ApiResponse(
  1509. code=0,
  1510. message=f"应用已{action}",
  1511. timestamp=datetime.now(timezone.utc).isoformat()
  1512. ).model_dump()
  1513. except Exception as e:
  1514. print(f"切换应用状态错误: {e}")
  1515. return ApiResponse(
  1516. code=500001,
  1517. message="服务器内部错误",
  1518. timestamp=datetime.now(timezone.utc).isoformat()
  1519. ).model_dump()
  1520. @app.put("/api/v1/apps/{app_id}")
  1521. async def update_app(
  1522. app_id: str,
  1523. app_data: dict,
  1524. credentials: HTTPAuthorizationCredentials = Depends(security)
  1525. ):
  1526. """更新应用信息"""
  1527. try:
  1528. # 验证令牌
  1529. payload = verify_token(credentials.credentials)
  1530. if not payload:
  1531. return ApiResponse(
  1532. code=200002,
  1533. message="无效的访问令牌",
  1534. timestamp=datetime.now(timezone.utc).isoformat()
  1535. ).model_dump()
  1536. user_id = payload.get("sub")
  1537. # 验证必要参数
  1538. name = app_data.get('name', '').strip()
  1539. if not name:
  1540. return ApiResponse(
  1541. code=100001,
  1542. message="应用名称不能为空",
  1543. timestamp=datetime.now(timezone.utc).isoformat()
  1544. ).model_dump()
  1545. # 获取数据库连接
  1546. conn = get_db_connection()
  1547. if not conn:
  1548. return ApiResponse(
  1549. code=500001,
  1550. message="数据库连接失败",
  1551. timestamp=datetime.now(timezone.utc).isoformat()
  1552. ).model_dump()
  1553. cursor = conn.cursor()
  1554. # 检查应用是否存在且属于当前用户
  1555. cursor.execute("""
  1556. SELECT id, name FROM apps
  1557. WHERE id = %s AND created_by = %s
  1558. """, (app_id, user_id))
  1559. existing_app = cursor.fetchone()
  1560. if not existing_app:
  1561. cursor.close()
  1562. conn.close()
  1563. return ApiResponse(
  1564. code=200001,
  1565. message="应用不存在或无权限",
  1566. timestamp=datetime.now(timezone.utc).isoformat()
  1567. ).model_dump()
  1568. # 检查应用名称是否已被其他应用使用
  1569. cursor.execute("""
  1570. SELECT id FROM apps
  1571. WHERE name = %s AND created_by = %s AND id != %s
  1572. """, (name, user_id, app_id))
  1573. if cursor.fetchone():
  1574. cursor.close()
  1575. conn.close()
  1576. return ApiResponse(
  1577. code=200001,
  1578. message="应用名称已存在",
  1579. timestamp=datetime.now(timezone.utc).isoformat()
  1580. ).model_dump()
  1581. # 准备更新数据
  1582. description = (app_data.get('description') or '').strip()
  1583. icon_url = (app_data.get('icon_url') or '').strip()
  1584. redirect_uris = app_data.get('redirect_uris', [])
  1585. scope = app_data.get('scope', ['profile', 'email'])
  1586. is_trusted = app_data.get('is_trusted', False)
  1587. access_token_expires = app_data.get('access_token_expires', 7200)
  1588. refresh_token_expires = app_data.get('refresh_token_expires', 2592000)
  1589. # 验证回调URL
  1590. if not redirect_uris or not isinstance(redirect_uris, list):
  1591. cursor.close()
  1592. conn.close()
  1593. return ApiResponse(
  1594. code=100001,
  1595. message="至少需要一个回调URL",
  1596. timestamp=datetime.now(timezone.utc).isoformat()
  1597. ).model_dump()
  1598. # 验证权限范围
  1599. if not scope or not isinstance(scope, list):
  1600. scope = ['profile', 'email']
  1601. # 更新应用信息
  1602. cursor.execute("""
  1603. UPDATE apps
  1604. SET name = %s, description = %s, icon_url = %s,
  1605. redirect_uris = %s, scope = %s, is_trusted = %s,
  1606. access_token_expires = %s, refresh_token_expires = %s,
  1607. updated_at = NOW()
  1608. WHERE id = %s
  1609. """, (
  1610. name, description, icon_url,
  1611. json.dumps(redirect_uris), json.dumps(scope), is_trusted,
  1612. access_token_expires, refresh_token_expires, app_id
  1613. ))
  1614. conn.commit()
  1615. # 获取更新后的应用信息
  1616. cursor.execute("""
  1617. SELECT id, name, app_key, description, icon_url,
  1618. redirect_uris, scope, is_active, is_trusted,
  1619. access_token_expires, refresh_token_expires,
  1620. created_at, updated_at
  1621. FROM apps
  1622. WHERE id = %s
  1623. """, (app_id,))
  1624. app_info = cursor.fetchone()
  1625. cursor.close()
  1626. conn.close()
  1627. if app_info:
  1628. app_result = {
  1629. "id": app_info[0],
  1630. "name": app_info[1],
  1631. "app_key": app_info[2],
  1632. "description": app_info[3],
  1633. "icon_url": app_info[4],
  1634. "redirect_uris": json.loads(app_info[5]) if app_info[5] else [],
  1635. "scope": json.loads(app_info[6]) if app_info[6] else [],
  1636. "is_active": bool(app_info[7]),
  1637. "is_trusted": bool(app_info[8]),
  1638. "access_token_expires": app_info[9],
  1639. "refresh_token_expires": app_info[10],
  1640. "created_at": app_info[11].isoformat() if app_info[11] else None,
  1641. "updated_at": app_info[12].isoformat() if app_info[12] else None
  1642. }
  1643. print(f"✅ 应用已更新: {name}")
  1644. return ApiResponse(
  1645. code=0,
  1646. message="应用更新成功",
  1647. data=app_result,
  1648. timestamp=datetime.now(timezone.utc).isoformat()
  1649. ).model_dump()
  1650. else:
  1651. return ApiResponse(
  1652. code=500001,
  1653. message="获取更新后的应用信息失败",
  1654. timestamp=datetime.now(timezone.utc).isoformat()
  1655. ).model_dump()
  1656. except Exception as e:
  1657. print(f"更新应用错误: {e}")
  1658. return ApiResponse(
  1659. code=500001,
  1660. message="服务器内部错误",
  1661. timestamp=datetime.now(timezone.utc).isoformat()
  1662. ).model_dump()
  1663. @app.delete("/api/v1/apps/{app_id}")
  1664. async def delete_app(
  1665. app_id: str,
  1666. credentials: HTTPAuthorizationCredentials = Depends(security)
  1667. ):
  1668. """删除应用"""
  1669. try:
  1670. # 验证令牌
  1671. payload = verify_token(credentials.credentials)
  1672. if not payload:
  1673. return ApiResponse(
  1674. code=200002,
  1675. message="无效的访问令牌",
  1676. timestamp=datetime.now(timezone.utc).isoformat()
  1677. ).model_dump()
  1678. user_id = payload.get("sub")
  1679. # 获取数据库连接
  1680. conn = get_db_connection()
  1681. if not conn:
  1682. return ApiResponse(
  1683. code=500001,
  1684. message="数据库连接失败",
  1685. timestamp=datetime.now(timezone.utc).isoformat()
  1686. ).model_dump()
  1687. cursor = conn.cursor()
  1688. # 检查应用是否存在且属于当前用户
  1689. cursor.execute("""
  1690. SELECT id, name FROM apps
  1691. WHERE id = %s AND created_by = %s
  1692. """, (app_id, user_id))
  1693. app_data = cursor.fetchone()
  1694. if not app_data:
  1695. cursor.close()
  1696. conn.close()
  1697. return ApiResponse(
  1698. code=200001,
  1699. message="应用不存在或无权限",
  1700. timestamp=datetime.now(timezone.utc).isoformat()
  1701. ).model_dump()
  1702. # 删除应用(级联删除相关数据)
  1703. cursor.execute("DELETE FROM apps WHERE id = %s", (app_id,))
  1704. conn.commit()
  1705. cursor.close()
  1706. conn.close()
  1707. print(f"✅ 应用已删除: {app_data[1]}")
  1708. return ApiResponse(
  1709. code=0,
  1710. message="应用已删除",
  1711. timestamp=datetime.now(timezone.utc).isoformat()
  1712. ).model_dump()
  1713. except Exception as e:
  1714. print(f"删除应用错误: {e}")
  1715. return ApiResponse(
  1716. code=500001,
  1717. message="服务器内部错误",
  1718. timestamp=datetime.now(timezone.utc).isoformat()
  1719. ).model_dump()
  1720. @app.post("/api/v1/apps/{app_id}/reset-secret")
  1721. async def reset_app_secret(
  1722. app_id: str,
  1723. credentials: HTTPAuthorizationCredentials = Depends(security)
  1724. ):
  1725. """重置应用密钥"""
  1726. try:
  1727. # 验证令牌
  1728. payload = verify_token(credentials.credentials)
  1729. if not payload:
  1730. return ApiResponse(
  1731. code=200002,
  1732. message="无效的访问令牌",
  1733. timestamp=datetime.now(timezone.utc).isoformat()
  1734. ).model_dump()
  1735. user_id = payload.get("sub")
  1736. # 获取数据库连接
  1737. conn = get_db_connection()
  1738. if not conn:
  1739. return ApiResponse(
  1740. code=500001,
  1741. message="数据库连接失败",
  1742. timestamp=datetime.now(timezone.utc).isoformat()
  1743. ).model_dump()
  1744. cursor = conn.cursor()
  1745. # 检查应用是否存在且属于当前用户
  1746. cursor.execute("""
  1747. SELECT id, name FROM apps
  1748. WHERE id = %s AND created_by = %s
  1749. """, (app_id, user_id))
  1750. app_data = cursor.fetchone()
  1751. if not app_data:
  1752. cursor.close()
  1753. conn.close()
  1754. return ApiResponse(
  1755. code=200001,
  1756. message="应用不存在或无权限",
  1757. timestamp=datetime.now(timezone.utc).isoformat()
  1758. ).model_dump()
  1759. # 生成新的应用密钥
  1760. new_secret = generate_random_string(64)
  1761. # 更新应用密钥
  1762. cursor.execute("""
  1763. UPDATE apps
  1764. SET app_secret = %s, updated_at = NOW()
  1765. WHERE id = %s
  1766. """, (new_secret, app_id))
  1767. conn.commit()
  1768. cursor.close()
  1769. conn.close()
  1770. print(f"✅ 应用密钥已重置: {app_data[1]}")
  1771. return ApiResponse(
  1772. code=0,
  1773. message="应用密钥已重置",
  1774. data={"app_secret": new_secret},
  1775. timestamp=datetime.now(timezone.utc).isoformat()
  1776. ).model_dump()
  1777. except Exception as e:
  1778. print(f"重置应用密钥错误: {e}")
  1779. return ApiResponse(
  1780. code=500001,
  1781. message="服务器内部错误",
  1782. timestamp=datetime.now(timezone.utc).isoformat()
  1783. ).model_dump()
  1784. def generate_random_string(length=32):
  1785. """生成随机字符串"""
  1786. import secrets
  1787. import string
  1788. alphabet = string.ascii_letters + string.digits
  1789. return ''.join(secrets.choice(alphabet) for _ in range(length))
  1790. """获取验证码"""
  1791. try:
  1792. # 生成验证码
  1793. captcha_text, captcha_image = generate_captcha()
  1794. # 这里应该将验证码文本存储到缓存中(Redis或内存)
  1795. # 为了简化,我们暂时返回固定的验证码
  1796. captcha_id = secrets.token_hex(16)
  1797. return ApiResponse(
  1798. code=0,
  1799. message="获取验证码成功",
  1800. data={
  1801. "captcha_id": captcha_id,
  1802. "captcha_image": captcha_image,
  1803. "captcha_text": captcha_text # 生产环境中不应该返回这个
  1804. },
  1805. timestamp=datetime.now(timezone.utc).isoformat()
  1806. ).model_dump()
  1807. except Exception as e:
  1808. print(f"生成验证码错误: {e}")
  1809. return ApiResponse(
  1810. code=500001,
  1811. message="生成验证码失败",
  1812. timestamp=datetime.now(timezone.utc).isoformat()
  1813. ).model_dump()
  1814. def generate_captcha():
  1815. """生成验证码"""
  1816. try:
  1817. from PIL import Image, ImageDraw, ImageFont
  1818. import io
  1819. import base64
  1820. import random
  1821. import string
  1822. # 生成随机验证码文本
  1823. captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
  1824. # 创建图片
  1825. width, height = 120, 40
  1826. image = Image.new('RGB', (width, height), color='white')
  1827. draw = ImageDraw.Draw(image)
  1828. # 尝试使用系统字体,如果失败则使用默认字体
  1829. try:
  1830. # Windows系统字体
  1831. font = ImageFont.truetype("arial.ttf", 20)
  1832. except:
  1833. try:
  1834. # 备用字体
  1835. font = ImageFont.truetype("C:/Windows/Fonts/arial.ttf", 20)
  1836. except:
  1837. # 使用默认字体
  1838. font = ImageFont.load_default()
  1839. # 绘制验证码文本
  1840. text_width = draw.textlength(captcha_text, font=font)
  1841. text_height = 20
  1842. x = (width - text_width) // 2
  1843. y = (height - text_height) // 2
  1844. # 添加一些随机颜色
  1845. colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD']
  1846. text_color = random.choice(colors)
  1847. draw.text((x, y), captcha_text, fill=text_color, font=font)
  1848. # 添加一些干扰线
  1849. for _ in range(3):
  1850. x1 = random.randint(0, width)
  1851. y1 = random.randint(0, height)
  1852. x2 = random.randint(0, width)
  1853. y2 = random.randint(0, height)
  1854. draw.line([(x1, y1), (x2, y2)], fill=random.choice(colors), width=1)
  1855. # 添加一些干扰点
  1856. for _ in range(20):
  1857. x = random.randint(0, width)
  1858. y = random.randint(0, height)
  1859. draw.point((x, y), fill=random.choice(colors))
  1860. # 转换为base64
  1861. buffer = io.BytesIO()
  1862. image.save(buffer, format='PNG')
  1863. image_data = buffer.getvalue()
  1864. image_base64 = base64.b64encode(image_data).decode('utf-8')
  1865. return captcha_text, f"data:image/png;base64,{image_base64}"
  1866. except ImportError:
  1867. # 如果PIL不可用,返回简单的文本验证码
  1868. captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
  1869. # 创建一个简单的SVG验证码
  1870. svg_captcha = f"""
  1871. <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
  1872. <rect width="120" height="40" fill="#f0f0f0" stroke="#ccc"/>
  1873. <text x="60" y="25" font-family="Arial" font-size="18" text-anchor="middle" fill="#333">{captcha_text}</text>
  1874. </svg>
  1875. """
  1876. svg_base64 = base64.b64encode(svg_captcha.encode('utf-8')).decode('utf-8')
  1877. return captcha_text, f"data:image/svg+xml;base64,{svg_base64}"
  1878. except Exception as e:
  1879. print(f"生成验证码图片失败: {e}")
  1880. # 返回默认验证码
  1881. return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="
  1882. if __name__ == "__main__":
  1883. import uvicorn
  1884. # 查找可用端口
  1885. port = find_available_port()
  1886. if port is None:
  1887. print("❌ 无法找到可用端口 (8000-8010)")
  1888. print("请手动停止占用这些端口的进程")
  1889. sys.exit(1)
  1890. print("=" * 60)
  1891. print("🚀 SSO认证中心完整服务器")
  1892. print("=" * 60)
  1893. print(f"✅ 找到可用端口: {port}")
  1894. print(f"🌐 访问地址: http://localhost:{port}")
  1895. print(f"📚 API文档: http://localhost:{port}/docs")
  1896. print(f"❤️ 健康检查: http://localhost:{port}/health")
  1897. print(f"🔐 登录API: http://localhost:{port}/api/v1/auth/login")
  1898. print("=" * 60)
  1899. print("📝 前端配置:")
  1900. print(f" VITE_API_BASE_URL=http://localhost:{port}")
  1901. print("=" * 60)
  1902. print("👤 测试账号:")
  1903. print(" 用户名: admin")
  1904. print(" 密码: Admin123456")
  1905. print("=" * 60)
  1906. print("按 Ctrl+C 停止服务器")
  1907. print()
  1908. try:
  1909. uvicorn.run(
  1910. app,
  1911. host="0.0.0.0",
  1912. port=port,
  1913. log_level="info"
  1914. )
  1915. except KeyboardInterrupt:
  1916. print("\n👋 服务器已停止")
  1917. except Exception as e:
  1918. print(f"❌ 启动失败: {e}")
  1919. sys.exit(1)
  1920. @app.get("/api/v1/auth/captcha")
  1921. async def get_captcha():
  1922. """获取验证码"""
  1923. try:
  1924. # 生成验证码
  1925. captcha_text, captcha_image = generate_captcha()
  1926. # 这里应该将验证码文本存储到缓存中(Redis或内存)
  1927. # 为了简化,我们暂时返回固定的验证码
  1928. captcha_id = secrets.token_hex(16)
  1929. return ApiResponse(
  1930. code=0,
  1931. message="获取验证码成功",
  1932. data={
  1933. "captcha_id": captcha_id,
  1934. "captcha_image": captcha_image,
  1935. "captcha_text": captcha_text # 生产环境中不应该返回这个
  1936. },
  1937. timestamp=datetime.now(timezone.utc).isoformat()
  1938. ).model_dump()
  1939. except Exception as e:
  1940. print(f"生成验证码错误: {e}")
  1941. return ApiResponse(
  1942. code=500001,
  1943. message="生成验证码失败",
  1944. timestamp=datetime.now(timezone.utc).isoformat()
  1945. ).model_dump()
  1946. def generate_captcha():
  1947. """生成验证码"""
  1948. try:
  1949. import random
  1950. import string
  1951. import base64
  1952. # 生成随机验证码文本
  1953. captcha_text = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4))
  1954. # 创建一个简单的SVG验证码
  1955. svg_captcha = f"""
  1956. <svg width="120" height="40" xmlns="http://www.w3.org/2000/svg">
  1957. <rect width="120" height="40" fill="#f0f0f0" stroke="#ccc"/>
  1958. <text x="60" y="25" font-family="Arial" font-size="18" text-anchor="middle" fill="#333">{captcha_text}</text>
  1959. </svg>
  1960. """
  1961. svg_base64 = base64.b64encode(svg_captcha.encode('utf-8')).decode('utf-8')
  1962. return captcha_text, f"data:image/svg+xml;base64,{svg_base64}"
  1963. except Exception as e:
  1964. print(f"生成验证码失败: {e}")
  1965. # 返回默认验证码
  1966. return "1234", "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIwIiBoZWlnaHQ9IjQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMjAiIGhlaWdodD0iNDAiIGZpbGw9IiNmMGYwZjAiIHN0cm9rZT0iI2NjYyIvPjx0ZXh0IHg9IjYwIiB5PSIyNSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMzMzIj4xMjM0PC90ZXh0Pjwvc3ZnPg=="