diff --git a/app/chat/api/etc/chat-api.yaml b/app/chat/api/etc/chat-api.yaml index fff721d..a4a04d1 100644 --- a/app/chat/api/etc/chat-api.yaml +++ b/app/chat/api/etc/chat-api.yaml @@ -7,7 +7,7 @@ Hybrid: Protocol: auto Ws: Name: chat-ws - Addr: :8889 + Addr: :8888 Path: /ws/chat MaxConnections: 10000 Auth: @@ -19,12 +19,17 @@ Hybrid: Path: /wt/chat CertFile: /etc/certs/tls.crt KeyFile: /etc/certs/tls.key + Auth: + Enabled: true FallbackStrategy: auto MaxRetries: 3 MaxConnections: 10000 Auth: Enabled: true WsHeaderName: x-auth-user-id + WtTokenSource: cookie + WtTokenName: JToken + WtJWTSecret: MGUyMWE3ZDhjMTQ5ZDg1MWViOWU0MGM3OTE2NWVkYTBlOTE5ZWRkZDU1YjYzOGJjOWRiNzM0NTc4NDIyMjlkZQ Stateless: PollInterval: 100ms diff --git a/app/chat/api/internal/handler/chat/messaging.go b/app/chat/api/internal/handler/chat/messaging.go index b218552..bba909b 100644 --- a/app/chat/api/internal/handler/chat/messaging.go +++ b/app/chat/api/internal/handler/chat/messaging.go @@ -64,6 +64,12 @@ func (h *Handler) handleJoin(conn protocol.Connection, msg *WsMessage) error { func (h *Handler) handleLeave(conn protocol.Connection, msg *WsMessage) error { uid := h.getUserId(conn) + if uid <= 0 { + return conn.SendJSON(context.Background(), WsResponse{ + Type: "error", + Content: "authentication required", + }) + } sessionId := msg.SessionId if sessionId <= 0 { if sid, ok := conn.Metadata()["sessionId"].(int64); ok { @@ -73,6 +79,12 @@ func (h *Handler) handleLeave(conn protocol.Connection, msg *WsMessage) error { if sessionId <= 0 { return nil } + if !h.svcCtx.Store.IsParticipant(sessionId, uid) { + return conn.SendJSON(context.Background(), WsResponse{ + Type: "error", + Content: "not a member of this session", + }) + } session, err := h.svcCtx.Store.GetSession(sessionId) if err == nil { @@ -108,6 +120,12 @@ func (h *Handler) handleMessage(conn protocol.Connection, msg *WsMessage) error Content: "sessionId is required, join a session first", }) } + if !h.svcCtx.Store.IsParticipant(sessionId, uid) { + return conn.SendJSON(context.Background(), WsResponse{ + Type: "error", + Content: "not a member of this session", + }) + } msgType := chatcore.MessageType(msg.MsgType) if msgType == "" { @@ -151,12 +169,25 @@ func (h *Handler) handleMessage(conn protocol.Connection, msg *WsMessage) error } func (h *Handler) handleHistory(conn protocol.Connection, msg *WsMessage) error { + uid := h.getUserId(conn) + if uid <= 0 { + return conn.SendJSON(context.Background(), WsResponse{ + Type: "error", + Content: "authentication required", + }) + } if msg.SessionId <= 0 { return conn.SendJSON(context.Background(), WsResponse{ Type: "error", Content: "sessionId is required", }) } + if !h.svcCtx.Store.IsParticipant(msg.SessionId, uid) { + return conn.SendJSON(context.Background(), WsResponse{ + Type: "error", + Content: "not a member of this session", + }) + } messages := h.svcCtx.Store.GetMessages(msg.SessionId, 0, 50) diff --git a/app/chat/chatcore/store.go b/app/chat/chatcore/store.go index 17fa313..65b422e 100644 --- a/app/chat/chatcore/store.go +++ b/app/chat/chatcore/store.go @@ -109,6 +109,22 @@ func (s *Store) GetSession(id int64) (*Session, error) { return session, nil } +func (s *Store) IsParticipant(sessionId, userId int64) bool { + s.mu.RLock() + defer s.mu.RUnlock() + + session, ok := s.Sessions[sessionId] + if !ok { + return false + } + for _, p := range session.Participants { + if p == userId { + return true + } + } + return false +} + func (s *Store) ListUserSessions(userId int64, page, limit int) []*Session { s.mu.RLock() defer s.mu.RUnlock() diff --git a/app/chat/test/chat-api-local.yaml b/app/chat/test/chat-api-local.yaml index 879fe2b..d84cf18 100644 --- a/app/chat/test/chat-api-local.yaml +++ b/app/chat/test/chat-api-local.yaml @@ -7,7 +7,7 @@ Hybrid: Protocol: auto Ws: Name: chat-ws - Addr: :28889 + Addr: :28888 Path: /ws/chat MaxConnections: 10000 Auth: diff --git a/app/chat/test/chat-api-test.yaml b/app/chat/test/chat-api-test.yaml index 85eb6d2..8ded9b2 100644 --- a/app/chat/test/chat-api-test.yaml +++ b/app/chat/test/chat-api-test.yaml @@ -7,7 +7,7 @@ Hybrid: Protocol: auto Ws: Name: chat-ws - Addr: :8889 + Addr: :8888 Path: /ws/chat MaxConnections: 10000 Auth: @@ -19,6 +19,8 @@ Hybrid: Path: /wt/chat CertFile: /etc/certs/tls.crt KeyFile: /etc/certs/tls.key + Auth: + Enabled: true FallbackStrategy: auto MaxRetries: 3 MaxConnections: 10000 diff --git a/app/chat/test/docker-compose.yml b/app/chat/test/docker-compose.yml index 43825b5..e657a8b 100644 --- a/app/chat/test/docker-compose.yml +++ b/app/chat/test/docker-compose.yml @@ -6,7 +6,6 @@ services: container_name: chat-api-test ports: - "28888:8888" - - "28889:8889" - "28443:8443/udp" volumes: - ./certs:/etc/certs:ro diff --git a/app/chat/test/test_ws.py b/app/chat/test/test_ws.py index bc685a1..9006421 100644 --- a/app/chat/test/test_ws.py +++ b/app/chat/test/test_ws.py @@ -14,7 +14,7 @@ except ImportError: subprocess.check_call([sys.executable, "-m", "pip", "install", "websockets", "-q"]) import websockets -WS_URL = "ws://localhost:28889/ws/chat" +WS_URL = "ws://localhost:28888/ws/chat" RESULTS = [] def log(tag, msg): diff --git a/app/chat/test/test_wt.py b/app/chat/test/test_wt.py index 8a6595e..f29d7ec 100644 --- a/app/chat/test/test_wt.py +++ b/app/chat/test/test_wt.py @@ -5,8 +5,6 @@ import asyncio import json import sys import time -import urllib.request -import urllib.error try: import websockets @@ -15,7 +13,7 @@ except ImportError: subprocess.check_call([sys.executable, "-m", "pip", "install", "websockets", "-q"]) import websockets -WS_URL = "ws://localhost:28889/ws/chat" +WS_URL = "ws://localhost:28888/ws/chat" API_BASE = "http://localhost:28888" RESULTS = [] diff --git a/app/users/rpc/internal/utils/jwt.go b/app/users/rpc/internal/utils/jwt.go index e6815df..7679296 100644 --- a/app/users/rpc/internal/utils/jwt.go +++ b/app/users/rpc/internal/utils/jwt.go @@ -67,6 +67,7 @@ func (m *JwtManager) New(ctx context.Context, payload *TokenPayload) (string, er ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(now), Issuer: m.issuer, + Subject: strconv.FormatInt(payload.UserId, 10), }, } diff --git a/deploy/dev/README.md b/deploy/dev/README.md index 34acb0c..8cccb5d 100644 --- a/deploy/dev/README.md +++ b/deploy/dev/README.md @@ -31,6 +31,8 @@ docker compose down 端到端接口测试走网关 `http://127.0.0.1:18080`,`18801-18814` 是各服务的直连端口,不经过认证链路。 +Chat WebSocket 通过网关 `ws://127.0.0.1:18080/ws/chat` 访问。WebTransport 使用 `18443/udp` 的 `/wt/chat` 入口。 + 如需只启动部分服务: ```bash @@ -39,26 +41,26 @@ docker compose up -d postgres redis snowflake player-rpc player-api ## 端口映射 -| 服务 | 宿主机端口 | -| ---------------- | ---------- | -| PostgreSQL | 15432 | -| Redis | 16379 | -| Kafka | 19092 | -| Envoy Gateway | 18080 | -| users-api | 18801 | -| player-api | 18802 | -| game-api | 18803 | -| shop-api | 18804 | -| order-api | 18805 | -| wallet-api | 18806 | -| community-api | 18807 | -| objectstory-api | 18808 | -| email-api | 18809 | -| chat-api | 18810 | -| review-api | 18811 | -| dispute-api | 18812 | -| notification-api | 18813 | -| search-api | 18814 | +| 服务 | 宿主机端口 | +| ---------------- | ---------------- | +| PostgreSQL | 15432 | +| Redis | 16379 | +| Kafka | 19092 | +| Envoy Gateway | 18080 | +| users-api | 18801 | +| player-api | 18802 | +| game-api | 18803 | +| shop-api | 18804 | +| order-api | 18805 | +| wallet-api | 18806 | +| community-api | 18807 | +| objectstory-api | 18808 | +| email-api | 18809 | +| chat-api | 18810, 18443/udp | +| review-api | 18811 | +| dispute-api | 18812 | +| notification-api | 18813 | +| search-api | 18814 | ## 环境变量 diff --git a/deploy/dev/docker-compose.yml b/deploy/dev/docker-compose.yml index 2833400..0092593 100644 --- a/deploy/dev/docker-compose.yml +++ b/deploy/dev/docker-compose.yml @@ -444,7 +444,6 @@ services: restart: unless-stopped ports: - "18810:8888" - - "18889:8889" - "18443:8443/udp" volumes: - ./certs:/etc/certs:ro @@ -464,6 +463,8 @@ services: condition: service_started order-rpc: condition: service_started + player-rpc: + condition: service_started dispute-api: image: juwan/dispute-api:dev @@ -477,6 +478,8 @@ services: condition: service_started order-rpc: condition: service_started + player-rpc: + condition: service_started notification-api: image: juwan/notification-api:dev diff --git a/deploy/dev/envoy.yaml b/deploy/dev/envoy.yaml index 0dc1575..5e31842 100644 --- a/deploy/dev/envoy.yaml +++ b/deploy/dev/envoy.yaml @@ -375,10 +375,12 @@ static_resources: timeout: 30s - match: - prefix: /api/v1/chat + path: /ws/chat route: cluster: chat_api_cluster - timeout: 30s + timeout: 0s + upgrade_configs: + - upgrade_type: websocket - match: path: /api/v1/upload @@ -597,6 +599,10 @@ static_resources: rules: - match: path: /healthz + - match: + path: /ws/chat + requires: + provider_name: juwan_user_jwt - match: prefix: /api/v1 headers: diff --git a/desc/api/chat.api b/desc/api/chat.api deleted file mode 100644 index e38ab9d..0000000 --- a/desc/api/chat.api +++ /dev/null @@ -1,70 +0,0 @@ -syntax = "v1" - -import "common.api" - -type ( - SessionIdReq { - Id int64 `path:"id"` - } - CreateGroupReq { - Name string `json:"name"` - Participants []int64 `json:"participants,optional"` - } - CreateDMReq { - TargetId int64 `json:"targetId"` - } - ChatSession { - Id int64 `json:"id"` - Type string `json:"type"` - Name string `json:"name"` - CreatorId int64 `json:"creatorId"` - Participants []int64 `json:"participants"` - LastMessage string `json:"lastMessage"` - LastMessageAt int64 `json:"lastMessageAt"` - CreatedAt int64 `json:"createdAt"` - } - ChatSessionListResp { - Items []ChatSession `json:"items"` - } - ChatMessage { - Id int64 `json:"id"` - SessionId int64 `json:"sessionId"` - SenderId int64 `json:"senderId"` - Type string `json:"type"` - Content string `json:"content"` - CreatedAt int64 `json:"createdAt"` - } - ChatMessageListResp { - Items []ChatMessage `json:"items"` - } - ListMessageReq { - SessionIdReq - PageReq - } -) - -@server ( - prefix: api/v1/chat - group: chat -) -service chat-api { - @doc "create group session" - @handler CreateGroup - post /sessions/group (CreateGroupReq) returns (ChatSession) - - @doc "create dm session" - @handler CreateDM - post /sessions/dm (CreateDMReq) returns (ChatSession) - - @doc "list user sessions" - @handler ListSessions - get /sessions (PageReq) returns (ChatSessionListResp) - - @doc "get session detail" - @handler GetSession - get /sessions/:id (SessionIdReq) returns (ChatSession) - - @doc "get message history" - @handler ListMessages - get /sessions/:id/messages (ListMessageReq) returns (ChatMessageListResp) -} diff --git a/docs/PROJECT_GUIDE.md b/docs/PROJECT_GUIDE.md index 923da29..689f6cc 100644 --- a/docs/PROJECT_GUIDE.md +++ b/docs/PROJECT_GUIDE.md @@ -14,7 +14,7 @@ Juwan 是一个基于 Go-Zero 微服务框架的分布式后端系统,采用 │ │ ┌───▼────────┐ ┌───▼────────┐ │ User API │ │ Order API │ - │ (8888) │ │ (8889) │ + │ (8888) │ │ (8888) │ └───┬────────┘ └────────────┘ │ ┌───▼────────────────────┐