"""SSO 单点登录中心 —— FastAPI""" import os from datetime import datetime, timedelta, timezone from typing import Optional from fastapi import FastAPI, Form, HTTPException, Request, Response from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.middleware.cors import CORSMiddleware from jose import jwt, JWTError from passlib.context import CryptContext import database from database import SessionLocal import models from sso_config import ( SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES, DEFAULT_USERS, COOKIE_DOMAIN, COOKIE_SAMESITE, ) app = FastAPI(title="SSO Center") # CORS:允许 blog 前端 8888 调用 app.add_middleware( CORSMiddleware, allow_origins=[ "http://129.211.173.87:8888", "http://129.211.173.87:8889", "http://129.211.173.87:8892", "http://129.211.173.87:8895", # happy-games "http://129.211.173.87:9000", "http://129.211.173.87", ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # ── 初始化数据库 ────────────────────────── models.Base.metadata.create_all(bind=database.engine) db = database.SessionLocal() try: if db.query(models.User).count() == 0: for uname, udata in DEFAULT_USERS.items(): hashed = pwd_context.hash(udata["password"]) db.add(models.User(username=uname, password=hashed)) db.commit() finally: db.close() # ── 加载静态登录页 ──────────────────────── _static_dir = os.path.join(os.path.dirname(__file__), "static") _login_html_path = os.path.join(_static_dir, "login.html") with open(_login_html_path, "r", encoding="utf-8") as f: LOGIN_HTML = f.read() def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) to_encode.update({"exp": expire}) return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) def verify_user(username: str, password: str) -> bool: db = database.SessionLocal() try: user = db.query(models.User).filter(models.User.username == username).first() if not user: return False return pwd_context.verify(password, user.password) finally: db.close() # ── 登录页 ────────────────────────────── @app.get("/", response_class=HTMLResponse) def login_page(request: Request): redirect = request.query_params.get("redirect", "") # 如果已登录(有 Cookie),直接跳转回原系统;无 redirect 则展示用户中心 token = request.cookies.get("access_token") if token: try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username = payload.get("sub") if not username: raise JWTError if redirect: return RedirectResponse(redirect) html = LOGGED_IN_HTML.replace("{{username}}", username).replace("{{redirect}}", "") return HTMLResponse(html) except JWTError: pass # token 无效,继续展示登录页 return HTMLResponse(LOGIN_HTML.replace("{{redirect}}", redirect)) # ── 已登录页面 ────────────────────────── LOGGED_IN_HTML = ''' SSO 用户中心

🔐 SSO 用户中心

欢迎,{{username}}

修改密码

''' # ── API ───────────────────────────────── @app.post("/api/login") def api_login(response: Response, username: str = Form(...), password: str = Form(...), redirect: str = Form("")): if not verify_user(username, password): raise HTTPException(status_code=401, detail="用户名或密码错误") access_token = create_access_token(data={"sub": username}) # 种 Cookie(Domain=IP,SameSite=Lax,同 IP 不同端口共享) cookie_opts = { "key": "access_token", "value": access_token, "domain": COOKIE_DOMAIN, "path": "/", "httponly": True, "samesite": COOKIE_SAMESITE, "max_age": ACCESS_TOKEN_EXPIRE_MINUTES * 60, } response.set_cookie(**cookie_opts) return {"success": True, "username": username, "redirect": redirect} @app.post("/api/logout") def api_logout(response: Response): response.delete_cookie("access_token", domain=COOKIE_DOMAIN, path="/") return {"success": True} @app.post("/api/change-password") def api_change_password( request: Request, response: Response, username: str = Form(""), old_password: str = Form(...), new_password: str = Form(...) ): """修改密码 —— 支持两种身份验证方式: 1) Cookie 中的 token(已登录状态) 2) 提交 username + old_password(未登录状态,登录页直接修改) 成功后清除 Cookie 要求重新登录 """ # 1) 确定用户名 target_username = None # 先尝试 Cookie token = request.cookies.get("access_token") if token: try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) target_username = payload.get("sub") except JWTError: pass # 无 Cookie 则使用提交的 username + old_password 验证 if not target_username: if not username: raise HTTPException(status_code=401, detail="未登录,请提供用户名") db = SessionLocal() try: user = db.query(models.User).filter(models.User.username == username).first() if not user: raise HTTPException(status_code=401, detail="用户名或密码错误") if not pwd_context.verify(old_password, user.password): raise HTTPException(status_code=401, detail="用户名或密码错误") target_username = username finally: db.close() # 2) 验证旧密码并更新 db = SessionLocal() try: user = db.query(models.User).filter(models.User.username == target_username).first() if not user: raise HTTPException(status_code=404, detail="用户不存在") if not pwd_context.verify(old_password, user.password): raise HTTPException(status_code=400, detail="旧密码错误") user.password = pwd_context.hash(new_password) db.commit() finally: db.close() # 3) 清除 Cookie,强制重新登录 response.delete_cookie("access_token", domain=COOKIE_DOMAIN, path="/") return {"success": True, "message": "密码修改成功,请重新登录"} @app.get("/api/userinfo") def api_userinfo(request: Request): token = request.cookies.get("access_token") if not token: raise HTTPException(status_code=401, detail="未登录") try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username = payload.get("sub") if not username: raise HTTPException(status_code=401, detail="无效令牌") return {"username": username} except JWTError: raise HTTPException(status_code=401, detail="令牌已过期或无效") @app.get("/api/validate") def api_validate(request: Request): """供业务系统可选调用:验证 Cookie 中的 token""" try: return api_userinfo(request) except HTTPException: raise if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=9000)