Files
gochat/docs/product/07-design-auth-and-realtime.md
T
rogee 0dabb8cfa5 docs: 整理文档目录结构 — 清理过时文档、归集功能子目录、统一命名规范
清理:
- 删除 34 份过时文档(gap reports/QA临时报告/验收报告/阶段性文档)
- 删除 docs/.hermes/skills 第三方 skills 副本(16 文件)
- 删除 skills-lock.json

目录归集:
- 根目录仅保留 README.md 索引
- product/ — 产品与架构设计(PRD + ARCHITECTURE + P2设计文档 + AI/企业路线图)
- tracking/ — Chatwoot parity 开发跟踪
- requirements/ — M01-M12 模块需求
- plans/ — 历史实现计划
- parity/ — 路由 parity 与前端契约
- qa/ — QA 报告与测试计划
- ops/ — 运维部署

命名规范:
- 全小写 kebab-case,禁止全大写文件名
- product/tracking/ops 用 NN- 序号前缀
- requirements 用 MNN- 两位零填充模块号
- plans/qa 用 YYYY-MM-DD- 日期前缀
- requirements M1-M9 零填充为 M01-M09(修复字典序)

同步更新:
- backend/cmd/route_parity/main.go 路径默认值
- backend/scripts/parity_frontend_smoke.sh 报告路径
- 所有 docs 内部交叉引用
- .gitignore 排除编译产物 (backend/gochat, backend/route_parity)
- 新增迁移 000052/000053
- 前端 WS 相关修改
2026-07-09 14:53:27 +08:00

21 KiB
Raw Blame History

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 headersaccess-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 结构

// 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 认证中间件

// 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 角色体系

// 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 权限检查实现

// 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 模型设计

// 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决定当前账户上下文

对比ChatwootChatwoot通过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 连接架构

// 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 事件消息格式

// 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>
   → 验证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 设计

// 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 注册

// 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 启动

// 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 实现

// 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无对应gemJWT更标准 多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架构设计阶段完整收官。