清理: - 删除 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 相关修改
21 KiB
21 KiB
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 结构
// 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对比的关键简化
- 去除Pundit Policy类 — 每个资源一个Policy文件(如AccountPolicy, ConversationPolicy),GoChat统一为一个PolicyContext+权限矩阵
- Scope过滤合并 — Chatwoot的Policy::Scope(返回过滤后的数据集),GoChat用PolicyContext.Scope()返回GORM WHERE条件
- 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决定当前账户上下文
对比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 连接架构
// 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配置DeviseSamlAuthenticatablegem处理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无对应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架构设计阶段完整收官。