后端架构
QXJ 的"后端"由五个定位不同的工程组成:一个生产服务、一个被所有业务后端消费的 SDK、一个教学 Demo,以及一对开发 / 部署阶段使用的配置校验工具。它们通过 NSP-SM 令牌约定、Redis 会话密钥模型与统一的 .env 配置规范协作。
后端项目地图
| 项目 | 定位 | 形态 / 运行方式 |
|---|---|---|
| qxj-backend-admin | 认证平台主体:注册 / 握手 / 令牌 / 用户设备策略管理 | Django + DRF,gunicorn :4607 |
| qxj-backend-sdk | 业务后端的安全接入层(token 验证 + 国密加解密) | Python monorepo,3 个可 pip 安装的包,vendor wheel 交付 |
| qxj-backend-mock-demo | 后端教学:改造前 / 改造后对照 | 两个 Django 工程(before 8100 / after 8101) |
| qxj-backend-frontend-env-config-helper-cli | 前后端 .env 一致性校验(零依赖) | Node ESM CLI,命令 qxj-envcheck |
| qxj-backend-frontend-env-config-helper-web | 同一能力的浏览器版 | Vue 3 + TS SPA,:5174 |
协作关系
关键关系:
- backend-admin 是唯一接触用户口令与服务器私钥的服务;它只负责签发令牌和 管理 Redis 中的会话密钥,不参与业务;
- backend-sdk 是业务后端的唯一接入点:业务方不直接调用密码库、不需要知道 握手细节,三个高层函数(验 token / 加密 / 解密)覆盖全部场景;
- mock-demo after 是 SDK 的"官方参考实现":接入方照抄它约 40 行的认证类 即可完成改造;
- envcheck 是开发 / 部署期工具,不参与运行时,解决"前后端
.env开关 对不齐导致的联调问题";CLI 用于终端 / CI,Web 用于不想装 Node 工具的场景。
一、qxj-backend-admin(认证后端)
基于 Django 4.2 + DRF,是系统的信任根。模块总览与业务链路见 认证后端仓库页,本页聚焦工程与代码组织。
目录结构(实际)
qxj-backend-admin/
├── apps/ # 14 个业务 app(见下表)
├── common/ # DRF 公共件:exceptions/filters/pagination/permissions/throttling/http
├── config/
│ ├── settings/ # base.py / development.py / production.py
│ ├── urls.py # 总路由
│ ├── wsgi.py / asgi.py
├── tools/
│ ├── setup.py # 统一运维 CLI(init/migrate/qrcode/...)
│ ├── gen_token.py # 手工生成令牌(调试)
│ ├── register_device/ # 设备 .dat 批量导入(import_keys.py + files/)
│ └── init/ # 演示数据初始化(run_all/admin/roles/devices/
│ # geofences/schedule/base)
├── docs/ # 操作文档(HTTPS、部署等)
├── agent_docs/ # 交接 / 计划类文档
├── docker/ # Dockerfile + compose(安全基线,可选)
├── certs/ # 自签 TLS(server.crt/key)
├── vendor/ # qxj_backend_sdk 本地 wheel
├── frontend_dist/ # 前端 build:backend 输出(同源托管)
├── exported_qrcode/ # 二维码导出目录
├── requirements.txt
└── db.sqlite3 # 默认数据库文件(生产 MySQL)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
app 分层
| 层 | app |
|---|---|
| 账户 | users、roles、user_roles |
| 认证 | auth(sm2_jwt / authentication / services / views) |
| 密钥与握手 | keymgr(models + key_exchange_services + utils: crypto/key_exchange/packet_parser/qrcode_gen + libs/*.so) |
| 设备 | devices、device_public_keys |
| 策略 | geofences、role_geofences、time_rules、role_time_rules |
| 审计 | security_logs(含 middleware) |
| 验证渠道 | sms、captcha |
注意:没有 audit app——审计在 security_logs。
14 个 app 完整字段表(点击展开)
| app | 模型 | 关键字段 |
|---|---|---|
| users | User | real_name, phone, status(0/1/2), expires_at, failed_attempts, allowed_ips(CIDR), token_version |
| roles | Role(→Group) | name, description |
| user_roles | UserRole | user_id, role_id |
| auth | (无模型) | sm2_jwt, BlacklistJWTAuthentication, services |
| keymgr | ServerKey | sm2_key_pair(SM4 加密), active, rotation_count |
| devices | Device | device_id(UUID), user_id, status, public_key, last_seen |
| device_public_keys | DevicePublicKey | device_id, public_key_pem, algorithm, status |
| geofences | Geofence | name, coords_type(GCJ02/WGS84/BD09), polygon, active |
| role_geofences | RoleGeofence | role_id, geofence_id, effect(ALLOW/DENY) |
| time_rules | TimeRule | name, schedule_type, start_time, end_time, weekdays, week_type, effect |
| role_time_rules | RoleTimeRule | role_id, time_rule_id |
| security_logs | SecurityLog | user_id, event_type, ip, path, method, status_code, detail |
| sms | SmsCode | phone, code, purpose, expires_at, used, failed_attempts |
| captcha | Captcha | ticket, content, expires_at, used, ip |
路由分层(config/urls.py)
| 前缀 | 归属 | 认证要求 |
|---|---|---|
/api/v3/(GET 根) | api_index:开发期路由清单(SHOW_API_DOCS) | 生产 404 |
/api/v3/user/login/、logout/、token/refresh/ | apps.auth.urls | 登录/登出/刷新各自策略 |
/api/v3/user/captcha/ | apps.captcha | AllowAny(JSBridge 预验证票据) |
/api/v3/keymgr/sm2/build_resp/、build_token/ | keymgr.urls_sm2 | AllowAny,octet-stream |
/api/v3/users/me/、sms/ | 业务侧 | 登录即可(BlacklistJWTAuthentication) |
/api/v3/admin/... | 12 组模块路由 | 登录 + IsAdmin(IsSuperUser 用于高敏操作) |
/api/v3/schema/、docs/、redoc/ | drf-spectacular | 仅 SHOW_API_DOCS(development) |
/admin/django/ | Django 自带后台 | ENABLE_DJANGO_ADMIN 开关 |
| 其余路径 | frontend-spa 回退 index.html | 排除 api/admin/static/assets 与带后缀文件 |
NSP-SM 编解码(apps/auth/sm2_jwt.py)
encode(payload, private_key):header{"alg":"NSP-SM","typ":"JWT"}, base64url 后对 message 先 SM3 摘要再用服务器 SM2 私钥签名 (r‖s 共 64B,签名随机数强制 CSPRNG),输出三段式 token;decode(token, public_key, leeway=0):结构校验 → alg 判定 → SM2withSM3 验签 (失败即 -3)→ 有效期校验(-4)→ 返回 payload;- 内层 HMAC 不在此文件:payload.custom.hmac 由认证类 / SDK 用设备会话密钥校验, 签名 buffer 为
device_id(UTF-8) + user_id(>Q) + exp(>Q),要求 custom.exp_s == exp。
算法标识辨析
令牌 header 的 alg 字段值是自定义字符串 NSP-SM(不是 SM2), 而 NSP-SM 背后的实际签名算法是 SM2withSM3。区分这两者很重要: 验签时先按 alg=NSP-SM 识别令牌类型,再执行 SM2 验签。
缓存配置(config/settings/base.py)
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': REDIS_URL, # 默认 redis://:****@127.0.0.1:6379/0
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
'SERIALIZER': 'django_redis.serializers.json.JSONSerializer',
'CONNECTION_POOL_KWARGS': {'max_connections': 50},
},
'KEY_PREFIX': 'qxj',
'VERSION': 1,
'TIMEOUT': 300,
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
Key 实际口径见 整体架构·数据与缓存层: 握手服务(session / pending_ack,10 分钟)、设备会话密钥(7 天)、验证码、黑名单。
验证码(apps/sms)
- 手机 + 邮箱同一个验证码,有哪个发哪个,任一通道通过即放行;带频率限制;
- 渠道:内网网关(gateway)或阿里云 dypns(
SendSmsVerifyCode, TemplateParam{"code":"##code##","min":"1"},ReturnVerifyCode 取回明文存本地); - 邮件支持纯文本/HTML 模板(EMAIL_USE_HTML)。
生产安全基线(config/settings/production.py)
- fail-closed:SECRET_KEY / QXJ_KEY_ENC_KEY 缺失拒绝启动;
- DEBUG=False、仅 JSONRenderer、SSL 跳转 + HSTS + 安全 Cookie;
- CORS_ALLOWED_ORIGINS 必须显式配置,CSRF_TRUSTED_ORIGINS 对齐 CORS;
- VERIFY_INNER_HMAC=True;日志落
logs/。
二、qxj-backend-sdk(后端国密 SDK)
业务后端的安全接入层,Python monorepo 结构,包含三个可独立发布的包:
| 包 | 职责 |
|---|---|
qxj-backend-sdk(根,统一包) | 高层统一接口,导出 verify_access_token / sm4_gcm_encrypt / sm4_gcm_decrypt |
qxj-backend-sdk-auth | 认证:token 解析、SM2 外层验签、SM3-HMAC 内层校验、Redis 会话密钥查询 |
qxj-backend-sdk-cryptography | 加解密:SM2/SM3/SM3-HMAC/SM4-CBC/SM4-GCM + 64B 安全信封 |
底层国密算法基于 gmssl(纯 Python,跨平台无需编译),不是 C 动态库。 完整 API 手册见 API 参考 · 后端 SDK。
高层接口(统一包,九成场景只用这三个函数)
from qxj_backend_sdk import verify_access_token, sm4_gcm_encrypt, sm4_gcm_decrypt
# ① 验 token:外层 SM2 验签 + Redis 取会话密钥 + 内层 HMAC
device_id, expire_time, code = verify_access_token(
token, public_key, redis_client, key_prefix="qxj:1:")
# ② 加密:自动按 device_id 取会话密钥,返回 64B 信封 hex
frame_hex, code = sm4_gcm_encrypt(device_id, plaintext, redis_client, key_prefix="qxj:1:")
# ③ 解密:从信封帧头自动解析 sender / IV,返回明文
plaintext, code = sm4_gcm_decrypt(frame_hex, redis_client, key_prefix="qxj:1:")2
3
4
5
6
7
8
9
10
11
设计要点:
- 设备 ID 自动提取:验 token 时从
custom.device_id、解密时从信封帧头的 sender 字段提取,调用方无需重复传入; - redis_client 与 redis_url 二选一:传 URL 时 SDK 内部自动构造 SessionKeyStore;
skip_hmac=True可只做外层验签(device_id 为空的令牌场景);- 所有失败均以错误码返回、不抛异常,业务代码无需 try/catch 包裹密码调用。
结果码体系
Token 验证:
| code | 含义 |
|---|---|
| 0 | 完全通过(外层 + 内层) |
| 1 | 外层通过,内层跳过(device_id 为空 / skip_hmac) |
| -1 / -2 | Token 为空 / 解析失败 |
| -3 / -4 | SM2 验签失败 / 已过期 |
| -5 / -6 | custom 缺失 / 内层 HMAC 失败 |
| -7 / -8 | 无会话密钥 / Redis 异常 |
加解密:
| code | 含义 |
|---|---|
| 0 | 成功 |
| -3 / -4 | 无会话密钥 / 信封格式错误 |
| -5 / -9 | GCM tag 校验失败 / hex 解析失败 |
64B 安全信封
高层加解密自动处理的信封结构:
┌──────────────────────────────────────────────────────────┐
│ 64B 帧头:version + main_cmd + sub_cmd + total_len + │
│ frame_seq + sender_id(36B) + enc_auth + IV(16B) + ... │
├──────────────────────────────────────────────────────────┤
│ SM4-GCM 密文 │
├──────────────────────────────────────────────────────────┤
│ 16B GCM tag │
└──────────────────────────────────────────────────────────┘2
3
4
5
6
7
8
帧头自包含 sender(设备 IDA)与 IV,因此解密方只需信封本身 + Redis 即可还原, 不需要额外的密钥协商上下文。
认证包的低层接口(需要精细控制时)
from qxj_backend_sdk_auth import TokenVerifier, SessionKeyStore
verifier = TokenVerifier(
public_key_hex="服务器公钥128hex",
session_store=SessionKeyStore(redis_url="redis://...", key_prefix="qxj:1:"),
)
result = verifier.verify(token) # result.ok / outer_ok / user_id / device_id / message2
3
4
5
6
7
还附带命令行工具(qxj-backend-sdk-auth verify/parse),便于在终端排查令牌。
安装方式
# 生产:统一 wheel
pip install qxj_backend_sdk-0.1.0-py3-none-any.whl
# 开发:可编辑安装(src/ 改动即时生效)
pip install -e .
# 只需单一能力
pip install qxj-backend-sdk-auth # 或 qxj-backend-sdk-cryptography2
3
4
5
6
7
8
三、qxj-backend-mock-demo(教学 Demo)
面向业务后端接入方的教学工程:同一个极简 Django 业务(登录 + 需认证接口), 给出 before / after 两份完整可运行代码,回答"接入 QXJ 后后端要改什么"。 配套前端 Demo 为 qxj-frontend-mock-demo(5200 / 5201)。
before / after 对比
| before(传统前后端分离) | after(QXJ SDK 方案) | |
|---|---|---|
| Token 来源 | 自己签 HS256 JWT | 不签发 token,token 来自认证服务器 |
| 认证校验 | 手写 Bearer + HS256 验签 | verify_access_token()(SM2 + HMAC 双层) |
| 业务报文 | 明文 JSON | SM4-GCM 信封(64B 帧头 + 密文 + tag) |
| 密钥管理 | 无 / 本地配置 | 按 device_id 从 Redis 读握手会话密钥 |
after 的工作方式
鸿蒙 Pad(原生安全区) 业务后端(after)
access/refresh/K_client ──Bearer NSP-SM JWT──▶ verify_access_token()
SM2 握手 / SM4-GCM ──64B 信封 hex──────▶ sm4_gcm_decrypt()
◀──64B 信封 hex────── sm4_gcm_encrypt()
▲ ▲
└──── Redis(会话密钥由认证服务器握手后写入)┘2
3
4
5
6
after/mock_pad/ 是仅用于浏览器演示的 app:没有真机 Pad 时,由它模拟认证 服务器签发 token、模拟 Pad 原生的登录 / 刷新 / 加解密落点,供前端 mock-jsbridge 调用。真实业务后端不应包含该 app。
接入自己的后端:只需两处
① 认证(约 40 行,见 after/auth_app/qxj_auth.py):配置认证服务器公钥 + Redis,调用 verify_access_token;
② 加解密(见 after/auth_app/views.py):业务入口处 sm4_gcm_decrypt, 出口处 sm4_gcm_encrypt。
会话密钥的 Redis key 约定:{key_prefix}sm2:device_session_key:{device_id}, 由认证服务器的握手流程写入、TTL 7 天。
真机联调的两种模式
after/.env 中预置两套模板:
- B1 局域网台式机:真实后端跑在本机 WSL、Redis db0 可达,配真实服务器公钥
key_prefix=qxj:1:,token 验签 / 内层 HMAC / SM4 解密可完整闭环;
- B2 外网环境:Redis 6379 外部不可达,只能验证登录页跳转 / 回跳 / getAccess 带 token 到本后端的链路;读不到会话密钥时业务接口返回 -7/-8,属预期。
公钥获取:在认证服务器执行 python -m tools.setup qrcode,取 server_id#timestamp#public_key 的第 3 段。
启动
# before(端口 8100)
cd before && pip install -r requirements.txt
python manage.py runserver 0.0.0.0:8100
# after(端口 8101)
cd after && pip install -r requirements.txt # 自动安装 vendor/ 下 SDK wheel
cp .env.example .env
python tools/init_demo.py # 生成演示密钥对 + 预置设备 + 自检
python manage.py runserver 0.0.0.0:81012
3
4
5
6
7
8
9
四、env-config-helper-cli(配置校验工具)
命令名 qxj-envcheck:校验 backend-admin 与 frontend-admin 的 .env 配置完整性与前后端一致性。前后端有大量联动开关(JSON 登录、验证码场景、 CORS 端口、锁屏密钥…),人工核对极易出错,该工具把这些关系固化为规则。
特点
- 零运行时依赖:仅用 Node 内置模块(fs/path/readline/crypto);
- ESM、ANSI 彩色输出(直接写转义序列);
- vue-cli 风格:
qxj-envcheck <command> [options]。
命令
| 命令 | 说明 |
|---|---|
check | 单项目配置完整性(必填缺失 / 占位值 / 格式错误) |
validate | 前后端一致性(核心命令,8 条关系规则) |
diff | .env 与 .env.example 差异对比 |
show | 当前生效配置展示(密钥自动脱敏) |
init | 交互式从模板初始化 .env |
通用选项:--backend / --frontend / --env <development\|production>; 退出码 0 无问题 / 1 有警告 / 2 有错误(可接入 CI 卡点)。
8 条跨项目关系规则(validate)
| 规则 | 校验内容 |
|---|---|
| R1 | 登录模式联动:后端 ALLOW_JSON_LOGIN 与前端 VITE_ALLOW_JSON_LOGIN 必须相等 |
| R2 | 验证码场景联动:启用验证码时前后端 SCENE_ID 必须一致 |
| R3 | 专用设备门禁联动:前端要求专用设备时后端不应允许跳过手机号 |
| R4 | CORS 跨域端口校验:前端端口必须在后端 CORS 白名单中 |
| R5 | 阿里云 AccessKey:启用验证码 / 阿里云短信时 AK 必须配置 |
| R6 | 短信通道配置:按 SMS_PROVIDER 检查对应必填项 |
| R7 | 跳过校验开关:生产环境不得开启任何跳过开关 |
| R8 | 前端锁屏密钥:VITE_LOCK_ENCRYPT_KEY 必须设置且非占位值 |
工程结构
src/
├── bin/cli.mjs # 命令路由
├── core/ # parser(.env 解析)/ merger(多级合并)/
│ # writer / validator(占位检测 + 规则执行)
├── schema/ # backend / frontend 变量元数据 + relationships(8 条规则)
└── commands/ # check / validate / diff / show / init2
3
4
5
6
五、env-config-helper-web(工具的 Web 版)
同一能力的浏览器版:在页面里粘贴前后端 .env,完成解析、规则校验、密钥 / 变量合并与差异对比,并导出合并结果。适合不想安装 CLI、或需要可视化查看问题 清单的场景。
技术栈与功能
- Vue 3 + TypeScript + Vite + Element Plus(unplugin-auto-import 按需)+ Pinia;
- 三视图:后端视图(Django
.envschema 校验)、前端视图(ViteVITE_*校验)、对比视图(前后端差异对照); - 密钥类字段单独输入(SecretInput),不随明文导出;
- 粘贴整份配置导入(PasteImport),一键复制合并输出(ExportOutput)。
npm install
npm run dev # http://localhost:5174(strictPort)2
与 CLI 的关系
两者的 schema 与规则口径一致、互相独立:CLI 面向终端 / CI(退出码卡点), Web 面向人工排查(可视化问题面板 + 差异高亮)。
跨项目统一约定
| 约定 | 内容 |
|---|---|
| 信任边界 | 用户口令、服务器私钥只存在于 backend-admin;业务后端只持有服务器公钥 |
| 会话密钥模型 | Redis key {prefix}sm2:device_session_key:{device_id},握手写入、TTL 7 天 |
| 令牌标识 | header alg=NSP-SM(自定义),签名算法 SM2withSM3,内层 SM3-HMAC |
| 信封结构 | 统一 64B 帧头(自包含 sender/IV)+ SM4-GCM 密文 + 16B tag |
| 错误处理 | SDK 全部以错误码返回、不抛异常;token / 加解密两套码表跨语言一致 |
| 配置管理 | 前后端联动开关以 envcheck 8 条规则为准,生产禁止跳过类开关 |
| 内网交付 | SDK 以 vendor wheel、零依赖 CLI 交付,运行与构建不依赖公网 |