fix: bypass JWT for voice WebSocket route (fixes 401 on WS upgrade)
根因:Kong 日志显示 voice WebSocket 连接被 JWT 插件返回 401,
因为 WebSocket RFC 6455 不支持自定义 header,Flutter 的
WebSocketChannel.connect 无法携带 Authorization header。
修复策略(业界标准做法):
1. Kong: 将 voice-service 的 JWT 从 service 级别改为 route
级别,仅在 voice-api 和 twilio-webhook 路由启用 JWT,
voice-ws 路由免除(session 创建已通过 JWT 验证,
session_id 本身作为认证凭据)
2. 后端: session_router 返回的 websocket_url 改为
/ws/voice/{session_id}(匹配 Kong voice-ws 路由路径)
3. FastAPI: 在 app 级别增加 /ws/voice/{session_id} 顶级
WebSocket 路由,委托给 session_router 的 handler
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2a87dd346e
commit
6cd53e713c
|
|
@ -186,7 +186,14 @@ plugins:
|
|||
- exp
|
||||
|
||||
- name: jwt
|
||||
service: voice-service
|
||||
route: voice-api
|
||||
config:
|
||||
key_claim_name: kid
|
||||
claims_to_verify:
|
||||
- exp
|
||||
|
||||
- name: jwt
|
||||
route: twilio-webhook
|
||||
config:
|
||||
key_claim_name: kid
|
||||
claims_to_verify:
|
||||
|
|
|
|||
|
|
@ -31,6 +31,21 @@ app.include_router(session_router, prefix="/api/v1/voice", tags=["sessions"])
|
|||
app.include_router(twilio_router, prefix="/api/v1/twilio", tags=["twilio"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Top-level WebSocket route for Kong's voice-ws route (/ws/voice/*)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kong forwards the full path /ws/voice/{session_id} without JWT.
|
||||
# We import the handler from session_router and re-expose it here so the
|
||||
# path matches exactly.
|
||||
from fastapi import WebSocket as _WS # noqa: E402
|
||||
|
||||
@app.websocket("/ws/voice/{session_id}")
|
||||
async def voice_ws_proxy(websocket: _WS, session_id: str):
|
||||
"""Proxy route that delegates to the session_router WebSocket handler."""
|
||||
from .session_router import voice_websocket
|
||||
await voice_websocket(websocket, session_id)
|
||||
|
||||
|
||||
async def _session_cleanup_loop():
|
||||
"""Periodically remove sessions that have been disconnected longer than session_ttl.
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ async def create_session(request: CreateSessionRequest, req: Request):
|
|||
"task": None,
|
||||
}
|
||||
|
||||
websocket_url = f"/api/v1/voice/ws/{session_id}"
|
||||
websocket_url = f"/ws/voice/{session_id}"
|
||||
|
||||
return SessionResponse(
|
||||
session_id=session_id,
|
||||
|
|
@ -155,7 +155,7 @@ async def reconnect_session(session_id: str, req: Request):
|
|||
content={"error": "Session expired", "session_id": session_id},
|
||||
)
|
||||
|
||||
websocket_url = f"/api/v1/voice/ws/{session_id}"
|
||||
websocket_url = f"/ws/voice/{session_id}"
|
||||
return SessionResponse(
|
||||
session_id=session_id,
|
||||
status="disconnected",
|
||||
|
|
|
|||
Loading…
Reference in New Issue