Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds an SMS-based authentication flow against SDU CAS, exposing it as a small FastAPI service and providing a local interactive script to manually complete the captcha + SMS login flow.
Changes:
- Added
sms_auth_api.pyimplementing/code,/sms, and/loginendpoints for captcha retrieval, SMS code sending, and login + user info fetch. - Added
cas_sms_auth.pyas a Tkinter-assisted CLI script to complete CAS SMS login and print resulting cookies.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 14 comments.
| File | Description |
|---|---|
| sms_auth_api.py | New FastAPI-based SMS auth API with in-memory session/cookie handling and upstream calls to pass.sdu.edu.cn + service.sdu.edu.cn. |
| cas_sms_auth.py | New interactive Tkinter script to obtain image captcha, request SMS code, and perform CAS login. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import httpx | ||
| from fastapi import Body, FastAPI, HTTPException, Request, Response | ||
| from httpx import Client, Cookies, HTTPError | ||
|
|
||
| from uniform_login_des import strEnc | ||
|
|
||
| app = FastAPI(debug=True, title="统一认证短信登录") | ||
|
|
||
|
|
There was a problem hiding this comment.
Running FastAPI(debug=True) should not be the default for a deployed auth API, since debug mode can leak internal details. Consider making this configurable via environment (or default to debug=False).
| import httpx | |
| from fastapi import Body, FastAPI, HTTPException, Request, Response | |
| from httpx import Client, Cookies, HTTPError | |
| from uniform_login_des import strEnc | |
| app = FastAPI(debug=True, title="统一认证短信登录") | |
| import os | |
| import httpx | |
| from fastapi import Body, FastAPI, HTTPException, Request, Response | |
| from httpx import Client, Cookies, HTTPError | |
| from uniform_login_des import strEnc | |
| DEBUG = os.getenv("SMS_AUTH_API_DEBUG", "").lower() in ("1", "true", "yes") | |
| app = FastAPI(debug=DEBUG, title="统一认证短信登录") |
| app = FastAPI(debug=True, title="统一认证短信登录") | ||
|
|
||
|
|
||
| login_sessions: dict[str, Cookies] = {} |
There was a problem hiding this comment.
login_sessions is a global in-memory dict used for auth session state. This is not safe under multi-threaded access and won’t work correctly with multiple Uvicorn/Gunicorn workers (sessions will be lost or inconsistent). Use a process-safe store (Redis/db) or at least guard access with a lock and document the single-process limitation.
sms_auth_api.py
Outdated
| raise HTTPException(503, "cas unavailable") | ||
| login_sessions[(ls := uuid7().hex)] = upstream.cookies | ||
| response = Response(upstream.content, media_type="image/gif") | ||
| response.set_cookie("login_session", ls, max_age=60, httponly=True) |
There was a problem hiding this comment.
The session cookie is set without secure and samesite attributes. For an authentication flow, consider secure=True (when served over HTTPS) and an explicit samesite policy to reduce CSRF risk; also consider path/domain scoping as appropriate.
| response.set_cookie("login_session", ls, max_age=60, httponly=True) | |
| response.set_cookie( | |
| "login_session", | |
| ls, | |
| max_age=60, | |
| httponly=True, | |
| secure=True, | |
| samesite="lax", | |
| ) |
| upstream = httpx.get(f"https://pass.sdu.edu.cn/cas/code?{random()}") | ||
| except HTTPError: |
There was a problem hiding this comment.
Outbound httpx.get/post calls have no explicit timeout, so a slow/unresponsive upstream can tie up your worker threads indefinitely. Pass a reasonable timeout (and consider reusing a shared httpx.Client for connection pooling).
| if res.json().get("redirectUrl") != "login": | ||
| raise HTTPException(400, "Invalid code") |
There was a problem hiding this comment.
res.json() can raise JSONDecodeError (and res could be a non-2xx response) which currently bubbles up as a 500. Consider calling res.raise_for_status() and handling JSON decode/shape errors, mapping them to a 502/503 with a clear message.
| client = Client() | ||
|
|
||
| if mobile == "": | ||
| mobile = input("手机号: ") | ||
| if service == "": | ||
| exit("请填写service") | ||
| while True: | ||
| window = Tk() | ||
| window.title("图片验证码") | ||
| window.bind("<Escape>", lambda e: window.destroy()) | ||
| photo = PhotoImage(data=client.get("https://pass.sdu.edu.cn/cas/code").content) | ||
| img_label = Label(window, image=photo) | ||
| img_label.pack(pady=10) | ||
| window.mainloop() | ||
| imgcode = input("图片验证码: ") | ||
| url = ( | ||
| client.post( | ||
| "https://pass.sdu.edu.cn/cas/loginByMorE", | ||
| data={ | ||
| "method": "sendMobileCode", | ||
| "sendConfirm": imgcode, | ||
| "mobile": mobile, | ||
| "random": random(), | ||
| }, | ||
| ) | ||
| .json() | ||
| .get("redirectUrl") | ||
| ) | ||
| if url == "login": | ||
| break | ||
| else: | ||
| print("图片验证码错误,请重试。") | ||
|
|
||
| mobilecode = input("短信验证码: ") | ||
| if ( | ||
| url := client.post( | ||
| "https://pass.sdu.edu.cn/cas/loginByMorE", | ||
| data={ | ||
| "method": "login", | ||
| "mobile": mobile, | ||
| "mobileCode": mobilecode, | ||
| "random": random(), | ||
| "service": service, | ||
| }, | ||
| ) | ||
| .json() | ||
| .get("redirectUrl") | ||
| ): | ||
| res = client.get(url, follow_redirects=True) | ||
| print(client.cookies) |
There was a problem hiding this comment.
The httpx.Client() is never closed. Wrap it in a context manager (with Client() as client:) or call client.close() at the end to avoid leaving connections open.
| client = Client() | |
| if mobile == "": | |
| mobile = input("手机号: ") | |
| if service == "": | |
| exit("请填写service") | |
| while True: | |
| window = Tk() | |
| window.title("图片验证码") | |
| window.bind("<Escape>", lambda e: window.destroy()) | |
| photo = PhotoImage(data=client.get("https://pass.sdu.edu.cn/cas/code").content) | |
| img_label = Label(window, image=photo) | |
| img_label.pack(pady=10) | |
| window.mainloop() | |
| imgcode = input("图片验证码: ") | |
| url = ( | |
| client.post( | |
| "https://pass.sdu.edu.cn/cas/loginByMorE", | |
| data={ | |
| "method": "sendMobileCode", | |
| "sendConfirm": imgcode, | |
| "mobile": mobile, | |
| "random": random(), | |
| }, | |
| ) | |
| .json() | |
| .get("redirectUrl") | |
| ) | |
| if url == "login": | |
| break | |
| else: | |
| print("图片验证码错误,请重试。") | |
| mobilecode = input("短信验证码: ") | |
| if ( | |
| url := client.post( | |
| "https://pass.sdu.edu.cn/cas/loginByMorE", | |
| data={ | |
| "method": "login", | |
| "mobile": mobile, | |
| "mobileCode": mobilecode, | |
| "random": random(), | |
| "service": service, | |
| }, | |
| ) | |
| .json() | |
| .get("redirectUrl") | |
| ): | |
| res = client.get(url, follow_redirects=True) | |
| print(client.cookies) | |
| with Client() as client: | |
| if mobile == "": | |
| mobile = input("手机号: ") | |
| if service == "": | |
| exit("请填写service") | |
| while True: | |
| window = Tk() | |
| window.title("图片验证码") | |
| window.bind("<Escape>", lambda e: window.destroy()) | |
| photo = PhotoImage(data=client.get("https://pass.sdu.edu.cn/cas/code").content) | |
| img_label = Label(window, image=photo) | |
| img_label.pack(pady=10) | |
| window.mainloop() | |
| imgcode = input("图片验证码: ") | |
| url = ( | |
| client.post( | |
| "https://pass.sdu.edu.cn/cas/loginByMorE", | |
| data={ | |
| "method": "sendMobileCode", | |
| "sendConfirm": imgcode, | |
| "mobile": mobile, | |
| "random": random(), | |
| }, | |
| ) | |
| .json() | |
| .get("redirectUrl") | |
| ) | |
| if url == "login": | |
| break | |
| else: | |
| print("图片验证码错误,请重试。") | |
| mobilecode = input("短信验证码: ") | |
| if ( | |
| url := client.post( | |
| "https://pass.sdu.edu.cn/cas/loginByMorE", | |
| data={ | |
| "method": "login", | |
| "mobile": mobile, | |
| "mobileCode": mobilecode, | |
| "random": random(), | |
| "service": service, | |
| }, | |
| ) | |
| .json() | |
| .get("redirectUrl") | |
| ): | |
| res = client.get(url, follow_redirects=True) | |
| print(client.cookies) |
| if service == "": | ||
| exit("请填写service") |
There was a problem hiding this comment.
Using exit("请填写service") relies on the interactive helper and is not guaranteed in non-interactive/script contexts. Prefer sys.exit(...) for scripts.
sms_auth_api.py
Outdated
|
|
||
| def clean_expired_sessions(): | ||
| now = datetime.now().timestamp() | ||
| [login_sessions.pop(u) for u in login_sessions if now > UUID(u).time + 60 * 1000] |
There was a problem hiding this comment.
clean_expired_sessions() mutates login_sessions while iterating over it (for u in login_sessions + pop), which will raise RuntimeError: dictionary changed size during iteration. Build a list of expired keys first (e.g., expired = [...]), then delete them in a second loop; also avoid list-comprehensions for side effects.
| [login_sessions.pop(u) for u in login_sessions if now > UUID(u).time + 60 * 1000] | |
| expired_keys = [] | |
| for u in login_sessions: | |
| if now > UUID(u).time + 60 * 1000: | |
| expired_keys.append(u) | |
| for u in expired_keys: | |
| login_sessions.pop(u, None) |
| @@ -0,0 +1,109 @@ | |||
| from datetime import datetime | |||
| from random import random | |||
| from uuid import UUID, uuid7 | |||
There was a problem hiding this comment.
from uuid import ... uuid7 will fail on Python versions where uuid7() is not available in the stdlib (common today). Either pin the project to a Python version that provides uuid7, or switch to a supported UUID generator (e.g., uuid4) and store timestamps separately for expiry.
| from fastapi import Body, FastAPI, HTTPException, Request, Response | ||
| from httpx import Client, Cookies, HTTPError | ||
|
|
||
| from uniform_login_des import strEnc | ||
|
|
||
| app = FastAPI(debug=True, title="统一认证短信登录") |
There was a problem hiding this comment.
This module introduces a FastAPI app, but requirements.txt currently does not include fastapi (or an ASGI server like uvicorn). As-is, a fresh install from requirements will fail at import/runtime; add the missing dependencies or document the expected runtime environment.
2. Refactor session management functions and update cookie expiration time
No description provided.