# P2E — GoChat 认证授权与实时通信架构设计 > 版本: v1.1 | 更新日期: 2026-07-09 > 参照: Chatwoot DeviseTokenAuth / Pundit / ActionCable / Dispatcher / Listener > 状态:设计文档,认证授权与 WebSocket 已落地。当前 auth 包含 JWT/OAuth/MFA/SAML/RBAC/Platform Auth。 --- ## 1. JWT 认证体系 ### 1.1 对比 Chatwoot DeviseTokenAuth | 特性 | Chatwoot (Rails) | GoChat (Go) | |---|---|---| | 认证库 | DeviseTokenAuth gem | 自实现 JWT middleware | | Token存储 | 多token机制(client_id+token对) | 单JWT + Refresh token | | Token传递 | HTTP headers(access-token/client/uid) | Authorization: Bearer | | Token刷新 | 每次请求自动刷新(竞态锁定) | Refresh token 端点显式刷新 | | 多账户 | Devise scope切换 | JWT claims 含 account_id | | MFA | Devise two_factor_authentication | TOTP 验证中间件 | | OAuth | Omniauth callbacks | OAuth2 redirect flow | | SAML | Devise_saml_authenticatable | 自实现 SAML SP(企业版) | ### 1.2 JWT Token 结构 ```go // token/jwt.go type Claims struct { UserID uint `json:"user_id"` AccountID uint `json:"account_id"` // 当前活跃账户 Role string `json:"role"` // agent/administrator/custom_role Provider string `json:"provider"` // email/google/saml CustomRoleID uint `json:"custom_role_id,omitempty"` // 企业版自定义角色 jwt.RegisteredClaims } ``` **Access Token**: 15分钟过期,含完整Claims **Refresh Token**: 7天过期,仅含 UserID + Provider,存储在Redis ### 1.3 认证流程 ``` 1. 用户登录 POST /api/v1/auth/login → 验证 email+password (bcrypt) → 查询 AccountUser 获取角色 → 生成 Access Token + Refresh Token → 返回 {user, access_token, refresh_token} 2. 切换账户 POST /api/v1/auth/switch_account → JWT claims.account_id 更换 → 重新签发 Access Token(新 account_id) 3. Token刷新 POST /api/v1/auth/refresh → 验证 Refresh Token → 重新签发 Access Token 4. OAuth登录 GET /api/v1/auth/:provider/callback → Google OAuth2 redirect → callback → 创建/查找 User + AccountUser → 签发 JWT 5. 企业版SAML (见 §4) ``` ### 1.4 认证中间件 ```go // middleware/auth.go func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token := extractBearerToken(c) claims, err := ValidateAccessToken(token) if err != nil { c.AbortWithStatusJSON(401, gin.H{"error": "unauthenticated"}) return } c.Set("current_user_id", claims.UserID) c.Set("current_account_id", claims.AccountID) c.Set("current_role", claims.Role) c.Set("current_custom_role_id", claims.CustomRoleID) c.Next() } } ``` **认证级别分层:** - `PublicAPI` — 无认证(WebWidget嵌入、CSAT提交、Portal文章) - `AuthenticatedAPI` — AuthMiddleware(绝大多数API) - `AdminAPI` — AuthMiddleware + RoleCheck("administrator")(账户设置、团队管理等) - `EnterpriseAPI` — AuthMiddleware + FeatureFlagCheck("enterprise_*")(企业版功能) - `SuperAdminAPI` — SuperAdmin middleware(平台管理API) --- ## 2. RBAC 权限系统 ### 2.1 对比 Chatwoot Pundit + CustomRole | 特性 | Chatwoot | GoChat | |---|---|---| | 权限框架 | Pundit (Policy类) | 中间件+Policy函数 | | 内置角色 | agent / administrator | agent / administrator | | 自定义角色 | CustomRole (企业版, 6权限维度) | CustomRole (企业版, 同6维度) | | Policy检查 | `authorize @resource` in controller | `policy.Check(user, resource, action)` middleware | | 范围过滤 | `scope = Policy::Scope.resolve` | `policy.Scope(user, resource)` 返回过滤条件 | ### 2.2 角色体系 ```go // model/role.go type Role string const ( RoleAgent Role = "agent" RoleAdministrator Role = "administrator" ) // 企业版 CustomRole type CustomRole struct { ID uint `gorm:"primaryKey"` AccountID uint `gorm:"index"` Name string `gorm:"size:255"` Permissions Permissions `gorm:"type:jsonb"` // 6维度权限 } type Permissions struct { ConversationManage PermissionLevel `json:"conversation_manage"` // full/read ConversationDelete PermissionLevel `json:"conversation_delete"` // full/none ContactManage PermissionLevel `json:"contact_manage"` // full/read ReportManage PermissionLevel `json:"report_manage"` // full/none KnowledgeBaseManage PermissionLevel `json:"knowledge_base_manage"` // full/read AutomationManage PermissionLevel `json:"automation_manage"` // full/none } type PermissionLevel string const ( PermissionFull PermissionLevel = "full" PermissionRead PermissionLevel = "read" PermissionNone PermissionLevel = "none" ) ``` ### 2.3 权限检查实现 ```go // policy/base.go type PolicyContext struct { UserID uint AccountID uint Role Role CustomRoleID uint Permissions Permissions // 企业版加载 } func (pc *PolicyContext) Can(action string, resource string) bool { // administrator → 全权限 if pc.Role == RoleAdministrator { return true } // agent → 基本权限 if pc.Role == RoleAgent { return agentDefaultPermissions[action][resource] } // custom_role → 查权限矩阵 return checkCustomPermission(pc.Permissions, action, resource) } // 在中间件中注入 func PolicyMiddleware(resource string, action string) gin.HandlerFunc { return func(c *gin.Context) { pc := buildPolicyContext(c) if !pc.Can(action, resource) { c.AbortWithStatusJSON(403, gin.H{"error": "unauthorized"}) return } c.Set("policy_context", pc) c.Next() } } ``` ### 2.4 与Chatwoot对比的关键简化 1. **去除Pundit Policy类** — 每个资源一个Policy文件(如AccountPolicy, ConversationPolicy),GoChat统一为一个PolicyContext+权限矩阵 2. **Scope过滤合并** — Chatwoot的Policy::Scope(返回过滤后的数据集),GoChat用PolicyContext.Scope()返回GORM WHERE条件 3. **CustomRole权限合并** — Chatwoot的AccountUser.role + CustomRole双判断,GoChat在JWT claims中直接合并 --- ## 3. AccountUser 多账户角色切换 ### 3.1 模型设计 ```go // model/account_user.go type AccountUser struct { ID uint `gorm:"primaryKey"` AccountID uint `gorm:"index;not null"` UserID uint `gorm:"index;not null"` Role Role `gorm:"size:255;not null;default:'agent'"` CustomRoleID uint `gorm:"index"` // 企业版 Availability string `gorm:"size:255;default:'offline'"` // online/offline/busy AutoOffline bool `gorm:"default:false"` AgentCapacityID uint `gorm:"index"` // 企业版容量策略 ActiveAt *time.Time InvitedByID uint CreatedAt time.Time UpdatedAt time.Time } ``` ### 3.2 切换流程 ``` 1. 用户登录 → 默认选择第一个AccountUser对应的账户 2. JWT claims.account_id = 选中的账户ID 3. 切换账户 → POST /api/v1/auth/switch_account {account_id: N} → 验证 AccountUser 存在 → 重新签发JWT(新account_id + 对应role) → 前端刷新所有数据 4. 每个API请求 → JWT claims决定当前账户上下文 ``` 对比Chatwoot:Chatwoot通过`Current.account`全局变量切换,GoChat通过JWT claims显式传递,避免全局状态。 --- ## 4. Redis Pub/Sub 实时通信架构 ### 4.1 对比 Chatwoot ActionCable | 特性 | Chatwoot (ActionCable) | GoChat (Redis Pub/Sub + WebSocket) | |---|---|---| | 连接管理 | ActionCable Server (Rails内置) | gorilla/websocket + Redis subscriber | | Channel订阅 | `ConversationChannel`, `AccountChannel` | Redis topic: `account:{id}`, `conversation:{id}` | | 消息推送 | `broadcast_to` | Redis PUBLISH → WebSocket send | | 连接认证 | `connect` 方法验证cookie | WebSocket握手时验证JWT | | 并发 | 每连接一个Redis subscriber | 每账户一个Redis subscriber(共享) | | 重连 | 客户端自动重连 | 客户端重连+服务器Redis重订阅 | ### 4.2 WebSocket 连接架构 ```go // realtime/hub.go type Hub struct { // account_id → set of websocket connections AccountConns map[uint]*AccountRoom // conversation_id → set of websocket connections ConvConns map[uint]*ConvRoom // Redis subscriber RedisSub *redis.PubSub mu sync.RWMutex } type AccountRoom struct { AccountID uint Conns map[uint]*WSConn // user_id → connection Sub *redis.PubSub // 订阅 account:{id} topic } type WSConn struct { UserID uint AccountID uint Conn *websocket.Conn Send chan []byte } ``` ### 4.3 Redis Topic 设计 ``` Topics: account:{account_id} — 账户级事件(新对话、通知、状态变化) conversation:{conv_id} — 对话级事件(新消息、状态流转、打字状态) inbox:{inbox_id} — Inbox级事件(新对话分配) user:{user_id} — 用户级事件(个人通知) captain:{assistant_id} — AI助手事件(企业版) copilot:{user_id}:{conv_id} — Copilot建议推送(企业版) ``` ### 4.4 事件消息格式 ```go // realtime/event.go type RealtimeEvent struct { Type string `json:"type"` // message_created, conversation_updated, etc. ActorID uint `json:"actor_id"` Resource string `json:"resource"` // conversation, message, notification, etc. Data json.RawMessage `json:"data"` // 资源JSON Timestamp time.Time `json:"timestamp"` } ``` ### 4.5 连接流程 ``` 1. 客户端 WebSocket握手 → ws://host/cable?token= → 验证JWT → 提取user_id+account_id → 注册到Hub.AccountConns[account_id] 2. 客户端订阅对话 → subscribe {conversation_id: N} → Hub注册到ConvConns[N] → Redis SUBSCRIBE conversation:N 3. 事件到达 → Redis消息 → Hub.Dispatch() → 根据topic路由到对应Room → Room广播到所有连接的WSConn → WSConn.Send channel → websocket.WriteJSON 4. 断连 → Hub注销 → Redis UNSUBSCRIBE ``` --- ## 5. 事件分发器设计 ### 5.1 对比 Chatwoot Dispatcher/Listener Chatwoot 采用 **Dispatcher → Listener** 模式: ``` Dispatcher.dispatch(event_name, timestamp, event_data) → 构造 Event对象 → Redis PUBLISH "chatwoot_events:{account_id}" → 各Listener订阅Redis channel → Listener#process(event) 执行业务逻辑 ``` **12个Listener:** - ActionCableListener → WebSocket推送 - AgentBotListener → 触发Bot响应 - AutomationRuleListener → 触发自动化规则 - CampaignListener → 触发营销活动 - CsatSurveyListener → 发送满意度调查 - HookListener → 触发自定义Webhook - InstallationWebhookListener → 触发平台Webhook - NotificationListener → 创建通知 - ParticipationListener → 更新参与状态 - ReportingEventListener → 记录报告事件 - WebhookListener → 发送Webhook回调 - BaseListener → 提供订阅基础设施 ### 5.2 GoChat Event Dispatcher 设计 ```go // event/dispatcher.go // 事件类型枚举 type EventType string const ( EventMessageCreated EventType = "message.created" EventMessageUpdated EventType = "message.updated" EventConversationCreated EventType = "conversation.created" EventConversationUpdated EventType = "conversation.updated" EventConversationAssigned EventType = "conversation.assigned" EventContactCreated EventType = "contact.created" EventContactUpdated EventType = "contact.updated" EventAgentAssigned EventType = "agent.assigned" EventCSATSubmitted EventType = "csat.submitted" // ... 更多 ) type Event struct { Type EventType `json:"type"` AccountID uint `json:"account_id"` Data json.RawMessage `json:"data"` Timestamp time.Time `json:"timestamp"` } type Dispatcher struct { Redis *redis.Client } func (d *Dispatcher) Dispatch(event Event) error { // 1. 发布到Redis(异步Listener消费) topic := fmt.Sprintf("events:account:%d", event.AccountID) payload, _ := json.Marshal(event) return d.Redis.Publish(ctx, topic, payload).Err() } ``` ### 5.3 Listener Handler 注册 ```go // event/listener.go type EventHandler func(event Event) error type ListenerHub struct { handlers map[EventType][]EventHandler } func RegisterHandler(eventType EventType, handler EventHandler) { globalHub.handlers[eventType] = append(globalHub.handlers[eventType], handler) } // GoChat的Listener注册(替代Chatwoot的独立Listener文件) func init() { RegisterHandler(EventMessageCreated, HandleRealtimePush) RegisterHandler(EventMessageCreated, HandleNotification) RegisterHandler(EventMessageCreated, HandleWebhook) RegisterHandler(EventMessageCreated, HandleAutomationRule) RegisterHandler(EventMessageCreated, HandleReporting) RegisterHandler(EventMessageCreated, HandleAgentBot) RegisterHandler(EventConversationCreated, HandleAutoAssignment) RegisterHandler(EventConversationCreated, HandleRealtimePush) // ... } ``` ### 5.4 Redis Consumer 启动 ```go // event/consumer.go func StartConsumer(accountID uint) { sub := redis.Subscribe(ctx, fmt.Sprintf("events:account:%d", accountID)) for msg := range sub.Channel() { event := parseEvent(msg.Payload) handlers := globalHub.handlers[event.Type] for _, h := range handlers { go h(event) // 异步执行,不阻塞 } } } ``` ### 5.5 对比总结 | 方面 | Chatwoot | GoChat | |---|---|---| | 事件发布 | Dispatcher → Redis PUBLISH | Dispatcher → Redis PUBLISH(相同) | | 事件消费 | 12个独立Listener类 | Handler函数注册表(更轻量) | | 执行方式 | Sidekiq异步Job | goroutine异步执行 | | 注册方式 | Rails autoload | init()函数静态注册 | | 水平扩展 | Sidekiq worker进程 | Consumer goroutine per account | --- ## 6. 企业版 SAML SSO 设计 ### 6.1 对比 Chatwoot Chatwoot企业版SAML实现: - `AccountSamlSettings` 模型存储IdP配置 - `DeviseSamlAuthenticatable` gem处理SAML流程 - `SamlUserBuilder` 构建User对象 - `Saml::UpdateAccountUsersProviderJob` 批量更新provider ### 6.2 GoChat SAML SP 实现 ```go // enterprise/auth/saml.go type AccountSamlSettings struct { ID uint `gorm:"primaryKey"` AccountID uint `gorm:"uniqueIndex;not null"` IdpEntityID string `gorm:"size:512"` IdpSsoTargetURL string `gorm:"size:512"` IdpSloTargetURL string `gorm:"size:512"` IdpCertificate string `gorm:"type:text"` SpEntityID string `gorm:"size:512"` SpAssertionURL string `gorm:"size:512"` // 自动生成 SpX509Certificate string `gorm:"type:text"` // 自签证书 SpPrivateKey string `gorm:"type:text"` // RSA私钥 RoleMappings json.RawMessage `gorm:"type:jsonb"` // SAML属性→角色映射 Active bool `gorm:"default:true"` } // SAML 流程: // 1. SP-initiated: GET /api/v1/auth/saml/{account_id}/login // → 生成SAML AuthnRequest → 重定向到IdP // 2. IdP回调: POST /api/v1/auth/saml/{account_id}/callback // → 解析SAML Response → 验证签名 // → SamlUserBuilder.Create/Find → 签发JWT // 3. SLO: GET /api/v1/auth/saml/{account_id}/logout // → 生成SAML LogoutRequest → 重定向到IdP SLO URL type SamlUserBuilder struct { Settings AccountSamlSettings } func (b *SamlUserBuilder) Build(samlResponse *SamlResponse) (*User, error) { email := samlResponse.GetAttribute("email") name := samlResponse.GetAttribute("name") role := b.mapRole(samlResponse.GetAttribute("role")) // 查找或创建User user, err := FindOrCreateUserByEmail(email, name, "saml") // 创建/更新AccountUser EnsureAccountUser(user.ID, b.Settings.AccountID, role) return user, err } ``` --- ## 7. 架构全景图 ``` ┌─────────────────────────────────────────────────────┐ │ GoChat 单体架构 │ │ │ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ Auth │ │ RBAC │ │ API │ │Event │ │ │ │ JWT │ │Polic │ │ Gin │ │Disp │ │ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────┐ │ │ │ Service Layer │ │ │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │ │ │Conv│ │Msg │ │Cntc│ │Auto│ │ │ │ │ └────┘ └────┘ └────┘ └────┘ │ │ │ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │ │ │ │Rprt│ │Ntfy│ │Team│ │Capn│ │ │ │ │ └────┘ └────┘ └────┘ └────┘ │ │ │ └─────────────────────────────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────┐ ┌──────────┐ ┌───────────┐ │ │ │ GORM/ │ │ Redis │ │ Channel │ │ │ │ PgSQL │ │ Pub/Sub │ │ Registry │ │ │ └─────────┘ └──────────┘ └───────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────┐ │ │ │ WebSocket Hub + Event Listener │ │ │ └─────────────────────────────────────┘ │ │ │ │ 企业版模块: │ │ ┌──────────┐ ┌────────┐ ┌──────┐ ┌──────┐ │ │ │ SAML SSO │ │CustomR │ │ SLA │ │Call │ │ │ └──────────┘ └────────┘ └──────┘ └──────┘ │ │ ┌──────────┐ ┌────────┐ ┌──────┐ │ │ │ Captain │ │Copilot │ │Audit │ │ │ └──────────┘ └────────┘ └──────┘ │ └─────────────────────────────────────────────────────┘ ``` --- ## 8. 关键设计决策总结 | # | 决策 | 原因 | 对比Chatwoot | |---|---|---|---| | 1 | JWT替代DeviseTokenAuth | Go无对应gem,JWT更标准 | 多token→单token+refresh | | 2 | PolicyContext替代Pundit | 统一权限检查入口 | 12个Policy类→1个PolicyContext | | 3 | Handler注册表替代Listener类 | Go无Rails autoload,函数注册更轻量 | 12个Listener→Handler map | | 4 | JWT claims传递账户上下文 | 避免全局状态 | Current.account全局→JWT claims | | 5 | Redis Pub/Sub+WebSocket | Go生态成熟选择 | ActionCable→自定义WS Hub | | 6 | goroutine异步替代Sidekiq | 单体架构内异步足够 | Sidekiq进程→goroutine | | 7 | 共享Redis subscriber | 减少Redis连接数 | 每连接1subscriber→每账户1 | | 8 | 企业版SAML自实现 | Go无成熟SAML SP库 | DeviseSamlAuthenticatable→自实现 | --- > **下一步:** P2B(数据库设计)和 P2C(路由设计)完成后,P2架构设计阶段完整收官。