package service import ( "crypto/rand" "encoding/hex" "errors" "sync" "time" ) // WebSocket 票据默认有效期:短期、一次性,把 /ws 握手从长期 JWT 中解耦。 // 浏览器不能把 JWT 放 URL,也不能仅凭 URL 里的公司 ID 订阅,所以先用受保护的接口 // 换取一张短暂票据,再凭它建连。 const WebSocketTicketTTL = 30 * time.Second var ( // ErrWebSocketTicketInvalid 表示票据不存在、格式非法或已被消费(重放)。 ErrWebSocketTicketInvalid = errors.New("websocket ticket invalid or already used") // ErrWebSocketTicketExpired 表示票据已超过有效期。 ErrWebSocketTicketExpired = errors.New("websocket ticket expired") ) // WebSocketTicket 绑定一次握手所需的身份与公司:票据 ID 本身即一次性随机标识, // 连同 userID、organizationID 与签发时间一起记录。本系统当前没有会话 ID, // 因此用“随机票据 + 签发时间 + 数据库实时复核”共同承担“会话标识”的职责。 type WebSocketTicket struct { Ticket string UserID int64 OrganizationID int64 IssuedAt time.Time ExpiresAt time.Time } // WebSocketTicketService 维护未消费的票据。票据只驻留内存,进程重启即全部失效。 type WebSocketTicketService struct { ttl time.Duration now func() time.Time mu sync.Mutex tickets map[string]*WebSocketTicket } // NewWebSocketTicketService 构造票据服务。ttl 可选,测试用它构造“已过期”票据, // 不传则使用默认 30 秒。 func NewWebSocketTicketService(ttl ...time.Duration) *WebSocketTicketService { effective := WebSocketTicketTTL if len(ttl) > 0 { effective = ttl[0] } return &WebSocketTicketService{ ttl: effective, now: time.Now, tickets: make(map[string]*WebSocketTicket), } } // Issue 为指定用户与公司签发一张一次性票据。 func (s *WebSocketTicketService) Issue(userID, organizationID int64) (*WebSocketTicket, error) { if userID <= 0 || organizationID <= 0 { return nil, ErrInvalidOrganizationID } raw := make([]byte, 32) if _, err := rand.Read(raw); err != nil { return nil, err } now := s.now() ticket := &WebSocketTicket{ Ticket: hex.EncodeToString(raw), UserID: userID, OrganizationID: organizationID, IssuedAt: now, ExpiresAt: now.Add(s.ttl), } s.mu.Lock() defer s.mu.Unlock() // 顺手清理已过期票据,避免长期运行后 map 无限增长。 s.purgeExpiredLocked(now) s.tickets[ticket.Ticket] = ticket return ticket, nil } // Consume 校验并一次性消费票据。无论是否过期都先删除:过期票据、已消费票据 // 和重放请求都无法再次连接。返回的票据已通过有效期检查。 func (s *WebSocketTicketService) Consume(value string) (*WebSocketTicket, error) { if value == "" { return nil, ErrWebSocketTicketInvalid } s.mu.Lock() defer s.mu.Unlock() ticket, ok := s.tickets[value] if !ok { return nil, ErrWebSocketTicketInvalid } delete(s.tickets, value) if s.now().After(ticket.ExpiresAt) { return nil, ErrWebSocketTicketExpired } return ticket, nil } func (s *WebSocketTicketService) purgeExpiredLocked(now time.Time) { for key, ticket := range s.tickets { if now.After(ticket.ExpiresAt) { delete(s.tickets, key) } } } // defaultTicketService 是进程内共享的票据服务,签发接口与握手共用同一实例。 var defaultTicketService = NewWebSocketTicketService() // GetWebSocketTicketService 返回共享票据服务。 func GetWebSocketTicketService() *WebSocketTicketService { return defaultTicketService } // SetDefaultWebSocketTicketService 仅供测试替换共享票据服务,返回恢复函数。 // 生产代码请使用 GetWebSocketTicketService。 func SetDefaultWebSocketTicketService(s *WebSocketTicketService) func() { previous := defaultTicketService defaultTicketService = s return func() { defaultTicketService = previous } }