- Add OpenTelemetry SDK, OTLP exporter, Prometheus integration - Implement connection tracking with active/total/disconnection metrics - Add health endpoint with uptime and connection counts - Integrate tracing spans for socket events and engine messages - Add metrics collection for event handling duration - Update health endpoint to include live runtime state - Add graceful telemetry shutdown in main function - Implement engine session active metrics tracking - Add namespace-specific attributes to connection metrics - Introduce message edit history retrieval endpoint - Add scheduled message CRUD operations and dispatcher - Update Socket.IO event registration with observability - Refactor component update to remove dead code allowance - Add comprehensive environment variables documentation - Implement detailed development guidelines in AGENTS.md
26 KiB
Auth 认证方案
架构总览
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Client │ │ appks │ │ imks │
│ (浏览器/ │ │ (core) │ │ (IM服务) │
│ APP) │ │ │ │ │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
│ 1. POST /api/v1/auth/login │
│───────────────────▶│ │
│ 2. {access_token, refresh_token} │
│◀───────────────────│ │
│ │ │
│ 3. WS/gRPC/HTTP 携带 JWT │
│──────────────────────────────────────▶│
│ │ │
│ │ 4a. VerifyToken RPC (RPC模式)
│ │◀─────────────────│
│ │ 4b. GetSigningKeys (本地模式)
│ │◀─────────────────│
│ │ │
│ │ 5. TokenClaims / SigningKeys
│ │─────────────────▶│
│ │ │
│ 6. 业务响应 │ │
│◀─────────────────────────────────────│
角色分工:
| 服务 | 职责 |
|---|---|
| appks (core) | 颁发 JWT、刷新 JWT、撤销 JWT、管理签名密钥、提供 TokenService gRPC |
| imks (IM) | 接收客户端 JWT,通过 RPC 或本地密钥验证用户身份 |
Proto 契约
定义在 proto/core/auth.proto,package appks.core.v1。
appks 和 imks 各自维护一份相同的 proto 文件:
- appks 编译为 server stub(提供服务)
- imks 编译为 client stub(调用服务)
TokenService RPC
service TokenService {
// 令牌生命周期 (appks 内部调用)
rpc IssueToken(IssueTokenRequest) returns (IssueTokenResponse);
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
rpc RevokeToken(RevokeTokenRequest) returns (RevokeTokenResponse);
// imks 验证 (RPC 模式)
rpc VerifyToken(VerifyTokenRequest) returns (VerifyTokenResponse);
// imks 密钥拉取 (本地验证模式)
rpc GetSigningKeys(GetSigningKeysRequest) returns (GetSigningKeysResponse);
}
JWT 令牌
结构
JWT Header:
{
"alg": "HS256",
"typ": "JWT",
"kid": "01909a..." // 签名密钥 ID,用于匹配 SigningKey
}
JWT Payload (TokenClaims):
{
"sub": "user-uuid",
"iss": "appks",
"iat": 1718000000,
"exp": 1718003600,
"jti": "01909b...",
"scope": "im:read im:write",
"extra": {
"workspace_id": "..."
}
}
令牌类型
| 类型 | 格式 | 存储 | 用途 |
|---|---|---|---|
| access_token | JWT (HS256) | 无状态,客户端持有 | 每次请求携带,验证用户身份 |
| refresh_token | rt_{UUIDv7} 不透明字符串 |
Redis core:token:refresh:{token} → user_id |
换取新的 access_token + refresh_token(旋转) |
双模式验证
imks 可选择以下任一模式验证客户端 JWT:
模式 A:RPC 验证(VerifyToken)
imks → appks TokenService.VerifyToken(jwt) → {valid, claims}
- 优点:实时权威,能感知撤销
- 缺点:每次请求增加一次 RPC 往返
- 适用场景:高安全要求操作(管理员操作、敏感数据)
模式 B:本地验证(GetSigningKeys)
imks 启动时 → appks TokenService.GetSigningKeys() → 缓存密钥到本地
后续请求 → imks 用本地密钥解码 JWT(HS256 验签)
定期刷新 → 根据 next_rotation_at 拉取新密钥
- 优点:零 RPC 延迟,appks 不可用时仍能验证
- 缺点:撤销有最多一个密钥窗口(3h)的延迟
- 适用场景:高频低延迟操作(消息收发、实时通信)
推荐策略
混合使用:
- 普通操作(发消息、读频道)→ 本地验证
- 敏感操作(踢人、删频道、改权限)→ RPC 验证
签名密钥管理
密钥窗口
时间轴:
─────────┬──────────┬──────────┬────────
│ key A │ key B │ key C
│ (过期) │ (活跃) │ (未来)
└──────────┴──────────┴────────
issued_at issued_at issued_at
+3h +3h +3h
- 每个签名密钥有效期 3 小时
- 同一时刻可能有 2 个有效密钥(滚动窗口,平滑过渡)
- JWT header 的
kid字段标识使用哪个密钥签名
密钥轮换流程
1. 当前密钥到达 3h → TokenService.rotate_if_needed()
2. Redis 分布式锁 (core:token:rotation_lock, 10s TTL) 防止多实例竞争
3. 旧密钥标记 active=false,仍保留在 Redis 用于验证旧 token
4. 生成新密钥,active=true
5. ArcSwap 原子替换当前签名密钥
6. 旧密钥 TTL = 6h (2× window) 后从 Redis 自动清除
密钥存储(Redis)
core:token:active_key → kid (当前活跃密钥 ID)
core:token:key:{kid} → SigningKey JSON (TTL = 6h)
core:token:rotation_lock → "1" (TTL = 10s, 分布式锁)
SigningKey 结构
pub struct SigningKeyInfo {
pub kid: String, // UUIDv7
pub algorithm: String, // "HS256"
pub key_material: String, // base64(32 bytes random)
pub issued_at: i64,
pub expires_at: i64, // issued_at + 3h
pub active: bool,
}
撤销机制
Redis 布局
core:token:revoked:{jti} → "1" (TTL = token 剩余有效期)
core:token:refresh:{token} → user_id (TTL = 7d)
撤销方式
| 操作 | RPC | 效果 |
|---|---|---|
| 撤销单个 token | RevokeToken(jti) |
将 jti 加入撤销列表 |
| 撤销用户所有 token | RevokeToken(user_id) |
删除该用户所有 refresh token |
撤销感知延迟
| 验证模式 | 延迟 |
|---|---|
RPC (VerifyToken) |
实时 — 每次检查撤销列表 |
本地 (GetSigningKeys) |
最多 3h — 密钥过期前无法感知 jti 撤销 |
appks 实现
模块结构
service/internal_auth.rs → TokenService (业务逻辑)
grpc/auth.rs → TokenGrpcService (gRPC handler)
grpc/mod.rs → TokenServiceServer 注册到 tonic server
api/internal/issue_api_key.rs → REST: POST /api/v1/internal/tokens
TokenService 核心
pub struct TokenService {
redis: AppRedis,
current_key: Arc<ArcSwap<SigningKeyInfo>>, // 无锁读
}
- 启动时从 Redis 加载活跃密钥,无则生成
- 签名使用
jsonwebtokencrate (HS256) - 密钥轮换使用 Redis 分布式锁,支持多实例部署
ArcSwap保证签名密钥读取无锁、写入原子
imks 实现指南
启动流程
// 1. 连接 appks TokenService
let mut token_client = TokenServiceClient::connect(appks_addr).await?;
// 2. 拉取签名密钥
let resp = token_client.get_signing_keys(GetSigningKeysRequest { kid: "" }).await?;
let keys = resp.keys;
let next_rotation = resp.next_rotation_at;
// 3. 缓存密钥到本地 (HashMap<kid, SigningKey>)
key_store.insert_all(keys);
// 4. 安排定时刷新
tokio::spawn(async move {
loop {
let delay = next_rotation - now();
tokio::time::sleep(Duration::from_secs(delay as u64)).await;
let resp = token_client.get_signing_keys(...).await;
key_store.update(resp.keys);
}
});
连接时验证
// 客户端建立 WebSocket/gRPC 连接时携带 JWT
fn on_connect(headers: &Headers) -> Result<TokenClaims, AuthError> {
let token = headers.get("Authorization")
.and_then(|v| v.strip_prefix("Bearer "))
.ok_or(AuthError::MissingToken)?;
// 本地验证 (快速路径)
let header = decode_header(token)?;
let kid = header.kid.ok_or(AuthError::MissingKid)?;
let key = key_store.get(&kid).ok_or(AuthError::UnknownKey)?;
let mut validation = Validation::new(Algorithm::HS256);
validation.set_issuer(&["appks"]);
validation.validate_exp = true;
let data = decode::<TokenClaims>(token, &key, &validation)?;
Ok(data.claims)
}
敏感操作验证
// 敏感操作走 RPC 验证 (权威路径)
async fn on_sensitive_action(token: &str) -> Result<TokenClaims, AuthError> {
let resp = token_client.verify_token(VerifyTokenRequest {
token: token.to_string(),
}).await?;
if resp.valid {
Ok(resp.claims.unwrap())
} else {
Err(AuthError::from(resp.reason))
}
}
安全考虑
- 密钥传输:appks → imks 的 gRPC 连接应使用 mTLS,防止密钥在传输中被截获
- 密钥生命周期:3h 窗口平衡了安全性和可用性;缩短窗口可减少撤销延迟但增加轮换频率
- HS256 vs 非对称:当前使用 HS256(对称密钥),imks 拿到的密钥可以伪造 token。如果 imks 不可完全信任,应改用 RS256/EdDSA,imks 只持有公钥
- Refresh Token 安全:每次刷新都旋转(旧 token 立即失效),防止重放
- 撤销列表 TTL:与 token 剩余有效期对齐,过期 token 无需保留撤销记录
IM 服务 Proto 说明书
以下是 imks 侧 proto/core/ 下各 gRPC 服务的完整说明。所有 IM 服务定义在 appks.im.v1 包下,由 appks 提供 server 端,imks 消费 client 端。
服务总览
| Proto 文件 | 服务 | RPC 数量 | 职责 |
|---|---|---|---|
auth.proto |
TokenService | 5 | JWT 令牌生命周期 + 验证 + 密钥分发 |
channel.proto |
ChannelService | 10 | 频道/分类 CRUD + 统计 |
member.proto |
MemberService | 7 | 成员邀请/踢出/加入/离开/查询 |
permission.proto |
PermissionService | 7 | 权限检查 + 覆盖规则 + 频道解析 |
channel_settings.proto |
ChannelRoleService | 4 | 频道自定义角色 |
| ChannelInvitationService | 4 | 邀请生命周期 | |
| ChannelWebhookService | 4 | Webhook CRUD | |
| ChannelSlashCommandService | 4 | 斜杠命令注册 | |
| ChannelRepoLinkService | 3 | 频道 ↔ 代码仓库关联 | |
| ImIntegrationService | 4 | 外部平台集成(Slack/Discord 等) | |
| CustomEmojiService | 3 | 工作区自定义表情 | |
| ForumTagService | 4 | 论坛频道标签 | |
| VoiceService | 2 | 语音频道参与者状态 | |
| StageService | 4 | 舞台频道管理 | |
| ChannelAuditService | 1 | 频道审计日志查询 |
ChannelService(channel.proto)
频道和分类的 CRUD 管理,以及频道统计。
枚举
ChannelType — 频道类型:
| 值 | 含义 |
|---|---|
PUBLIC |
公开频道,workspace 内所有人可见 |
PRIVATE |
私有频道,仅被邀请成员可见 |
DIRECT |
私聊(一对一) |
GROUP |
群聊(多人私聊) |
REPO |
仓库关联频道(自动与 git repo 绑定) |
SYSTEM |
系统频道(公告、通知等,只读) |
ChannelKind — 频道形态:
| 值 | 含义 |
|---|---|
TEXT |
文本频道 |
VOICE |
语音频道 |
STAGE |
舞台频道(主持人+观众模式) |
FORUM |
论坛频道(帖子/主题式讨论) |
ANNOUNCEMENT |
公告频道(仅管理员可发消息) |
Visibility — 可见性级别(从低到高):
| 值 | 含义 |
|---|---|
PUBLIC |
所有人可见(含未登录用户) |
WORKSPACE |
workspace 成员可见 |
INTERNAL |
内部可见(组织成员) |
PRIVATE |
仅频道成员可见 |
PROTECTED |
受保护(不可被搜索/索引) |
HIDDEN |
隐藏(不显示在频道列表中) |
SECRET |
机密(仅通过直链访问) |
RPC 列表
GetChannel(channel_id) → Channel 获取频道详情
ListChannels(workspace, ...) → [Channel], total 列出频道(支持分类/类型/形态过滤)
CreateChannel(workspace, name) → Channel 创建频道
UpdateChannel(channel_id, ...) → Channel 更新频道属性
DeleteChannel(channel_id) → {} 删除频道
GetChannelStats(channel_id) → ChannelStats 获取频道统计(成员/消息/线程/反应数)
ListCategories(workspace) → [ChannelCategory] 列出分类
CreateCategory(workspace, name) → ChannelCategory 创建分类
UpdateCategory(category_id, ...) → ChannelCategory 更新分类
DeleteCategory(category_id) → {} 删除分类
核心消息
Channel — 频道主体:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
UUID | 频道 ID |
workspace_id |
UUID | 所属 workspace |
category_id |
UUID? | 所属分类(可选) |
parent_channel_id |
UUID? | 父频道(用于子频道/线程) |
name |
string | 频道名称 |
topic / description |
string? | 主题 / 描述 |
channel_type |
ChannelType | 频道类型 |
channel_kind |
ChannelKind | 频道形态 |
visibility |
Visibility | 可见性 |
position |
int32 | 排序位置 |
nsfw |
bool | NSFW 标记 |
read_only |
bool | 只读(仅管理员可发消息) |
archived |
bool | 已归档 |
rate_limit_per_user |
int32? | 慢速模式(秒/消息) |
last_message_id / last_message_at |
— | 最后一条消息信息 |
ChannelStats — 频道统计:
| 字段 | 说明 |
|---|---|
members_count |
成员数 |
messages_count |
消息数 |
threads_count |
线程数 |
reactions_count |
反应数 |
mentions_count |
@提及数 |
files_count |
文件数 |
last_activity_at |
最后活跃时间 |
MemberService(member.proto)
频道成员管理。
枚举
Role — 角色层级(从高到低):
| 值 | 含义 |
|---|---|
OWNER |
频道所有者 |
ADMIN |
管理员(全部权限) |
MAINTAINER |
维护者(管理频道设置、成员) |
MODERATOR |
版主(管理消息、踢人) |
MEMBER |
普通成员 |
CONTRIBUTOR |
贡献者(可发消息,部分限制) |
VIEWER |
观察者(只读) |
GUEST |
访客(临时访问) |
BOT |
机器人 |
MemberStatus — 成员状态:
| 值 | 含义 |
|---|---|
ACTIVE |
活跃成员 |
INVITED |
已邀请(尚未加入) |
LEFT |
已离开 |
KICKED |
被踢出 |
BANNED |
被封禁 |
RPC 列表
ListMembers(channel_id, ...) → [ChannelMember], total 列出成员(支持状态过滤)
InviteMember(channel_id, user_id) → ChannelMember 邀请用户加入频道
UpdateMember(channel_id, user_id) → ChannelMember 更新成员(角色/禁言/置顶)
KickMember(channel_id, user_id) → {} 踢出成员
JoinChannel(channel_id, user_id) → ChannelMember 用户主动加入
LeaveChannel(channel_id, user_id) → {} 用户主动离开
IsMember(channel_id, user_id) → is_member, role 检查是否为成员
核心消息
ChannelMember — 频道成员:
| 字段 | 说明 |
|---|---|
channel_id / user_id |
频道 + 用户 |
role |
角色(Role 枚举值字符串) |
status |
状态(MemberStatus 枚举值字符串) |
muted |
是否被禁言 |
pinned |
是否被置顶(频道侧标记) |
last_read_message_id / last_read_at |
已读进度 |
joined_at / left_at |
加入/离开时间 |
PermissionService(permission.proto)
频道级权限系统,独立于 workspace/repo 的通用权限。
权限枚举(ImPermission)
| 权限 | 说明 |
|---|---|
READ_CHANNEL |
查看频道 |
SEND_MESSAGE |
发送消息 |
MANAGE_THREADS |
管理线程 |
MANAGE_REACTIONS |
管理反应 |
MANAGE_PINS |
管理置顶消息 |
INVITE_MEMBERS |
邀请成员 |
KICK_MEMBERS |
踢出成员 |
MANAGE_CHANNEL |
管理频道设置 |
MANAGE_ROLES |
管理角色 |
MANAGE_WEBHOOKS |
管理 Webhook |
MANAGE_EMOJIS |
管理自定义表情 |
VIEW_AUDIT_LOG |
查看审计日志 |
MANAGE_INTEGRATIONS |
管理外部集成 |
SEND_TTS |
发送 TTS 消息 |
USE_SLASH_COMMANDS |
使用斜杠命令 |
ATTACH_FILES |
上传文件 |
MENTION_EVERYONE |
@所有人 |
MANAGE_MESSAGES |
管理消息(删除他人消息) |
ADMIN |
管理员(拥有所有权限) |
RPC 列表
CheckPermission(channel, user, perm) → allowed, role 检查单项权限
GetPermissions(channel, user) → [ImPermission] 获取用户全部权限
SetPermissionOverwrite(channel, target) → Overwrite 设置权限覆盖
GetPermissionOverwrites(channel) → [Overwrite] 获取覆盖列表
DeletePermissionOverwrite(channel, target) → {} 删除覆盖
ResolveChannel(channel_id) → 频道摘要信息 解析频道元数据
EnsureReadable(channel, user) → allowed 确保用户可读(快速检查)
权限覆盖(PermissionOverwrite)
权限覆盖允许对特定用户/角色在特定频道上覆盖默认权限:
| 字段 | 说明 |
|---|---|
target_type |
"user" 或 "role" |
target_id |
用户 ID 或角色 ID |
allow |
显式允许的权限列表 |
deny |
显式拒绝的权限列表 |
权限解析优先级:deny 覆盖 > allow 覆盖 > 角色权限
ChannelSettings 服务组(channel_settings.proto)
所有频道配置相关的服务定义在同一个 proto 文件中。
ChannelRoleService — 频道自定义角色
频道级别的自定义角色(不同于 member.proto 中的全局 Role 枚举)。
ListChannelRoles(channel_id) → [ChannelRole]
CreateChannelRole(channel, name) → ChannelRole
UpdateChannelRole(role_id, ...) → ChannelRole
DeleteChannelRole(role_id) → {}
ChannelRole 字段:name, permissions[](ImPermission 字符串列表), assignable(是否可被普通成员分配)
ChannelInvitationService — 邀请管理
ListInvitations(channel_id) → [ChannelInvitation]
CreateInvitation(channel, user) → ChannelInvitation
AcceptInvitation(invitation_id) → ChannelInvitation
RevokeInvitation(invitation_id) → {}
ChannelInvitation 字段:invited_by, invited_user_id, role(预设角色), status
ChannelWebhookService — Webhook 管理
ListWebhooks(channel_id) → [ChannelWebhook]
CreateWebhook(channel, name, url) → ChannelWebhook
UpdateWebhook(webhook_id, ...) → ChannelWebhook
DeleteWebhook(webhook_id) → {}
ChannelWebhook 字段:name, url, secret(签名验证用), events[](订阅事件列表), active
ChannelSlashCommandService — 斜杠命令注册
ListSlashCommands(channel_id) → [ChannelSlashCommand]
CreateSlashCommand(channel, cmd, url) → ChannelSlashCommand
UpdateSlashCommand(command_id, ...) → ChannelSlashCommand
DeleteSlashCommand(command_id) → {}
ChannelSlashCommand 字段:command(命令名如 /deploy), description, request_url(回调地址), scopes[]
ChannelRepoLinkService — 仓库关联
将频道与代码仓库关联,自动推送仓库事件到频道。
ListRepoLinks(channel_id) → [ChannelRepoLink]
CreateRepoLink(channel, repo, type) → ChannelRepoLink
DeleteRepoLink(link_id) → {}
ChannelRepoLink 字段:repo_id, link_type, events[](订阅的仓库事件:push、pr、issue 等)
ImIntegrationService — 外部平台集成
与 Slack、Discord 等外部平台的消息同步。
ListIntegrations(channel_id) → [ImIntegration]
CreateIntegration(channel, provider, ...)→ ImIntegration
UpdateIntegration(integration_id, ...) → ImIntegration
DeleteIntegration(integration_id) → {}
ImIntegration 字段:provider(平台名), external_channel_id(外部频道 ID), sync_direction(inbound/outbound/bidirectional), active
CustomEmojiService — 自定义表情
工作区级别的自定义表情管理。
ListCustomEmojis(workspace_id) → [CustomEmoji]
CreateCustomEmoji(workspace, name, url) → CustomEmoji
DeleteCustomEmoji(emoji_id) → {}
CustomEmoji 字段:workspace_id, name(表情名如 :appks:), image_url
ForumTagService — 论坛标签
论坛频道(ChannelKind::FORUM)的帖子分类标签。
ListForumTags(channel_id) → [ForumTag]
CreateForumTag(channel, name, ...) → ForumTag
UpdateForumTag(tag_id, ...) → ForumTag
DeleteForumTag(tag_id) → {}
ForumTag 字段:name, moderated(是否需要管理员审核), position
VoiceService — 语音频道
语音频道的参与者状态管理。
ListVoiceParticipants(channel_id) → [VoiceParticipant]
UpdateVoiceState(channel, user, ...) → VoiceParticipant
VoiceParticipant 字段:user_id, muted(静音), deafened(屏蔽音频), joined_at
StageService — 舞台频道
舞台频道(ChannelKind::STAGE)的管理。主持人说话,观众收听。
GetStage(channel_id) → Stage
CreateStage(channel, topic, ...) → Stage
UpdateStage(stage_id, ...) → Stage
DeleteStage(stage_id) → {}
Stage 字段:topic(当前话题), privacy_level, discoverable(是否可被发现), started_at / ended_at
ChannelAuditService — 审计日志
频道操作审计日志查询(只读)。
ListChannelEvents(channel_id, ...) → [ChannelAuditEvent], total
ChannelAuditEvent 字段:actor_id(操作者), event_type(事件类型字符串), target_type / target_id(操作对象), old_value / new_value(变更前后值)
imks 与 appks 的调用关系
┌────────────────────────────────────────────────────────────┐
│ imks │
│ │
│ Socket.IO / WebSocket / WebTransport │
│ │ │
│ ▼ │
│ 连接握手 ──→ TokenService.VerifyToken() 或 本地密钥验证 │
│ │ │
│ ▼ │
│ 消息收发 ──→ ChannelService + MemberService │
│ │ PermissionService.EnsureReadable() │
│ │ │
│ ▼ │
│ 频道管理 ──→ ChannelService CRUD │
│ │ ChannelRoleService │
│ │ ChannelInvitationService │
│ │ │
│ ▼ │
│ 语音/舞台 ──→ VoiceService + StageService │
│ │
│ 集成/扩展 ──→ WebhookService + SlashCommandService │
│ RepoLinkService + ImIntegrationService │
│ │
│ 审计查询 ──→ ChannelAuditService │
└────────────────────────┬───────────────────────────────────┘
│ gRPC
▼
┌────────────────────────────────────────────────────────────┐
│ appks │
│ TokenService server │ Channel/Member/Permission server │
│ Redis (JWT keys) │ Postgres (channel data) │
└────────────────────────────────────────────────────────────┘
imks 本地缓存建议
| 数据 | 缓存策略 | 刷新时机 |
|---|---|---|
签名密钥 (SigningKey[]) |
内存 HashMap | next_rotation_at 到达时拉取 |
频道信息 (Channel) |
LRU + TTL | 频道更新事件 (NATS) |
成员列表 (ChannelMember[]) |
LRU + TTL | 成员变更事件 (NATS) |
| 权限缓存 | 短期 TTL (30s) | 权限变更事件 (NATS) |
| 自定义表情 | 全量加载 + 事件增量 | emoji 增删事件 (NATS) |