前端架构
QXJ 的"前端"不是一个项目,而是四个定位完全不同的工程,加上一个独立的应用分发 站点。它们各自独立构建、独立部署,通过 NSP-SM 令牌约定与 JSBridge 协议 协作。先看全景,再逐项目展开。
前端项目地图
| 项目 | 定位 | 形态 / 部署 |
|---|---|---|
| qxj-frontend-admin | 管理控制台 + Pad 端 /m/* 移动 UI | Vue SPA,独立部署或输出到 Django frontend_dist/ 同源托管 |
| qxj-frontend-sdk | 浏览器侧安全能力库(JSBridge 封装 + 国密 + JWT 验证) | npm 包(ESM/CJS)+ UMD 单文件,被其他前端以 vendor 包消费 |
| qxj-frontend-mock-demo | 接入方教学 Demo:改造前 / 改造后对照 | 两个 Vue SPA(before / after) |
| qxj-frontend-web-test-sm2-jwt | NSP-SM JWT Inspector,对标 jwt.io 的解析验签工具 | 零构建静态三文件(HTML/CSS/JS),线上 jwt-inspector.qxj.nsp.ac.cn |
| apps.nsp.ac.cn(应用商店) | HarmonyNext 按设备分发 HAP 的应用中心 | Vue 静态站,详见仓库页 |
协作关系
关键关系:
- SDK 是其他三个 Vue 工程的共同依赖:以
vendor/*.tgz形式随仓库交付 (内网环境无需访问 npm registry); - JWT Inspector 故意不依赖 SDK:零构建、CDN 加载 sm-crypto,保证任何人拿到 三个文件就能离线打开;
- 业务 H5 与管理 UI 是两个站点:真机上 Pad 默认打开业务页,未登录时整页 跳转到管理前端的
/m/login,登录后跳回——跳转协议见 Mock Demo 小节。
一、qxj-frontend-admin(管理前端)
基于 art-design-pro 模板定制,承担全部管理功能;同时是 Pad 端移动 UI 与外域 登录页的提供者。业务页面清单见仓库页,本页聚焦 工程组织。
技术栈(实际版本)
Vue 3.5 + Vite 7 + TypeScript 5.6 + Element Plus 2.11 + ECharts 6 + Pinia 3 + Vue Router 4.5 + Tailwind CSS 4 + axios;包管理器 pnpm,Node ≥ 20.19。
src 结构(实际目录)
src/
├── api/
│ ├── auth.ts # 登录/登出/刷新/验证码/me
│ ├── qxj/ # users/roles/associations/devices/geofences/time-rules/...
│ ├── mobile/ # 移动端薄包装(9 个模块,聚合复用 qxj/ 与 auth)
│ └── system-manage.ts
├── config/qxj.ts # 业务开关(唯一自定义配置入口)
├── router/
│ ├── core/ # MenuProcessor/RouteRegistry/RouteTransformer/
│ │ # IframeRouteManager/ComponentLoader 等
│ ├── guards/ # beforeEach(含 JSBridge 检测)/ afterEach
│ ├── modules/ # 按业务分组的路由模块(overview/policy/audit/...)
│ └── routes/ # staticRoutes/asyncRoutes/mobileRoutes
├── store/modules/ # user/menu/setting/table/worktab(Pinia)
├── utils/
│ ├── http/ # axios 双分支实例(v3 / mock)
│ ├── security/ # anti-debug
│ └── ... # storage/ui/navigation/form/sys
├── views/
│ ├── qxj/ # PC 管理页(表格 + 弹窗)
│ ├── mobile/ # 移动管理页(/m/*)
│ ├── auth/ install/ # 登录、专用设备安装引导
│ └── dashboard/ examples/ template/ system/ exception/ ... # 模板页面
├── components/
│ ├── core/ # 模板组件(charts/forms/layouts/tables/...)
│ └── mobile/ # 移动端布局组件(ManageLayout/BackHeader/...)
├── types/api/qxj.d.ts # 与后端对齐的类型契约
└── main.ts2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
API 层
auth.ts:/api/v3/user/login/、logout/、/api/v3/token/refresh/、/api/v3/users/me/、/api/v3/sms/send_code/;qxj/:BASE/api/v3/admin/,按资源拆分;associations.ts统一管理 用户-角色、角色-围栏、角色-时间规则三张关联表;mobile/:9 个模块的薄包装,复用 qxj/ 与 auth.ts,不重复实现业务逻辑。
HTTP 层双分支(utils/http/index.ts)
- 按 URL 是否含
/api/v3/区分:v3 走 DRF 标准(2xx 成功、分页results/count、错误取detail),非 v3 走模板 mock 约定; - JSBridge 模式:请求前
await jsBridgeGetAccess()注入 Bearer;401 时jsBridgeRefresh()重试一次;会话失效派发事件跳登录; - 凭据来自 qxj-frontend-sdk(vendor tgz),不自行拼装桥接调用。
业务开关 config/qxj.ts
HOME_LINKS、ENABLE_MOBILE_UI、ENABLE_PC_UI、LOGIN_ROUTE_NAME、USE_JSBRIDGE、 ALLOW_JSON_LOGIN、REQUIRE_DEDICATED_DEVICE、ENABLE_INSTALL_PAGE、 MOBILE_BREAKPOINT = 768。桥接实现不在配置文件里。
状态管理(store/modules/user.ts)
loginMode: 'jsbridge' | 'json',用户信息 localStorage 持久化;- login/logout 调用收敛到 SDK / auth API;其余 stores(menu/setting/table/worktab) 沿用模板。
路由
- 静态路由 + 后端动态菜单,core 下完成菜单 → 路由转换、权限校验、iframe 路由 管理与组件懒加载;
- 守卫:JSBridge 可用性检测、按 is_superuser / role_type 映射权限;
- 移动路由集中在
routes/mobileRoutes.ts(/m/* 系列),按 ENABLE_MOBILE_UI 与 断点切换入口(不是按环境变量动态注册组件)。
PC / 移动双 UI
- PC:qxj/ 页面(搜索栏 + 表格 + 弹窗);
- 移动:mobile/ 页面(/m/*),同一套 API;
- 入口自适应:LOGIN_ROUTE_NAME 与 MOBILE_BREAKPOINT 控制登录页与布局走向;
- REQUIRE_DEDICATED_DEVICE 打开后,非 Pad 浏览器整站进入 wrong-device 页;
/m/login支持外域回跳:?redirect=<绝对URL>登录成功后整页跳回, 供业务 H5 接入(详见下文跳转协议)。
Vite 配置要点
- 双代理顺序关键:
/api/v3→ VITE_API_PROXY_URL(默认 http://127.0.0.1:4607),/api→ Apifox mock; - 构建:terser drop_console、cssCodeSplit:false、动态导入仅限 src/views;
- production 默认启用自研 vite-obfuscator(VITE_ENABLE_OBFUSCATOR=false 关闭; 仅处理业务 chunk);
- dev 端口 5173(VITE_PORT)。
构建模式
pnpm dev # 开发
pnpm build # 独立部署
pnpm build:backend # 输出 ../qxj-backend-admin/frontend_dist,由 Django 同源托管2
3
二、qxj-frontend-sdk(前端国密 SDK)
浏览器侧的安全接入层:把鸿蒙原生注入的 JSBridge、国密算法、NSP-SM JWT 验证统一封装为稳定 API,业务代码只与 SDK 打交道。底层国密依赖 sm-crypto。 完整 API 手册见 API 参考 · 前端 SDK。
产物形态
| 形态 | 文件 | 消费方式 |
|---|---|---|
| ESM | dist/index.mjs | import { ... } from 'qxj-frontend-sdk' |
| CommonJS | dist/index.cjs | Node / 老打包工具 require |
| UMD(已内联 sm-crypto) | dist/qxj-sdk.umd.js | <script> 直接引入,全局 QxjSdk,零构建页面可用 |
| 类型声明 | dist/index.d.ts | TypeScript 工程 |
| vendor 包 | qxj-frontend-sdk-0.2.3.tgz | 内网以 file:...tgz 安装 |
三大能力域
① JSBridge 封装:原生方法 window.jsbridgeHandle(login/getAccess/refresh/ encrypt/decrypt/logout)被包装为结构化结果 { code, message, data? }。
铁律:所有 bridge 函数的 Promise 永不 reject——任何异常都 resolve 一个带 错误码的对象,调用方只需判断 code,不需要 try/catch 包裹。
② NSP-SM JWT 解析与验证:
const r = verifyJwt(token, {
publicKey, // 服务器 SM2 公钥(128 hex,无 04 前缀)
sessionKey, // 设备会话密钥 hex
leeway: 0,
})
r.ok // 外层 SM2 签名 + 内层 SM3-HMAC 是否全部通过
r.stages // 分阶段结果:parse / alg / outer / expiry / custom / inner2
3
4
5
6
7
验证口径与后端 BlacklistJWTAuthentication 完全一致,失败码 10/11/12/20/21/ 30/31/32/33 分阶段给出,便于定位问题。
③ 国密原语:sm2 生成密钥对 / 加解密 / 签名验签、sm3 杂凑、sm3-HMAC、 sm4-CBC 加解密——这些是页面侧(非安全区)需要密码运算时的 JS 实现, 与 native 侧的 C 实现算法等价。
构建
pnpm install
pnpm run build # 产出 dist/ 四种产物
pnpm test # node test/run.js2
3
三、qxj-frontend-mock-demo(教学 Demo)
面向业务系统接入方的教学工程:同一个极简页面(登录 + 认证接口 + 加密接口), 给出 before / after 两份可直接运行的完整代码对照,回答"接入 QXJ 后前端到底 要改什么"。配套后端 Demo 为 qxj-backend-mock-demo(before 8100 / after 8101)。
before / after 对比
| before(传统前后端分离) | after(QXJ SDK 方案) | |
|---|---|---|
| Token 存储 | 双 token 存 localStorage | 网页不保存任何 token |
| 登录入口 | 页面内置登录表单 | 未登录整页跳转鉴权登录页,登录后跳回 |
| 请求鉴权 | axios 拦截器从 localStorage 取 Bearer | 请求时 jsBridgeGetAccess() 从安全区临时取到内存 |
| 401 刷新 | 拦截器用 localStorage 的 refresh | jsBridgeRefresh(),refresh 对网页不可见 |
| 业务报文 | 明文 JSON | SM4-GCM 信封(64B 帧头 + 密文 + tag) |
最直观的教学效果:before 页面底部"localStorage 实时内容"卡片能直接看到完整 token,控制台一句 localStorage.getItem(...) 即可取走;after 的 localStorage 始终为空,网络面板里业务请求只有密文信封 hex。
登录页跳转协议(业务页与登录页解耦)
业务页未登录时整页跳转到登录页,回跳地址放在 query:
{登录页URL}?redirect={encodeURIComponent(业务页URL?login=back)}- URL 上不携带任何 token;登录页调
jsBridgeLogin,token 只进原生安全区; - 业务页加载时以
jsBridgeGetAccess()探测 access 恢复登录态(刷新页面同理); - 真机登录页 =
VITE_LOGIN_PAGE_URL(frontend-admin 的/m/login); - 浏览器演示走工程内置
/mock-login模拟页(独立路由、不套业务布局)。
mock-jsbridge:让普通浏览器也能演示
普通浏览器没有原生注入的 window.jsbridgeHandle。VITE_ENABLE_MOCK_JSBRIDGE=true 时,工程启动时注入一个 HTTP 版模拟实现,把 6 个 bridge 方法转发到后端 after 的 /mock/pad/* 端点(由 qxj-backend-sdk 在服务端模拟原生登录 / 取 token / 刷新 / SM4-GCM)。
# after/.env
# 普通浏览器演示
VITE_ENABLE_MOCK_JSBRIDGE=true
# mock 端点留空走 vite proxy:/mock/ -> 127.0.0.1:8101
VITE_MOCK_PAD_BASE=
# 真机登录页(仅 mock 关闭时使用)
VITE_LOGIN_PAGE_URL=https://117.72.72.201:3006/m/login2
3
4
5
6
7
打 Pad 真机包时改为 false——window.jsbridgeHandle 由客户端原生注入, 网页代码无需任何改动,这正是接入层抽象的目的。
启动
cd before && pnpm install && pnpm dev # http://localhost:5200,对接后端 8100
cd after && pnpm install && pnpm dev # http://localhost:5201,对接后端 81012
四、qxj-frontend-web-test-sm2-jwt(JWT Inspector)
对标 jwt.io 的纯静态 NSP-SM JWT 解析 / 验签工具。标准 jwt.io 只支持国际 算法,无法验签 SM2withSM3,故自建。线上地址 jwt-inspector.qxj.nsp.ac.cn。
工程取舍
- 零构建、零框架:原生
index.html+style.css+app.js三个文件, SM2/SM3 能力从 CDN 动态加载 sm-crypto; - 不依赖 qxj-frontend-sdk,三个文件拷走即可离线使用(内网可把 CDN 换成本地 路径);
- 深色主题,对标 jwt.io 的三段式彩色布局。
功能
- 粘贴 JWT 自动拆分 Header / Payload / Signature 三块展示;
- 支持
?token=xxxURL 参数自动填充,便于分享; - 外层验签:SM3(signing_input) 后 SM2 验签(公钥 128 hex 无
04前缀, 工具自动补); - 内层 HMAC 校验:填入握手协商得到的会话密钥后校验 custom.hmac,期望载荷 =
device_id(UTF-8) ‖ user_id(BE 8) ‖ exp_s(BE 8); - exp 过期检查与剩余时间倒计时,一键复制各段与完整 Token。
本地使用
python -m http.server 8080
# http://localhost:80802
公钥获取(管理员):后端执行 python -m tools.setup qrcode,或 GET /api/v3/admin/keymgr/qrcode_string/。验签口径对齐 qxj-backend-admin/apps/auth/sm2_jwt.py 与 BlacklistJWTAuthentication。
跨项目统一约定
| 约定 | 内容 |
|---|---|
| 凭据边界 | 网页(含管理页)永不持久化 token;token 只存在于原生安全区,用时经 bridge 取到内存 |
| Bridge 调用契约 | 结构化 JSON、Promise 永不 reject、code=0 成功;错误码跨项目共用 |
| JWT 验证口径 | 前端 SDK verifyJwt、JWT Inspector、后端认证三处的载荷构造与验签顺序完全一致 |
| 国密标识 | 服务器公钥统一 128 hex(不带 04 前缀);密文信封统一 64B 帧头 + 密文 + 16B tag |
| 内网交付 | 依赖以 vendor tgz / CDN 本地化方式提供,构建不依赖公网 |
| API 基线 | REST 统一 /api/v3 前缀,DRF 分页与错误结构(detail) |