fix(security): complete security and delivery compliance remediation

This commit is contained in:
2026-07-28 17:56:28 +08:00
parent 5b07ec6df0
commit 81ab82e9ba
32 changed files with 798 additions and 94 deletions
+42 -19
View File
@@ -1,33 +1,48 @@
"""认证 API"""
import base64
import json
from fastapi import APIRouter, Depends, HTTPException, Header
import hmac
import secrets
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from fastapi import APIRouter, Depends, HTTPException, Header, Request, Response
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.casdoor import casdoor_sdk
from app.core.security import create_access_token, verify_token
from app.core.errors import internal_error
from app.core.security import create_access_token, verify_casdoor_token, verify_token
from app.core.config import settings
from app.models.user import User
from app.schemas.auth import Token, UserInfo
from datetime import datetime
router = APIRouter(prefix="/api/auth", tags=["认证"])
OAUTH_STATE_COOKIE = "h3c_oauth_state"
OAUTH_STATE_TTL_SECONDS = 300
def decode_jwt_payload(token: str) -> dict:
"""直接解码 JWT payload,不验签(Casdoor 已完成认证)"""
payload_b64 = token.split(".")[1]
rem = len(payload_b64) % 4
if rem:
payload_b64 += "=" * (4 - rem)
return json.loads(base64.urlsafe_b64decode(payload_b64))
def _with_oauth_state(url: str, state: str) -> str:
"""Replace the SDK-generated state with the browser-bound state value."""
parts = urlsplit(url)
query = [(key, value) for key, value in parse_qsl(parts.query, keep_blank_values=True) if key != "state"]
query.append(("state", state))
return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment))
@router.get("/login")
def login():
def login(response: Response):
"""获取 Casdoor 登录 URL"""
return {"url": casdoor_sdk.get_auth_link(settings.CASDOOR_REDIRECT_URL)}
state = secrets.token_urlsafe(32)
response.set_cookie(
key=OAUTH_STATE_COOKIE,
value=state,
max_age=OAUTH_STATE_TTL_SECONDS,
httponly=True,
secure=not settings.DEBUG,
samesite="lax",
path="/api/auth",
)
login_url = casdoor_sdk.get_auth_link(settings.CASDOOR_REDIRECT_URL)
return {"url": _with_oauth_state(login_url, state)}
class CallbackRequest(BaseModel):
@@ -36,18 +51,29 @@ class CallbackRequest(BaseModel):
@router.post("/callback", response_model=Token)
def callback(body: CallbackRequest, db: Session = Depends(get_db)):
def callback(
body: CallbackRequest,
request: Request,
response: Response,
db: Session = Depends(get_db),
):
"""Casdoor 登录回调"""
try:
expected_state = request.cookies.get(OAUTH_STATE_COOKIE)
response.delete_cookie(OAUTH_STATE_COOKIE, path="/api/auth")
if not expected_state or not hmac.compare_digest(body.state, expected_state):
raise HTTPException(status_code=400, detail="登录状态校验失败,请重新登录")
token_response = casdoor_sdk.get_oauth_token(code=body.code)
if isinstance(token_response, dict) and "error" in token_response:
raise HTTPException(status_code=400, detail=token_response.get("error_description", token_response["error"]))
access_token = token_response.get("access_token") if isinstance(token_response, dict) else token_response
identity_token = token_response.get("id_token") if isinstance(token_response, dict) else None
if not access_token:
raise HTTPException(status_code=400, detail="Casdoor 未返回 access_token")
casdoor_user = decode_jwt_payload(access_token)
casdoor_user = verify_casdoor_token(identity_token or access_token)
user = db.query(User).filter(User.casdoor_id == casdoor_user["sub"]).first()
if not user:
@@ -76,10 +102,7 @@ def callback(body: CallbackRequest, db: Session = Depends(get_db)):
except HTTPException:
raise
except Exception as e:
import traceback
import logging
logging.getLogger(__name__).error("callback error: %s\n%s", e, traceback.format_exc())
raise HTTPException(status_code=500, detail=str(e))
raise internal_error("Casdoor login callback", e)
@router.get("/permissions")