fix(ws): proxy Baidu WebSocket via Vite to bypass Origin 403

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dongzp 2026-07-22 10:01:23 +08:00
parent 497236b8b4
commit 1c1f370d59
3 changed files with 35 additions and 6 deletions

View File

@ -31,7 +31,7 @@
``` ```
页面挂载 页面挂载
→ quota.fetchQuote() // HTTP 全量 → quota.fetchQuote() // HTTP 全量
→ BaiduQuoteWs.connect() // wss://finance-ws.pae.baidu.com/ → BaiduQuoteWs.connect() // ws(s)://{host}/finance-ws/ → Vite/Nginx 代理到百度
→ subscribe tick + snapshot → subscribe tick + snapshot
→ 每 6s ping → 每 6s ping
→ 每 60s patch snapshot → 每 60s patch snapshot
@ -58,7 +58,7 @@
## 4. 协议细节 ## 4. 协议细节
**端点:** `wss://finance-ws.pae.baidu.com/` **端点:** 浏览器连同域 `/finance-ws/`;由 Vite(开发)或 Nginx(生产)代理到 `wss://finance-ws.pae.baidu.com/`,并改写 `Origin`/`Referer` 为 `https://gushitong.baidu.com`(否则 localhost Origin 会 403)。
**订阅(open 后立即发送两条):** **订阅(open 后立即发送两条):**
@ -143,4 +143,4 @@
- WS 与 HTTP 的 snapshot 字段结构不完全一致(HTTP 用 `origin_pankou`,WS 用 `pankouinfos` 数组);mapper 需独立,勿强行复用 HTTP mapper 全文 - WS 与 HTTP 的 snapshot 字段结构不完全一致(HTTP 用 `origin_pankou`,WS 用 `pankouinfos` 数组);mapper 需独立,勿强行复用 HTTP mapper 全文
- 去重键不含服务端唯一 id(协议未提供);极端情况下同秒同价同量同向两笔可能被误去重,可接受 - 去重键不含服务端唯一 id(协议未提供);极端情况下同秒同价同量同向两笔可能被误去重,可接受
- 跨域 / 鉴权:浏览器直连百度 WS;若环境拦截再评估代理,本期先直连 - 跨域 / 鉴权:浏览器不能直连(Origin=localhost → 403)。开发经 Vite 代理 `/finance-ws`,并把 Origin/Referer 改写为 `https://gushitong.baidu.com`;生产需同等反向代理。

View File

@ -1,12 +1,21 @@
import { contractConfig } from '../../config/contract' import { contractConfig } from '../../config/contract'
import type { BaiduWsItem, BaiduWsMessage, BaiduWsOutbound } from './wsTypes' import type { BaiduWsItem, BaiduWsMessage, BaiduWsOutbound } from './wsTypes'
const WS_URL = 'wss://finance-ws.pae.baidu.com/'
const PING_MS = 6_000 const PING_MS = 6_000
const PATCH_MS = 60_000 const PATCH_MS = 60_000
const RECONNECT_BASE_MS = 1_000 const RECONNECT_BASE_MS = 1_000
const RECONNECT_MAX_MS = 30_000 const RECONNECT_MAX_MS = 30_000
/**
* Same-origin path proxied by Vite/Nginx to wss://finance-ws.pae.baidu.com.
* Direct browser connect sends Origin=localhost and gets 403.
*/
function resolveWsUrl(): string {
const proto = typeof location !== 'undefined' && location.protocol === 'https:' ? 'wss:' : 'ws:'
const host = typeof location !== 'undefined' ? location.host : 'localhost:5173'
return `${proto}//${host}/finance-ws/`
}
export type BaiduQuoteWsHandlers = { export type BaiduQuoteWsHandlers = {
onMessage: (msg: BaiduWsMessage) => void onMessage: (msg: BaiduWsMessage) => void
onError?: (err: unknown) => void onError?: (err: unknown) => void
@ -71,7 +80,7 @@ export class BaiduQuoteWs {
private openSocket(): void { private openSocket(): void {
if (this.intentionalClose) return if (this.intentionalClose) return
const ws = new WebSocket(WS_URL) const ws = new WebSocket(resolveWsUrl())
this.ws = ws this.ws = ws
ws.onopen = () => { ws.onopen = () => {

View File

@ -2,6 +2,8 @@ import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
const BAIDU_ORIGIN = 'https://gushitong.baidu.com'
export default defineConfig({ export default defineConfig({
plugins: [vue()], plugins: [vue()],
resolve: { resolve: {
@ -11,12 +13,30 @@ export default defineConfig({
}, },
server: { server: {
proxy: { proxy: {
/**
* WebSocket must be registered before `/baidu` so it is not swallowed.
* Baidu rejects non-baidu Origins (localhost → 403); rewrite Origin here.
*/
'/finance-ws': {
target: 'wss://finance-ws.pae.baidu.com',
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(/^\/finance-ws/, '') || '/',
configure: (proxy) => {
const setBaiduHeaders = (proxyReq: { setHeader: (k: string, v: string) => void }) => {
proxyReq.setHeader('Origin', BAIDU_ORIGIN)
proxyReq.setHeader('Referer', `${BAIDU_ORIGIN}/`)
}
proxy.on('proxyReq', setBaiduHeaders)
proxy.on('proxyReqWs', setBaiduHeaders)
},
},
'/baidu': { '/baidu': {
target: 'https://finance.pae.baidu.com', target: 'https://finance.pae.baidu.com',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/baidu/, ''), rewrite: (path) => path.replace(/^\/baidu/, ''),
headers: { headers: {
Referer: 'https://gushitong.baidu.com/', Referer: `${BAIDU_ORIGIN}/`,
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
}, },