JavaScript 前端 SDK qxj-frontend-sdk
QXJ 前端国密 SDK,提供 SM2 / SM3 / SM3-HMAC / SM4-CBC 国密算法封装, 以及 NSP-SM JWT 的解析与验证(外层 SM2 签名 + 内层 SM3-HMAC),并把 鸿蒙 Pad WebView 原生能力(window.jsbridgeHandle)Promise 化为结构化 接口。网页端与鸿蒙浏览器集成的统一 SDK,当前版本 0.2.3,ESM,MIT。
底层依赖 sm-crypto 0.3.3。
安装
npm 包(ESM / CommonJS)
npm install qxj-frontend-sdkimport { init, verifyJwt, sm3 } from 'qxj-frontend-sdk'
await init()
const result = verifyJwt(token, { publicKey, sessionKey })2
3
4
浏览器原生 JS(UMD)
直接引入 dist/qxj-sdk.umd.js,全局对象为 QxjSdk(已内联 sm-crypto, 开箱即用):
<script src="qxj-sdk.umd.js"></script>
<script>
await QxjSdk.init()
const result = QxjSdk.verifyJwt(token, { publicKey, sessionKey })
</script>2
3
4
5
完整示例见仓库 examples/browser.html。
初始化
await init() // 自动加载 sm-crypto(npm 从 node_modules,浏览器从 CDN)
await init(smCryptoLib) // 或手动传入 sm-crypto 模块2
NSP-SM JWT 解析与验证
解析
const { header, payload, signatureHex, signingInput } = parseJwt(token)完整验证:verifyJwt(外层 SM2 + 内层 HMAC)
const result = verifyJwt(token, {
publicKey, // 服务器 SM2 公钥(128 hex,无 04 前缀)
sessionKey, // 设备会话密钥 hex(device_id 为空时可省略)
leeway: 0, // 过期宽容秒数
})2
3
4
5
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
token | string | NSP-SM JWT 字符串。 |
options.publicKey | string | 服务器 SM2 公钥(128 hex,无 04 前缀)。 |
options.sessionKey | string | 设备会话密钥 hex(device_id 为空时可省略)。 |
options.leeway | number | 过期宽容秒数,默认 0。 |
返回结构:
{
code: 0, // 0=通过, 1=外层通过内层跳过, 其他=失败
message: '...',
ok: true, // 是否完全通过
outerOk: true, // 外层是否通过
header, payload,
user_id, device_id, exp, jti, token_type,
hmac_expected, hmac_received,
stages: { parse, alg, outer, expiry, custom, inner }
}2
3
4
5
6
7
8
9
10
结果码 VerifyCode:
| code | 含义 |
|---|---|
| 0 | 验证通过(外层 + 内层) |
| 1 | 外层通过,device_id 为空跳过内层 |
| 10 | Token 为空 |
| 11 | JWT 解析失败 |
| 12 | alg 不是 NSP-SM |
| 20 | SM2 签名不匹配 |
| 21 | Token 已过期 |
| 30 | custom 字段缺失 |
| 31 | device_id 非空但 hmac 为空 |
| 32 | 需要会话密钥 |
| 33 | 内层 HMAC 不匹配 |
国密算法 API
// SM3 哈希
sm3('abc') // → '66c7f0f4...'
// SM3-HMAC
sm3Hmac(keyHex, messageBytes)
// SM2
sm2GenerateKeyPair() // → { privateKey, publicKey, publicKeyNoPrefix }
sm2Encrypt('hello', publicKey) // → ciphertextHex
sm2Decrypt(ciphertextHex, privateKey) // → 'hello'
sm2Sign(data, privateKey) // → signatureHex
sm2Verify(data, signatureHex, publicKey) // → true/false
// SM4-CBC
sm4CbcEncrypt('hello', keyHex, ivHex) // → ciphertextHex
sm4CbcDecrypt(ciphertextHex, keyHex, ivHex) // → 'hello'2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JSBridge 封装(鸿蒙 Pad WebView 原生能力)
场景:当页面运行在鸿蒙 Pad WebView 中时,原生客户端通过 window.jsbridgeHandle 暴露以下方法供 H5 调用: login / getAccess / refresh / encrypt / decrypt / logout(见 Pad 端 qxj_harmony_next_pad_nsp_browser/common/src/main/ets/util/JsBridgeHandle.ets 的 asyncMethodList)。
本 SDK 把这些原始方法包装为结构化结果对象 { code, message, data? } / { code, message, accessToken? } / { code, message },错误码与老项目 SDK 文档对齐,便于上层业务统一处理。
重要约定:所有 JSBridge 函数返回的 Promise 在任何情况下都不会被 reject(包括业务逻辑错误等),无论成功与否都会 resolve 一个包含 code 字段的对象。
通用:检测可用性
import { isJsBridgeAvailable } from 'qxj-frontend-sdk'
if (isJsBridgeAvailable()) {
// 运行在 Pad WebView 中,可调用加密登录等
} else {
// 普通浏览器,走降级流程
}2
3
4
5
6
7
1. 前端加密登录接口:jsBridgeLogin
功能:网页只负责把账密(及验证码/位置)传给客户端,客户端统一完成: 组装明文 JSON → SM4-GCM 加密 → POST 帧到后端 → 解密响应 → 持久化 token。 网页侧无需接触 K_client / 帧格式 / SM4,登录成功后用 jsBridgeGetAccess() 拿 token 即可。
加密流程在原生层完成,会话密钥 K_client 底层基于 SM2 国密握手协商获得, 由 Asset Store Kit(ASSET)在 TEE 中自动生成并安全存储。IV 使用鸿蒙官方 随机数接口生成。若网页未传 position,客户端会主动调高德定位 SDK 补齐 "经度,纬度"字符串填入登录明文。
调用场景:用户在 Pad WebView 中点击"登录"时调用(专用设备加密登录 模式)。
函数原型:
async function jsBridgeLogin(
username: string,
password: string,
verificationCode?: string,
position?: string,
): Promise<JsBridgeLoginResult>2
3
4
5
6
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
username | string | 用户名(必填)。 |
password | string | 密码(必填)。 |
verificationCode | string | 短信验证码(可选,默认空串)。网页管理端登录可空。 |
position | string | 位置坐标字符串(可选,如 "116.397,39.908")。默认空串,客户端会主动获取定位补齐。 |
返回值:
interface JsBridgeLoginResult {
success: boolean // 是否登录成功
access: string // Access Token(失败为空)
refresh: string // Refresh Token(失败为空)
message: string // 错误描述
}2
3
4
5
6
成功时 success=true,access/refresh 返回 token;失败时 success=false, access/refresh 为空,message 为错误描述。
示例:
import { jsBridgeLogin } from 'qxj-frontend-sdk'
const result = await jsBridgeLogin('admin', 'Nsp123456!')
if (result.success) {
console.log('登录成功', result.access, result.refresh)
} else {
console.error('登录失败', result.message)
}2
3
4
5
6
7
8
注意事项:
- 调用前需确保已完成 SM2 国密握手(Pad 端注册了
K_client); - 登录响应由 Pad 端
EncryptedLoginUtil处理,token 自动持久化到 PersistentStorage,网页侧无需自行存储。
2. 前端明文数据封装接口:jsBridgeEncrypt
功能:对传入的明文数据进行加密,明文与加密结果均以 hex 字符串形式 传递。加密流程在原生层完成,采用 SM4 算法 GCM 模式,会话密钥底层基于 SM2 国密握手协商获得,密钥存储由 Asset Store Kit(ASSET)在 TEE 中自动 生成并安全存储,无需业务侧传入。IV 使用鸿蒙官方随机数接口生成,确保每次 IV 值不同。
调用场景:对用户敏感请求信息上报前加密。
函数原型:
async function jsBridgeEncrypt(plainText: string): Promise<CryptoResult>
interface CryptoResult {
code: number
message: string
data?: string
}2
3
4
5
6
7
参数:plainText(string)——待加密的明文字符串。
返回值:无论成功或失败均 resolve 结构化对象。成功 code=0,data 为加密认证后的 hex 字符串(含 64B 帧头 + 密文 + 16B GCM tag);失败 code 不为 0,message 为错误描述,data 不存在。
| code | 含义 | 说明 |
|---|---|---|
| 0 | SUCCESS | 加密成功 |
| -11 | ERROR_EMPTY_INPUT | 传入的明文为空或空字符串 |
| -12 | ERROR_INPUT_TOO_LONG | 明文超过安全长度限制 |
| -14 | ERROR_ENCRYPT_FAILED | 底层加密库调用失败(含密钥不可用、TEE 异常等) |
| -15 | ERROR_BRIDGE_NOT_READY | JSBridge 未就绪(未检测到 window.jsbridgeHandle) |
| -99 | ERROR_UNKNOWN | 未知错误 |
import { jsBridgeEncrypt } from 'qxj-frontend-sdk'
const r = await jsBridgeEncrypt('hello world')
if (r.code === 0) {
console.log('密文:', r.data)
} else {
console.error(r.code, r.message)
}2
3
4
5
6
7
8
该函数为异步操作,Promise 永不 reject。同一份明文每次调用结果都不相同 (因 IV 随机),属正常现象。
3. 前端密文数据解封装接口:jsBridgeDecrypt
功能:对传入的密文数据进行解密,密文与解密结果以字符串形式传递。 解密流程在原生层完成,IV 从安全头部获得——虽然每次 IV 不同,但可以确保 每次解密出来的结果相同。
async function jsBridgeDecrypt(cipherText: string): Promise<CryptoResult>参数:cipherText(string)——待解密的密文 hex 字符串(64B 帧头 + 密文 + 16B GCM tag)。
返回值:成功 code=0,data 为解密后的明文字符串。
| code | 含义 | 说明 |
|---|---|---|
| 0 | SUCCESS | 解密成功 |
| -21 | ERROR_INVALID_CIPHERTEXT | 密文格式错误、非本算法产生或已被篡改 |
| -22 | ERROR_DECRYPT_AUTH_FAILED | 完整性校验失败(GCM tag 不匹配或密钥不可用) |
| -25 | ERROR_DECRYPT_FAILED | 底层解密库调用失败 |
| -15 | ERROR_BRIDGE_NOT_READY | JSBridge 未就绪 |
| -99 | ERROR_UNKNOWN | 未知错误 |
import { jsBridgeDecrypt } from 'qxj-frontend-sdk'
const r = await jsBridgeDecrypt(cipherHex)
if (r.code === 0) {
console.log('明文:', r.data)
}2
3
4
5
6
4. 前端获取访问令牌接口:jsBridgeGetAccess
功能:从原生客户端安全获取当前登录会话的短期访问令牌(Access Token)。Access Token 由原生客户端安全存储于 Asset Store Kit(ASSET), 由客户端负责生命周期管理。该接口只负责读取当前有效 Access Token 并返回 给网页使用;如果失效需要网页端执行刷新操作(jsBridgeRefresh)。
调用场景:HTTP 请求拦截器在专用设备加密登录模式下,每次请求前异步 调用本接口获取最新 Access Token。Access Token 生命周期较短,网页获取 Token 后应立即用于请求。
async function jsBridgeGetAccess(): Promise<TokenResult>
interface TokenResult {
code: number
message: string
accessToken?: string
}2
3
4
5
6
7
参数:无。
| code | 含义 | 说明 |
|---|---|---|
| 0 | SUCCESS | 获取成功 |
| -31 | ERROR_TOKEN_NOT_FOUND | 未找到 Access Token(未登录或已登出) |
| -15 | ERROR_BRIDGE_NOT_READY | JSBridge 未就绪 |
| -99 | ERROR_UNKNOWN | 未知错误 |
import { jsBridgeGetAccess } from 'qxj-frontend-sdk'
const r = await jsBridgeGetAccess()
if (r.code === 0) {
headers.Authorization = `Bearer ${r.accessToken}`
}2
3
4
5
6
该接口不保证返回时 Access Token 一定有效,调用方应在收到 401 时调用
jsBridgeRefresh刷新。
5. 前端令牌刷新接口:jsBridgeRefresh
功能:通知原生客户端使用 Refresh Token 刷新 Access Token。当网页 检测到 Access Token 过期或即将过期时调用(典型:后端返回 401/TokenExpired)。Refresh Token 由原生客户端安全存储,网页无法直接 访问。刷新成功后客户端会使用 Refresh Token 向认证服务器请求新的 Access Token,同时更新客户端本地 Token 存储,然后返回新的 Access Token 给网页。
async function jsBridgeRefresh(): Promise<TokenResult>返回结构同 jsBridgeGetAccess。
| code | 含义 | 说明 |
|---|---|---|
| 0 | SUCCESS | 刷新成功 |
| -41 | ERROR_REFRESH_TOKEN_NOT_FOUND | 未找到 Refresh Token(未登录/已登出/已过期) |
| -43 | ERROR_NETWORK_FAILED | 网络请求失败 |
| -15 | ERROR_BRIDGE_NOT_READY | JSBridge 未就绪 |
| -99 | ERROR_UNKNOWN | 未知错误 |
Refresh Token 仅存储在原生客户端安全区域,网页侧无法直接访问;若 Refresh Token 过期,需重新调用
jsBridgeLogin完成登录。
6. 前端退出登录接口:jsBridgeLogout
功能:执行用户退出登录操作,并清除客户端保存的所有认证信息。退出 流程包括:清空本地 Access Token、清空 Refresh Token 以及关闭当前登录 会话。服务器失败不阻断本地清除。
async function jsBridgeLogout(): Promise<CommonResult>
interface CommonResult {
code: number
message: string
}2
3
4
5
6
| code | 含义 | 说明 |
|---|---|---|
| 0 | SUCCESS | 退出成功 |
| -51 | ERROR_TOKEN_CLEAR_FAILED | Token 清理失败 |
| -15 | ERROR_BRIDGE_NOT_READY | JSBridge 未就绪 |
| -99 | ERROR_UNKNOWN | 未知错误 |
退出登录会清除所有本地认证信息,然后跳转至登录页面;切换用户登录也应 同时调用本接口清理当前账号会话。
错误码总览
所有 JSBridge 错误码集中在 JsBridgeCode 常量对象中:
import { JsBridgeCode, describeJsBridgeCode } from 'qxj-frontend-sdk'
console.log(JsBridgeCode.SUCCESS) // 0
console.log(JsBridgeCode.ERROR_EMPTY_INPUT) // -11
console.log(describeJsBridgeCode(-11)) // '传入的明文为空或空字符串'2
3
4
5
| 范围 | 接口 | 说明 |
|---|---|---|
| 0 | 全部 | SUCCESS |
| -11..-15 | jsBridgeEncrypt | 加密相关错误码 |
| -21..-25 | jsBridgeDecrypt | 解密相关错误码 |
| -31 | jsBridgeGetAccess | 获取 Access Token 错误码 |
| -41..-44 | jsBridgeRefresh | 刷新 Access Token 错误码 |
| -51 | jsBridgeLogout | 退出登录错误码 |
| -15 | 全部 | JSBridge 未就绪(可跨接口返回) |
| -99 | 全部 | 未知错误 |
对外能力总览(src)
- 国密:sm2GenerateKeyPair / sm2Encrypt / sm2Decrypt / sm2Sign / sm2Verify、sm3 / sm3Hmac、sm4CbcEncrypt / Decrypt(底层 sm-crypto 0.3.3);
- JWT:parseJwt、verifyJwt(外层 SM2 + 内层 SM3-HMAC)、VerifyCode、 CODE_DESC;
- JSBridge:6 个封装函数 + JsBridgeCode / describeJsBridgeCode / isJsBridgeAvailable;
- 工具:hex/bytes、packUint64BE、UTF-8、base64url;
init(lib?):加载 sm-crypto(node 从 node_modules;浏览器先查window.smCrypto,否则动态注入 CDN 脚本)。
构建(build.js,esbuild)
npm install
npm run build # 生成 dist/index.cjs 和 dist/qxj-sdk.umd.js
npm test # 运行测试2
3
- 三产物:
dist/index.cjs(node,sm-crypto 外置)、dist/index.mjs(browser,内联单文件)、dist/qxj-sdk.umd.js(IIFE,全局QxjSdk); 类型由src/index.d.ts拷贝; - 混淆:
build:min/MINIFY=true/ FORCE_MINIFY——esbuild minify 仅提高阅读门槛; - 发布包无明文 src:
files:["dist"],npm pack出qxj-frontend-sdk-0.2.3.tgz(仅 package.json/README/dist,约 52KB)。
消费方
- qxj-frontend-admin:vendor 0.2.3 tgz(登录/HTTP 层/路由守卫/user store);
- qxj-frontend-mock-demo/after:vendor 0.2.3 tgz;
- 也可 UMD
<script>全局使用(QxjSdk)。
与后端的对应关系
| 后端 (Python) | 前端 (JS) |
|---|---|
qxj_backend_sdk.verify_access_token | verifyJwt() |
qxj_backend_sdk.sm4_gcm_encrypt | jsBridgeEncrypt()(原生层) |
qxj_backend_sdk.sm4_gcm_decrypt | jsBridgeDecrypt()(原生层) |
qxj_backend_sdk_auth.TokenVerifier | verifyJwt() |
qxj_backend_sdk_cryptography.sm2_verify | sm2Verify() |
qxj_backend_sdk_cryptography.sm3_hmac | sm3Hmac() |
qxj_backend_sdk_cryptography.sm4_cbc_* | sm4Cbc*() |
相关文档
- 前端 SDK API 参考
- Python 后端 SDK
- README 约 520 行:API、VerifyCode 表、6 个 JSBridge 函数详解、错误码 总览、构建说明,以及与 Python 后端 SDK 的 API 对应关系表