Add implementation progress reports, review reports (CTO/quality/deploy), session log, signoff table, team roster, collaboration charter, tech-stack evaluation and inventory, research reports. Remove obsolete multisync_server.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
29 KiB
JumpServer & EasyNode 深度架构分析 — Nexus 技术参考报告
分析日期:2026-05-21 | 分析师:软件架构师 | 状态:ADRs 提议中
1. 执行摘要
分析范围:jumpserver/jumpserver、jumpserver/koko、jumpserver/luna、jumpserver/lion、chaos-zhu/easynode
对 Nexus 的核心参考价值排序:
| 排名 | 项目 | 技术栈 | Stars | 最大参考价值 | 直接可复用度 |
|---|---|---|---|---|---|
| 1 | JumpServer Core | Python + Django | 30.5k | 资产 Platform 模型、RBAC 授权链路、会话审计存储方案 | 设计思路完全可复用,代码需 Django→FastAPI 迁移 |
| 2 | Koko | Go + Vue | 531 | SSH 连接池、WebSocket-TTY 代理、asciicast 录像、Redis 房间模式 | Go→Python 重写,架构模式可直接借鉴 |
| 3 | EasyNode | Vue + Node.js | 2k | Web SSH 数据流、批量指令并行队列、凭据加密存储、脚本库设计 | 架构简洁适合小规模参考,Node.js→Python 重写 |
| 4 | Luna | Angular + TypeScript | 303 | xterm.js 集成方式、多标签终端管理、文件管理器 UI | 直接复用前端方案 |
| 5 | Lion | Go + Vue | 22 | RDP/VNC Guacamole 连接器 | 仅当 Nexus 需要图形协议时参考 |
2. 资产模型对比分析
2.1 JumpServer 资产模型架构
┌──────────────────┐
│ Platform │ (模板层)
│ - name (唯一) │
│ - category │ host/device/database/cloud/web/gpt/custom
│ - type │ linux/windows/mysql/k8s...
│ - charset │
│ - protocols ────┼──▶ PlatformProtocol (1:N)
│ - automation ───┼──▶ PlatformAutomation (1:1)
└────────┬─────────┘
│ FK (PROTECT)
▼
┌──────────────────────────────────────┐
│ Asset │ (实例层 - 多表继承)
│ - name, address │
│ - platform (FK) │
│ - nodes (M2M → Node) │
│ - zone (FK → Zone, 可选) │
│ - connectivity │
│ - gathered_info (JSON) │
│ ┌──────┬──────┬──────┬──────┐ │
│ │ Host │Device│Database│Cloud│ ... │ (子类特化)
│ └──────┴──────┴──────┴──────┘ │
└──────────────────────────────────────┘
│ M2M
▼
┌──────────────────────────────────────┐
│ Node │ (组织层 - 键位树)
│ - key: "1:2:3" (冒号分隔层级) │
│ - value: "WebServers" │
│ - full_value: "/Default/Prod/Web" │
│ - child_mark (子节点计数器) │
│ - assets_amount (缓存) │
└──────────────────────────────────────┘
2.2 核心设计决策解读
Platform = 资产模板/类型定义
- 不在 Asset 表直接存储
category和type,而是通过platform FK派生 - 查询时用
F()表达式优化:queryset.annotate(category=F("platform__category")) - 优点:修改资产分类只需换平台引用,无需改 Asset 记录
Node = 键位树(Key-Based Tree)
- 不存
parent_id自引用,而是用key字符串编码层级:"1:2:3" - 查询所有后代:
Q(key__istartswith=f'{self.key}:')—— 一次查询解决 - 查询祖先:
key.split(':')算出来,key__in=[...]一次查询 - 优点:避免了递归 CTE 和嵌套集(nested set)的复杂度
多表继承
Asset是基表,Host,Device,Database,Cloud等是子表- 每个子类存储类别独有的字段(Host 有 os/arch,Database 有 db_name/db_type)
2.3 Nexus 适配建议
| JumpServer 设计 | Nexus 适配 | 改造点 |
|---|---|---|
| Platform 模板 | 直接采纳 — 创建 platforms 表作为资产类型模板 |
Django FK → SQLAlchemy relationship + ForeignKey |
| Node 键位树 | 采纳但简化 — 2000+ 服务器规模用 parent_id 自引用更直观,配合 PostgreSQL ltree 扩展 |
键位树在 SQLAlchemy 中无原生优势 |
| Asset 多表继承 | 改为单表+JSON — 用 extra_attrs JSONB 替代多表继承 |
Django 多表继承 → SQLAlchemy 单表 + JSONB |
| Protocol 实例级覆盖 | 采纳 — 允许每个 Asset 覆盖默认端口 | 直接复用设计 |
| Node 缓存映射 | 采纳但用 Redis — {node_id: [asset_ids]} 的 Redis Hash 缓存 |
Django 三级缓存 → 直接用 Redis |
2.4 推荐的 Nexus Asset 模型
# SQLAlchemy Async 模型
class Platform(Base):
__tablename__ = "platforms"
id: int (PK)
name: str (unique) # "Linux", "Windows", "MySQL"
category: str # "host", "database", "device"
type: str # "linux", "mysql", "switch"
default_protocols: JSONB # [{"name":"ssh","port":22,"primary":true}]
class Asset(Base):
__tablename__ = "assets"
id: UUID (PK)
name: str
address: str # IP 或域名
platform_id: FK → Platform
node_id: FK → Node (可选)
tags: M2M → Tag
protocols: JSONB # 实例级覆盖: [{"name":"ssh","port":2222}]
extra_attrs: JSONB # 替代多表继承: {"os":"ubuntu","arch":"x86_64",...}
connectivity: str # "ok","err","unknown"
last_checked_at: datetime
class Node(Base):
__tablename__ = "nodes"
id: UUID (PK)
name: str
parent_id: FK → Node (nullable) # 自引用树
# 或直接用 PostgreSQL ltree:
# path: ltree # "root.production.web"
3. 权限体系对比分析
3.1 JumpServer 四层权限模型
Layer 1: 认证 (Authentication)
└── 密码/MFA/LDAP/CAS/OIDC/SAML2/OAuth2/RADIUS
Layer 2: RBAC (角色权限 — 控制管理界面操作)
└── Role → RoleBinding → User
├── SystemAdmin / SystemAuditor / SystemUser (全局)
└── OrgAdmin / OrgAuditor / OrgUser (组织级)
Layer 3: Asset Permission (资产授权 — 谁能访问什么)
└── AssetPermission {
users: M2M[User] ← 授权给谁
user_groups: M2M[Group]
assets: M2M[Asset] ← 授权哪些资产
nodes: M2M[Node] ← 授权哪些节点(子节点自动继承)
accounts: JSON[account_ids / "@ALL" / "@USER" / "@INPUT"]
protocols: JSON ← 允许的协议
actions: int ← connect/upload/download/clipboard
date_start / date_expired ← 时间窗口
is_active: bool ← 启用/停用
}
Layer 4: ACL (访问控制列表 — 精细化控制)
├── LoginAssetACL → accept/reject/review/notice/face_verify
├── CommandFilterACL → allow/deny/review (正则匹配危险命令)
├── ConnectMethodACL → 控制 WebCLI/SSH/RDP 等连接方式
└── DataMaskingRule → 数据库查询脱敏
3.2 资产授权链核心查询(JumpServer 实现)
# AssetPermission.get_all_assets() — 计算用户可访问的完整资产集
def get_all_assets(self):
# Step 1: 直接授权的资产
asset_ids = set(self.assets.all().values_list('id', flat=True))
# Step 2: 通过节点授权的资产(含后代节点)
nodes_keys = self.nodes.all().values_list('key', flat=True)
nodes_asset_ids = Node.get_nodes_all_asset_ids_by_keys(nodes_keys)
asset_ids.update(nodes_asset_ids)
return Asset.objects.filter(id__in=asset_ids)
3.3 Nexus 适配建议
| JumpServer 设计 | Nexus 适配 | 说明 |
|---|---|---|
| 四层权限模型 | 简化为三层:认证 → RBAC → 资产授权 | Nexus 初期不需要 ACL 层的全部复杂度 |
| AssetPermission 模型 | 直接采纳核心字段 | users/groups/assets/nodes/accounts/date_range/is_active |
| 命令过滤 | 后期实现,初期提供命令日志审计 | 使用正则匹配,会话中实时拦截 |
| 账号别名(@ALL/@USER/@INPUT) | 采纳 | JumpServer 的精妙设计 |
| 时间窗口授权 | 采纳 | date_start/date_expired,简单有效 |
3.4 推荐的 Nexus 权限模型
class AssetPermission(Base):
__tablename__ = "asset_permissions"
id: UUID (PK)
name: str
users: M2M[User]
user_groups: M2M[UserGroup]
assets: M2M[Asset]
nodes: M2M[Node]
accounts: JSONB # ["root", "@ALL", "@USER"]
actions: JSONB # ["connect", "upload", "download"]
date_start: datetime
date_expired: datetime
is_active: bool
4. 组件架构对比分析 (Core + Connector 模式)
4.1 JumpServer 组件通信架构
┌──────────────────────┐
│ Nginx / LB │
└───────┬──────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Core (Django) │ │ Koko (Go) │ │ Lion (Go) │
│ :8080 (REST) │ │ :2222 (SSH) │ │ :3389 (RDP) │
│ :8070 (WS) │ │ :5000 (WS) │ │ :8081 (WS) │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
│ Redis Pub/Sub │ │
├──────────────────────┼──────────────────────┤
│ DB 0: Session Rooms │ │
│ DB 4: Asset Cache │ │
│ DB 6: WS Messages │ │
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────────────┐
│ MySQL/Postgres │ │ Target Assets │
└─────────────────┘ └──────────────────────┘
4.2 Bootstrap Token 注册流程
Koko 启动
│
├─ 1. POST /api/v1/terminal/terminal-registrations/
│ Header: Authorization: BootstrapToken <TOKEN>
│ Body: {"name": "Koko-01", "type": "koko", ...}
│
▼
Core 验证 Bootstrap Token (与 config.txt 比对)
│
├─ 2. 生成 Access Key (ID + Secret)
│ 保存到 Core DB (terminal_terminalregistration 表)
│ 返回 {id, key, secret}
│
▼
Koko 保存 Access Key 到本地文件 (data/keys/.access_key)
│
├─ 3. 后续所有 API 调用使用 Access Key
│ Header: Authorization: Key <ACCESS_KEY_ID>:<SIGNATURE>
│
├─ 4. 心跳保活 (每 30s)
│ POST /api/v1/terminal/terminal-registrations/<id>/heartbeat/
│
▼
稳定运行
4.3 用户连接完整流程
用户(Browser) --WS--> Luna --WS--> Koko(:5000) --REST--> Core(:8080) --Redis/DB
│ │
│ 1. WebSocket Connect │
│ 2. TerminalInit(token) │
│ │
│ 3. Koko 验证 ConnectToken ─────────┤
│ 4. Core 返回(asset, account, cred) │
│ │
│ 5. Koko --SSH--> Target Asset │
│ 6. I/O 双向流 │
│ 7. asciicast 实时录像 │
│ │
│ 8. 会话结束,上传录像 ──────────────┤
4.4 Nexus 适配建议
| JumpServer 设计 | Nexus 适配 |
|---|---|
| Connector 分离 | 采纳但简化 — Nexus 可将 SSH 代理内嵌为 FastAPI 子进程或 Docker sidecar,不需要独立的 Go 二进制 |
| Bootstrap Token + Access Key | 采纳 — 内部服务间认证的最佳实践 |
| Redis Pub/Sub 房间模式 | 采纳 — Koko 的多实例会话共享通过 Redis Room,Nexus 多 worker 同理 |
| Connect Token | 采纳 — 短生命周期的一次性连接令牌,JumpServer 最精华的安全设计 |
4.5 Nexus 推荐架构
Nexus API Server (FastAPI)
├── /api/v1/* REST API
├── /ws/terminal/{token} WebSocket 终端端点
└── SSH Proxy Service 内置 SSH 代理 (asyncssh)
│
├── 连接池管理
├── asciicast 录像
└── 命令过滤拦截
5. Web SSH 方案对比
5.1 三项目数据流对比
Koko (Go) — 最成熟但最重
浏览器(Luna, xterm.js)
│ WebSocket (JSON: {type:"TERMINAL_DATA", data:"ls\n"})
▼
Koko HTTPD Server (:5000)
├── UserWebsocket → 解析消息类型
├── tty.go → TerminalInit(connectToken) 验证
├── srvconn/ssh.go → 建立 SSH 连接到目标
└── 双向 I/O 代理
├── 浏览器 → Koko → SSH (stdin)
├── SSH → Koko → 浏览器 (stdout)
├── asciicast 录像 (asciinema/asciinema.go)
└── 窗口 resize: TerminalResize → ssh.Window{Width,Height}
EasyNode (Node.js) — 最简洁
浏览器(Vue, xterm.js)
│ WebSocket (ws://host:8082/terminal?hostId=xxx)
▼
Koa Server
├── controller/terminal.js → 处理 terminal WS
├── controller/ssh.js → 凭证管理 (CRUD for NeDB)
└── ssh2 npm 库 → SSH 连接到目标
└── SSH Stream ↔ WebSocket 双向转发
Luna (Angular) — Web Terminal UI 参考
Luna 核心组件:
├── pages-terminal → 终端页面容器 (多标签)
├── elements-terminal → xterm.js 包装组件
├── elements-replay-asciicast → 录像回放
├── elements-sftp → SFTP 文件管理 UI
└── 通信层
├── WebSocket → /ws/terminal/?token=xxx
└── HTTP REST → Core API (/api/v1/...)
5.2 终端数据流关键细节
WebSocket 消息协议 (Koko)
// 浏览器 → Koko
{"id":"uuid","type":"TERMINAL_INIT","data":"{\"cols\":80,\"rows\":24,\"code\":\"...\"}"}
{"id":"uuid","type":"TERMINAL_DATA","data":"ls -la\n"}
{"id":"uuid","type":"TERMINAL_RESIZE","data":"{\"cols\":120,\"rows\":40}"}
{"id":"uuid","type":"TERMINAL_BINARY","raw":"<base64>"}
// Koko → 浏览器
{"id":"uuid","type":"TERMINAL_SESSION","data":"{\"session\":{...}}"}
{"id":"uuid","type":"TERMINAL_DATA","data":"[01;34mfile[0m\n"}
{"id":"uuid","type":"CLOSE"}
SSH 连接复用 (Koko)
// koko/pkg/srvconn/ssh.go
type SSHClient struct {
*gossh.Client
refCount int32 // 引用计数
traceSessionMap map[*gossh.Session]time.Time // 活跃 session 追踪
ProxyClient *SSHClient // 支持跳板机代理链
}
func (s *SSHClient) AcquireSession() // refCount++ 复用连接
func (s *SSHClient) ReleaseSession() // refCount-- 还回连接池
5.3 Nexus 推荐方案
综合最优方案:Koko 协议 + EasyNode 简洁架构 + Python 生态实现
| 维度 | 建议 | 技术选型 |
|---|---|---|
| WebSocket 协议 | 采用 Koko 的消息格式 (TERMINAL_INIT/DATA/RESIZE/CLOSE) | 已验证的协议设计 |
| SSH 后端 | Python asyncssh (纯 Python, async/await 原生支持) | 比 paramiko 更适合 FastAPI 异步 |
| 连接池 | 采用 Koko 的引用计数模式 | 复用 SSH 连接,减少握手开销 |
| SFTP | asyncssh 内置 SFTP 支持 | 无需额外库 |
| 窗口 resize | WebSocket SIGWINCH → ssh.resize_pty() | 标准实现 |
| 前端 | xterm.js + xterm-addon-fit/weblinks/webgl | 社区最成熟的 Web Terminal 方案 |
| 录像格式 | asciicast v2 (.cast.gz) | 与 asciinema 生态兼容 |
6. 会话审计方案
6.1 JumpServer 审计架构
会话生命周期:
Session.created → Session.connected → Session.disconnected → Session.ended
录像生成:
KoKo: SSH I/O → asciinema/asciinema.go → .cast.gz (单文件)
或: → part.1.cast + part.2.cast + ... + replay.json (流式分片)
存储路径:
MEDIA_ROOT/replay/{YYYY-MM-DD}/{session_id}/
├── {session_id}.cast.gz # asciicast SSH 录像
├── {session_id}.replay.json # 元数据清单 (v3+)
├── {session_id}.part.1.cast # 流式分片
└── {session_id}.part.N.cast
存储后端:
Local (default_storage) → Celery 异步上传 → S3/OSS/MinIO/Ceph (SERVER_REPLAY_STORAGE)
命令日志:
session.cmd_filter_rules_acls → CommandFilterACL 实时匹配
session.command_amount → 统计计数
6.2 审计相关模型
# JumpServer terminal/models/session.py (核心字段推断)
class Session:
id: UUID
user: FK[User]
asset: FK[Asset]
account: FK[Account]
protocol: str # "ssh", "rdp", "mysql"
remote_addr: str # 客户端 IP
is_finished: bool
date_start: datetime
date_end: datetime
has_replay: bool
has_command: bool
command_amount: int
terminal: FK[TerminalRegistration] # 哪个 Connector 处理的
6.3 Nexus 审计方案推荐
| 功能 | 方案 |
|---|---|
| 录像格式 | asciicast v2 (.cast.gz) — 纯文本+gzip,存储成本低,可流式播放 |
| 录像生成 | Python asciinema 兼容的输出格式,在 SSH 代理层直接生成 |
| 录制模式 | 流式分片 (part-based) — 每 5 分钟或 1MB 切一片,避免单文件过大 |
| 存储 | 本地磁盘 → 定时任务上传 S3/MinIO (异步) |
| 回放 | 前端 xterm.js + asciinema-player 组件 |
| 命令日志 | 在 SSH stdin 流中按 \n 分割提取,存入 command_logs 表 |
| 实时监控 | WebSocket 推送在线会话列表,支持管理员实时旁观 (Room 模式) |
| 保留策略 | 按天数自动过期删除 (配置化) |
7. 架构决策建议 (ADR 格式)
ADR-001: 资产模型采用 Platform 模板 + 单表 Asset + JSONB 扩展属性
状态: 提议
上下文: Nexus 管理 2000+ 服务器,覆盖 Linux/Windows/网络设备/数据库。JumpServer 使用 Django 多表继承实现分类,但这对 SQLAlchemy + FastAPI 过于复杂。
决策: 采用 JumpServer 的 Platform 概念(资产类型模板),但用单表 Asset + extra_attrs JSONB 替代多表继承。
后果:
- 优点:查询简单,JSONB 索引支持高效过滤,新增资产类型无需改表
- 缺点:无法在数据库层面约束特定类型的必填字段
- 缓解:Pydantic 验证层做类型特定的字段校验
ADR-002: Web SSH 采用 BFF (Backend For Frontend) 模式,内嵌 SSH 代理
状态: 提议
上下文: JumpServer 将 Core API 和 Koko 分离为不同进程(Django + Go)。这种分离增加了部署复杂度。Nexus 初期不需要支持多协议(RDP/VNC)。
决策: Nexus 将 SSH 代理功能内嵌在 FastAPI 进程中:
- WebSocket 端点
/ws/terminal/{token}直接在 FastAPI 中处理 - 使用
asyncssh库建立 SSH 连接 - 连接池管理通过 Redis 共享给所有 worker(支持多进程部署)
- Connect Token 机制照搬 JumpServer 的设计
后果:
- 优点:部署简化(一个服务),开发效率高(全 Python)
- 缺点:SSH 代理负载与 API 负载共用资源,大规模场景需要独立部署
- 缓解:架构预留分离接口,未来可拆分为独立 SSH Proxy 服务
ADR-003: 会话录像采用 asciicast v2 格式 + 流式分片上传
状态: 提议
上下文: JumpServer 的 asciicast 格式生态成熟。EasyNode 没有录像功能。
决策:
- 终端 I/O 按 asciicast v2 格式记录(每行 JSON:
[time, type, data]) - 每 5 分钟或 1MB 生成一个 part 文件
- 会话结束后合并为 .cast.gz
- 异步上传到 MinIO/S3
后果:
- 优点:与 asciinema 生态兼容,纯文本可搜索,gzip 压缩率高
- 缺点:不支持 RDP/VNC 图形协议录像(Nexus 不涉及)
ADR-004: 权限体系采用 RBAC + AssetPermission 两层
状态: 提议
上下文: JumpServer 四层(认证+RBAC+AssetPermission+ACL)对 Nexus 初期过度设计。
决策:
- Layer 1: JWT 认证 + MFA (TOTP)
- Layer 2: RBAC 角色 (admin/operator/auditor/user)
- Layer 3: AssetPermission (用户/用户组 → 资产/节点 + 账号 + 时间窗口 + 动作)
ACL 层(命令过滤、连接方式控制)推迟到 v2。
后果:
- 优点:实现简单,2000+ 服务器规模够用
- 缺点:缺少命令级精细化控制
- 缓解:v1 提供命令日志审计(事后追溯),v2 加上实时命令过滤
ADR-005: 前端终端方案采用 xterm.js + WebSocket 自定义协议
状态: 提议
上下文: Luna 使用 Angular + xterm.js,EasyNode 使用 Vue + xterm.js。两个项目的 xterm.js 集成模式类似。
决策:
- 前端:纯 HTML/Tailwind CSS v4 + 原生 JS 的 xterm.js(不引入 Vue/Angular 框架,与 Nexus 现有技术栈一致)
- WebSocket 消息格式:采用 Koko 的消息类型定义(TERMINAL_INIT/DATA/RESIZE/CLOSE/SFTP_*)
- 多标签页:轻量级标签管理器
后果:
- 优点:零额外框架依赖,体积小,性能好
- 缺点:需要自行实现终端标签管理、会话恢复等 UI 逻辑
8. 技术迁移评估:Django → FastAPI
8.1 关键差异对照表
| 维度 | Django / DRF | FastAPI | 迁移难度 |
|---|---|---|---|
| ORM | Django ORM (同步, lazy queryset) | SQLAlchemy 2.0 Async (显式 async, eager loading) | 高 |
| 序列化器 | DRF Serializer (声明式) | Pydantic v2 (类型注解) | 中 |
| 路由 | Django URL conf + ViewSet | FastAPI APIRouter + path decorators | 中 |
| 中间件 | Django Middleware | FastAPI Middleware / Depends | 低 |
| 权限 | Django Permission + DRF Permission | FastAPI Depends + 自定义权限函数 | 中 |
| 分页 | DRF PageNumberPagination | fastapi-pagination 或自行实现 | 低 |
| 过滤 | django-filter + FilterSet | SQLAlchemy 查询参数 + fastapi-filter | 中 |
| 信号 | Django Signals | SQLAlchemy Events | 低 |
| Celery | Django-Celery | Celery (独立) | 低 |
| WebSocket | Django Channels | FastAPI 原生 WebSocket | 低 |
| 组织隔离 | current_org 线程局部变量 |
ContextVar 或中间件注入 | 中 |
8.2 关键迁移模式
Django ORM → SQLAlchemy Async 查询等价写法
# Django: 分页 + 过滤 + 预加载
assets = Asset.objects.filter(
platform__name="Linux", is_active=True
).select_related("platform").prefetch_related("nodes").order_by("name")
paginator = PageNumberPagination()
page = paginator.paginate_queryset(assets, request)
# SQLAlchemy Async: 等价写法
from sqlalchemy import select
from sqlalchemy.orm import selectinload
stmt = (
select(Asset)
.options(
selectinload(Asset.platform),
selectinload(Asset.nodes),
)
.where(Asset.platform.has(name="Linux"), Asset.is_active == True)
.order_by(Asset.name)
)
page = await paginate(session, stmt, offset=offset, limit=limit)
DRF ViewSet → FastAPI Router
# Django DRF
class AssetViewSet(OrgBulkModelViewSet):
model = Asset
filterset_class = AssetFilterSet
search_fields = ("name", "address", "comment")
@action(methods=["GET"], detail=True, url_path="platform")
def platform(self, request, pk=None): ...
# FastAPI
router = APIRouter(prefix="/api/v1/assets/assets", tags=["assets"])
@router.get("/")
async def list_assets(
search: str = Query(None),
platform: str = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
): ...
@router.get("/{asset_id}/platform")
async def get_asset_platform(
asset_id: UUID,
db: AsyncSession = Depends(get_db),
): ...
Django Signals → SQLAlchemy Events
# Django
@receiver(post_save, sender=Session)
def on_session_end(sender, instance, **kwargs):
if instance.is_finished:
upload_replay_to_s3.delay(instance.id)
# SQLAlchemy Async
@event.listens_for(Session, "after_update")
def on_session_update(mapper, connection, target):
if target.is_finished:
# 注意: async event 需要特殊处理
asyncio.ensure_future(upload_replay(target.id))
8.3 Django ORM ↔ SQLAlchemy 2.0 常用查询对照
| Django ORM | SQLAlchemy 2.0 Async |
|---|---|
qs.filter(Q(a=1) | Q(b=2)) |
where(or_(Model.a==1, Model.b==2)) |
qs.annotate(cnt=Count('assets')) |
select(Model).options() + 子查询或 column_property |
qs.values_list('id', flat=True) |
select(Model.id) + scalars().all() |
qs.select_related('platform') |
.options(selectinload(Model.platform)) |
qs.prefetch_related('nodes') |
.options(selectinload(Model.nodes)) |
qs.bulk_create(objs) |
session.add_all(objs) 配合 server-side cursor |
qs.bulk_update(objs, ['name']) |
session.execute(update(Model), [...]) |
qs.get_or_create() |
手动 select → insert/return |
Atomic(transaction) |
async with session.begin(): |
Node.get_all_children() (key istartswith) |
where(Node.path.like(f'{parent_path}.%')) |
8.4 迁移风险与注意事项
- 查询性能:Django ORM 的 lazy loading 在 SQLAlchemy Async 中必须显式声明为
selectinload或joinedload,否则 N+1 问题立现 - 事务管理:Django 的
ATOMIC_REQUESTS自动包裹请求,FastAPI 需要显式管理async with session.begin() - 多租户过滤:JumpServer 的
current_org线程局部变量在 async 环境中不安全,必须用ContextVar或依赖注入 - WebSocket 中访问数据库:Django Channels 支持同步 ORM,FastAPI WebSocket 中需用
AsyncSession - Pydantic 验证层:CRUD 场景下,需要在 Pydantic schema 和 SQLAlchemy model 间建立清晰的转换层(create_dto / update_dto / response_dto)
9. 建议的 Nexus 实现优先级
| 优先级 | 模块 | 参考项目 | 工作量估计 |
|---|---|---|---|
| P0 | 资产 CRUD + Platform 模板 + Node 树 | JumpServer | 2-3 周 |
| P0 | JWT 认证 + MFA | JumpServer | 1 周 |
| P0 | Web SSH 终端 (WebSocket + asyncssh + xterm.js) | Koko + EasyNode + Luna | 3-4 周 |
| P1 | RBAC + AssetPermission 授权 | JumpServer | 2 周 |
| P1 | SFTP 文件管理 (Web 界面) | Luna + EasyNode | 1-2 周 |
| P1 | 会话录像 (asciicast) + 存储 | JumpServer | 2 周 |
| P2 | 批量指令下发 | EasyNode | 1-2 周 |
| P2 | 命令日志 + 审计 | JumpServer | 1 周 |
| P2 | 凭据托管 (加密存储) | EasyNode | 1 周 |
| P3 | 脚本库 | EasyNode | 1 周 |
| P3 | 命令过滤 ACL | JumpServer | 2 周 |