苹果4.3(a)被拒怎么办?码尚友科技分享IPA相似度检测经验
很多开发者认为,苹果反馈 4.3(a) 之后,只要换个图标、改个名称、调整几个页面就能重新提交。
但实际审核过程中,大量应用即使已经更换 UI,依然会再次收到 4.3(a) 拒绝。
根据码尚友科技近两年处理的上架案例统计,苹果对于应用相似性的判断,早已不仅限于界面层面,而是从代码结构、资源文件、业务逻辑、提交环境等多个维度进行综合分析。
苹果4.3(a)到底在检测什么?
苹果审核团队希望每个应用都具备独立价值,而不是简单复制已有产品。
从实际案例来看,相似性检测主要集中在以下几个维度:
检测维度
权重占比(经验数据)
常见问题
代码结构
35%
相同工程模板、相同类结构
资源文件
25%
图片、音频、字体高度重复
功能逻辑
20%
页面流程一致
账号关联
10%
开发者账号存在关联
提交环境
10%
IP、设备环境异常
根据码尚友上架系统统计,在近1000+个处理案例中,仅修改UI但未优化代码结构的项目,二次收到4.3(a)反馈的比例超过72%。
为什么改了界面还是会收到4.3(a)?
很多开发团队只修改:
Logo
App名称
首页配色
部分文案
但实际上:
Controller结构未变化
API调用逻辑未变化
资源目录未变化
工程架构未变化
苹果机审依然能够识别出较高相似度。
因此:
UI差异 ≠ 应用差异
码尚友检测系统发现的高风险项
在项目提交前,我们会通过内部IPA分析工具进行检测。
重点检查:
1、代码相似度分析
检测内容:
类名结构
方法调用链
Framework引用情况
工程目录结构
示例:
检测项
风险等级
类名重复率85%
高风险
页面结构重复率78%
高风险
Framework一致率92%
高风险
图上是2个不相关的APP,所以相似度特别低,事实证明,相似度检测的确可以检测出2个app是否有关联,是否会被4.3a被拒
2、资源包检测
系统会扫描:
图片Hash值
音频文件
视频资源
字体资源
部分项目虽然更换了图片名称,但实际文件内容未变,依然会被识别。
3、业务流程分析
重点分析:
页面跳转路径
功能触发逻辑
用户使用流程
例如:
A应用:
首页 → 会员 → AI生成 → 保存
B应用:
首页 → VIP → AI创作 → 导出
虽然名称不同,但业务路径完全一致,仍然存在较高相似风险。
码尚友科技的解决思路
针对4.3(a)问题,我们通常从四个层面进行优化:
第一层:代码层优化
工程结构重组
模块重新划分
方法逻辑重构
类结构调整
第二层:资源层优化
图片重新设计
资源目录重建
字体资源调整
启动页重构
第三层:业务层优化
增加独立功能模块
调整用户路径
增加业务价值点
第四层:提交层优化
提交环境隔离
审核资料检测
开发者账号评估
历史版本关联分析
数据统计
根据码尚友科技2025-2026年上架案例统计:
项目类型
初次通过率
仅修改UI
28%
UI+资源优化
47%
UI+资源+代码优化
76%
完整检测后提交
结语
苹果4.3(a)并不是简单的“换壳检测”。
从近年的审核趋势来看,苹果对于代码结构、资源内容、业务逻辑以及开发环境的综合分析越来越严格。
如果项目已经连续收到4.3(a)反馈,建议在重新提交前进行完整的IPA检测和风险评估,而不是仅修改图标或界面。
码尚友科技自主研发的IPA检测系统,可针对代码结构、资源文件、业务逻辑及提交环境进行多维度分析,帮助开发者提前发现高风险项,提高审核通过率。
很多开发者认为,苹果反馈 4.3(a) 之后,只要换个图标、改个名称、调整几个页面就能重新提交。
但实际审核过程中,大量应用即使已经更换 UI,依然会再次收到 4.3(a) 拒绝。
根据码尚友科技近两年处理的上架案例统计,苹果对于应用相似性的判断,早已不仅限于界面层面,而是从代码结构、资源文件、业务逻辑、提交环境等多个维度进行综合分析。
苹果4.3(a)到底在检测什么?
苹果审核团队希望每个应用都具备独立价值,而不是简单复制已有产品。
从实际案例来看,相似性检测主要集中在以下几个维度:
检测维度
权重占比(经验数据)
常见问题
代码结构
35%
相同工程模板、相同类结构
资源文件
25%
图片、音频、字体高度重复
功能逻辑
20%
页面流程一致
账号关联
10%
开发者账号存在关联
提交环境
10%
IP、设备环境异常
根据码尚友上架系统统计,在近1000+个处理案例中,仅修改UI但未优化代码结构的项目,二次收到4.3(a)反馈的比例超过72%。
为什么改了界面还是会收到4.3(a)?
很多开发团队只修改:
Logo
App名称
首页配色
部分文案
但实际上:
Controller结构未变化
API调用逻辑未变化
资源目录未变化
工程架构未变化
苹果机审依然能够识别出较高相似度。
因此:
UI差异 ≠ 应用差异
码尚友检测系统发现的高风险项
在项目提交前,我们会通过内部IPA分析工具进行检测。
重点检查:
1、代码相似度分析
检测内容:
类名结构
方法调用链
Framework引用情况
工程目录结构
示例:
检测项
风险等级
类名重复率85%
高风险
页面结构重复率78%
高风险
Framework一致率92%
高风险
图上是2个不相关的APP,所以相似度特别低,事实证明,相似度检测的确可以检测出2个app是否有关联,是否会被4.3a被拒
2、资源包检测
系统会扫描:
图片Hash值
音频文件
视频资源
字体资源
部分项目虽然更换了图片名称,但实际文件内容未变,依然会被识别。
3、业务流程分析
重点分析:
页面跳转路径
功能触发逻辑
用户使用流程
例如:
A应用:
首页 → 会员 → AI生成 → 保存
B应用:
首页 → VIP → AI创作 → 导出
虽然名称不同,但业务路径完全一致,仍然存在较高相似风险。
码尚友科技的解决思路
针对4.3(a)问题,我们通常从四个层面进行优化:
第一层:代码层优化
工程结构重组
模块重新划分
方法逻辑重构
类结构调整
第二层:资源层优化
图片重新设计
资源目录重建
字体资源调整
启动页重构
第三层:业务层优化
增加独立功能模块
调整用户路径
增加业务价值点
第四层:提交层优化
提交环境隔离
审核资料检测
开发者账号评估
历史版本关联分析
数据统计
根据码尚友科技2025-2026年上架案例统计:
项目类型
初次通过率
仅修改UI
28%
UI+资源优化
47%
UI+资源+代码优化
76%
完整检测后提交
结语
苹果4.3(a)并不是简单的“换壳检测”。
从近年的审核趋势来看,苹果对于代码结构、资源内容、业务逻辑以及开发环境的综合分析越来越严格。
如果项目已经连续收到4.3(a)反馈,建议在重新提交前进行完整的IPA检测和风险评估,而不是仅修改图标或界面。
码尚友科技自主研发的IPA检测系统,可针对代码结构、资源文件、业务逻辑及提交环境进行多维度分析,帮助开发者提前发现高风险项,提高审核通过率。
收起阅读 »uniapp x插件 SM2 SM3 SM4 AES RSA 加解密 签名 验签 客户端 服务端 全平台解决方案
sunrains-smutil 插件使用指南
本插件提供SM2、SM3 、 SM4、AES、RSA 客户端(ios,安卓,鸿蒙,微信小程序,h5),服务端(java)SM2、SM3 、 SM4、AES、RSA 算法的跨平台实现,支持以下功能:
SM2 非对称加密算法
✅ SM2 密钥对生成
✅ SM2 数字签名
✅ SM2 签名验证
✅ SM2 数据加密
✅ SM2 数据解密
SM3 密码杂凑算法
✅ SM3 哈希计算(字符串)
✅ HMAC-SM3 密钥哈希
✅ 文件 SM3 哈希
✅ 哈希值验证
SM4 对称加密算法
✅ SM4 密钥生成(含 IV)
✅ SM4 数据加密(CBC 模式)
✅ SM4 数据解密(CBC 模式)
AES 对称加密算法
✅ AES 密钥和 IV 生成
✅ AES 数据加密(CBC 模式, PKCS7/PKCS5 填充)
✅ AES 数据解密(CBC 模式, PKCS7/PKCS5 填充)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5/微信小程序)
RSA 非对称加密算法
✅ RSA 密钥对生成(2048位)
✅ RSA-OAEP-SHA256 数据加密
✅ RSA-OAEP-SHA256 数据解密
✅ SHA256withRSA 数字签名(PKCS#1 v1.5)
✅ SHA256withRSA 签名验证(PKCS#1 v1.5)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5)
⚠️ 微信小程序端暂不支持RSA-OAEP-SHA256加密解密,仅支持签名验签
调用方式
本插件同时支持回调风格和异步风格两种调用方式:
方式一:回调风格(传统方式)
import { sm2Sign } from "@/uni_modules/sunrains-util";
sm2Sign({
data: "需要签名的数据",
privateKey: "你的私钥",
success: (res) => {
console.log("签名结果:", res.signature);
},
fail: (err) => {
console.error("签名失败:", err);
}
});
方式二:异步风格(推荐,更简洁)
import { sm2SignAsync } from "@/uni_modules/sunrains-util";
async function doSign() {
try {
const result = await sm2SignAsync("需要签名的数据", "你的私钥")
console.log("签名结果:", result.signature)
} catch (error) {
console.error("签名失败:", error)
}
}
平台支持
| 平台 | SM2 实现方式 | SM4 实现方式 | AES 实现方式 | RSA 实现方式 | 状态 |
|---|---|---|---|---|---|
| Android | BouncyCastle | BouncyCastle | Java Crypto API | Java Crypto API (RSA-OAEP-SHA256) | ✅ 完全支持 |
| iOS | GMObjC | GMObjC | CommonCrypto | Security Framework (RSA-OAEP-SHA256) | ✅ 完全支持 |
| HarmonyOS | cryptoFramework | cryptoFramework | cryptoFramework | cryptoFramework (RSA-OAEP-SHA256) | ✅ 完全支持 |
| H5 | sm-crypto | sm-crypto | Web Crypto API | Web Crypto API (RSA-OAEP-SHA256) | ✅ 完全支持 |
| 微信小程序 | miniprogram-sm-crypto | miniprogram-sm-crypto | crypto-js | jsrsasign (仅签名验签) | ⚠️ 部分支持 |
使用示例
SM2 相关操作
1. 生成 SM2 密钥对
同步方式:
import { sm2GenerateKeyPair } from "@/uni_modules/sunrains-util";
const result = sm2GenerateKeyPair();
console.log("公钥:", result.publicKey); // 04开头的130位十六进制字符串
console.log("私钥:", result.privateKey); // 64位十六进制字符串
异步方式:
import { sm2GenerateKeyPairAsync } from "@/uni_modules/sunrains-util";
async function generateKeys() {
const result = await sm2GenerateKeyPairAsync()
console.log("公钥:", result.publicKey)
console.log("私钥:", result.privateKey)
}
2. SM2 签名
回调风格:
import { sm2Sign } from "@/uni_modules/sunrains-util";
sm2Sign({
data: "需要签名的数据",
privateKey: "你的私钥",
success: (res) => {
console.log("签名结果:", res.signature);
},
fail: (err) => {
console.error("签名失败:", err);
}
});
异步风格(推荐):
import { sm2SignAsync } from "@/uni_modules/sunrains-util";
async function doSign() {
try {
const result = await sm2SignAsync("需要签名的数据", "你的私钥")
console.log("签名结果:", result.signature)
} catch (error) {
console.error("签名失败:", error)
}
}
3. SM2 验签
回调风格:
import { sm2Verify } from "@/uni_modules/sunrains-util";
sm2Verify({
data: "原始数据",
signature: "签名值",
publicKey: "你的公钥",
success: (res) => {
console.log("验签结果:", res.valid); // true 或 false
},
fail: (err) => {
console.error("验签失败:", err);
}
});
异步风格(推荐):
import { sm2VerifyAsync } from "@/uni_modules/sunrains-util";
async function doVerify() {
try {
const result = await sm2VerifyAsync("原始数据", "签名值", "你的公钥")
console.log("验签结果:", result.valid) // true 或 false
} catch (error) {
console.error("验签失败:", error)
}
}
完整实战示例
以下是一个完整的国密算法测试页面示例,及服务端(java)工具类,展示了所有功能的实际使用:
客户端 Vue 3 Composition API 示例
<template>
<view class="order-content">
<view class="order-header">
<text class="order-title">国密算法测试</text>
</view>
<scroll-view class="order-list" scroll-y="true">
<view class="section">
<view class="section-title">SM4 对称加密</view>
<view class="input-group">
<text class="label">待加密内容:</text>
<input v-model="sm4Content" placeholder="请输入要加密的内容" class="input" />
</view>
<button @click="testSm4Encrypt" class="btn btn-primary">SM4 加密</button>
<view class="result-box" v-if="sm4Key.key">
<text class="result-label">密钥 (key):</text>
<text selectable="true" class="result-value">{{ sm4Key.key }}</text>
</view>
<view class="result-box" v-if="sm4Key.iv">
<text class="result-label">初始向量 (iv):</text>
<text selectable="true" class="result-value">{{ sm4Key.iv }}</text>
</view>
<view class="result-box" v-if="sm4Encrypted">
<text class="result-label">加密结果:</text>
<text selectable="true" class="result-value">{{ sm4Encrypted }}</text>
</view>
<button @click="testSm4Decrypt" :disabled="sm4Encrypted.length==0" class="btn btn-secondary">SM4 解密</button>
<button @click="testSm4Decrypt" :disabled="sm4Encrypted === ''" :class="sm4Encrypted === '' ? 'btn btn-secondary disabled' : 'btn btn-secondary'" class="btn btn-secondary">SM4 解密</button>
<view class="result-box" v-if="sm4Decrypted">
<text class="result-label">解密结果:</text>
<text selectable="true" class="result-value success">{{ sm4Decrypted }}</text>
</view>
</view>
<view class="section">
<view class="section-title">SM2 数字签名</view>
<view class="input-group">
<text class="label">待签名内容:</text>
<input v-model="sm2SignContent" placeholder="请输入要签名的内容" class="input" />
</view>
<button @click="testSm2Sign" class="btn btn-primary">生成密钥对并签名</button>
<view class="result-box" v-if="sm2KeyPair.publicKey">
<text class="result-label">公钥:</text>
<text selectable="true" class="result-value small">{{ sm2KeyPair.publicKey }}</text>
</view>
<view class="result-box" v-if="sm2KeyPair.privateKey">
<text class="result-label">私钥:</text>
<text selectable="true" class="result-value small">{{ sm2KeyPair.privateKey }}</text>
</view>
<view class="result-box" v-if="sm2Signature">
<text class="result-label">签名结果:</text>
<text selectable="true" class="result-value small">{{ sm2Signature }}</text>
</view>
<button @click="testSm2Verify" :disabled="sm2Signature.length==0" class="btn btn-secondary">验签</button>
<view class="result-box" v-if="sm2VerifyResult !== null">
<text class="result-label">验签结果:</text>
<text class="result-value" :class="sm2VerifyResult ==true? 'success' : 'error'">
{{ sm2VerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}
</text>
</view>
</view>
<view class="section">
<view class="section-title">SM2 非对称加密</view>
<view class="input-group">
<text class="label">待加密内容:</text>
<input v-model="sm2EncryptContent" placeholder="请输入要加密的内容" class="input" />
</view>
<button @click="testSm2Encrypt" class="btn btn-primary">SM2 加密</button>
<view class="result-box" v-if="sm2EncryptKeyPair.publicKey">
<text class="result-label">公钥:</text>
<text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.publicKey }}</text>
</view>
<view class="result-box" v-if="sm2EncryptKeyPair.privateKey">
<text class="result-label">私钥:</text>
<text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.privateKey }}</text>
</view>
<view class="result-box" v-if="sm2Encrypted">
<text class="result-label">加密结果:</text>
<text selectable="true" class="result-value small">{{ sm2Encrypted }}</text>
</view>
<button @click="testSm2Decrypt" :disabled="sm2Encrypted.length==0" class="btn btn-secondary">SM2 解密</button>
<view class="result-box" v-if="sm2Decrypted">
<text class="result-label">解密结果:</text>
<text selectable="true" class="result-value success">{{ sm2Decrypted }}</text>
</view>
</view>
<view class="section">
<view class="section-title">SM3 哈希算法</view>
<!-- 1. SM3 哈希 -->
<view class="input-group">
<text class="label">待哈希内容:</text>
<input v-model="sm3Content" placeholder="请输入要哈希的内容" class="input" />
</view>
<button @click="testSm3Hash" class="btn btn-primary">SM3 哈希</button>
<view class="result-box" v-if="sm3HashResult">
<text class="result-label">SM3 哈希值:</text>
<text selectable="true" class="result-value small">{{ sm3HashResult }}</text>
</view>
<!-- 2. SM3 哈希验证 -->
<view class="divider"></view>
<view class="sub-title">SM3 哈希验证</view>
<view class="input-group">
<text class="label">验证 - 原始内容:</text>
<input v-model="sm3VerifyContent" placeholder="请输入原始内容" class="input" />
</view>
<button @click="testSm3Verify" :disabled="sm3VerifyContent.length == 0" class="btn btn-secondary">验证哈希值</button>
<view class="result-box" v-if="sm3HashMatchResult !== null">
<text class="result-label">哈希匹配结果:</text>
<text class="result-value" :class="sm3HashMatchResult == true ? 'success' : 'error'">
{{ sm3HashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}
</text>
</view>
<!-- 3. HMAC-SM3 哈希 -->
<view class="divider"></view>
<view class="sub-title">HMAC-SM3 哈希</view>
<view class="input-group">
<text class="label">HMAC-SM3 待哈希内容:</text>
<input v-model="sm3HmacContent" placeholder="请输入要哈希的内容" class="input" />
</view>
<view class="input-group">
<text class="label">HMAC-SM3 密钥(HEX):</text>
<input v-model="sm3HmacKey" placeholder="请输入HMAC密钥(HEX格式)" class="input" />
</view>
<button @click="testSm3HmacHash" :disabled="sm3HmacContent.length == 0 || sm3HmacKey.length == 0" class="btn btn-primary">HMAC-SM3 哈希</button>
<view class="result-box" v-if="sm3HmacHashResult">
<text class="result-label">HMAC-SM3 哈希值:</text>
<text selectable="true" class="result-value small">{{ sm3HmacHashResult }}</text>
</view>
<!-- 4. HMAC-SM3 哈希验证 -->
<view class="divider"></view>
<view class="sub-title">HMAC-SM3 哈希验证</view>
<view class="input-group">
<text class="label">验证 - 原始内容:</text>
<input v-model="sm3HmacVerifyContent" placeholder="请输入原始内容" class="input" />
</view>
<view class="input-group">
<text class="label">验证 - HMAC 密钥:</text>
<input v-model="sm3HmacVerifyKey" placeholder="请输入HMAC密钥" class="input" />
</view>
<view class="input-group">
<text class="label">验证 - 期望哈希值:</text>
<input v-model="sm3HmacExpectedHash" placeholder="请输入期望的哈希值" class="input" />
</view>
<button @click="testSm3HmacHashWithExpected" :disabled="sm3HmacVerifyContent.length == 0 || sm3HmacVerifyKey.length == 0 || sm3HmacExpectedHash.length == 0" class="btn btn-secondary">验证 HMAC-SM3 哈希值</button>
<view class="result-box" v-if="sm3HmacHashMatchResult !== null">
<text class="result-label">HMAC-SM3 哈希匹配结果:</text>
<text class="result-value" :class="sm3HmacHashMatchResult == true ? 'success' : 'error'">
{{ sm3HmacHashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}
</text>
</view>
<!-- 5. 文件 SM3 哈希 -->
<view class="divider"></view>
<view class="sub-title">文件 SM3 哈希</view>
<button @click="chooseImageAndHash" class="btn btn-primary">选择图片并计算 SM3</button>
<button @click="chooseFileAndHash" class="btn btn-primary">选择文件并计算 SM3</button>
<view class="result-box" v-if="sm3FileName">
<text class="result-label">文件名:</text>
<text class="result-value">{{ sm3FileName }}</text>
</view>
<view class="result-box" v-if="sm3FileHashResult">
<text class="result-label">文件 SM3 哈希值:</text>
<text selectable="true" class="result-value small">{{ sm3FileHashResult }}</text>
</view>
</view>
<view class="section">
<view class="section-title">AES 对称加密</view>
<view class="input-group">
<text class="label">待加密内容:</text>
<input v-model="aesContent" placeholder="请输入要加密的内容" class="input" />
</view>
<button @click="testAesEncrypt" class="btn btn-primary">AES 加密</button>
<view class="result-box" v-if="aesKey.key">
<text class="result-label">密钥 (key):</text>
<text selectable="true" class="result-value">{{ aesKey.key }}</text>
</view>
<view class="result-box" v-if="aesKey.iv">
<text class="result-label">初始向量 (iv):</text>
<text selectable="true" class="result-value">{{ aesKey.iv }}</text>
</view>
<view class="result-box" v-if="aesEncrypted">
<text class="result-label">加密结果:</text>
<text selectable="true" class="result-value">{{ aesEncrypted }}</text>
</view>
<button @click="testAesDecrypt" :disabled="aesEncrypted.length==0" class="btn btn-secondary">AES 解密</button>
<view class="result-box" v-if="aesDecrypted">
<text class="result-label">解密结果:</text>
<text selectable="true" class="result-value success">{{ aesDecrypted }}</text>
</view>
</view>
<view class="section">
<view class="section-title">RSA 非对称加密</view>
<view class="input-group">
<text class="label">待加密内容:</text>
<input v-model="rsaContent" placeholder="请输入要加密的内容" class="input" />
</view>
<button @click="testRsaEncrypt" class="btn btn-primary">RSA 加密</button>
<view class="result-box" v-if="rsaKeyPair.publicKey">
<text class="result-label">公钥:</text>
<text selectable="true" class="result-value small">{{ rsaKeyPair.publicKey }}</text>
</view>
<view class="result-box" v-if="rsaKeyPair.privateKey">
<text class="result-label">私钥:</text>
<text selectable="true" class="result-value small">{{ rsaKeyPair.privateKey }}</text>
</view>
<view class="result-box" v-if="rsaEncrypted">
<text class="result-label">加密结果:</text>
<text selectable="true" class="result-value small">{{ rsaEncrypted }}</text>
</view>
<button @click="testRsaDecrypt" :disabled="rsaEncrypted.length==0" class="btn btn-secondary">RSA 解密</button>
<view class="result-box" v-if="rsaDecrypted">
<text class="result-label">解密结果:</text>
<text selectable="true" class="result-value success">{{ rsaDecrypted }}</text>
</view>
</view>
<view class="section">
<view class="section-title">RSA 数字签名</view>
<view class="input-group">
<text class="label">待签名内容:</text>
<input v-model="rsaSignContent" placeholder="请输入要签名的内容" class="input" />
</view>
<button @click="testRsaSign" class="btn btn-primary">生成密钥对并签名</button>
<view class="result-box" v-if="rsaSignKeyPair.publicKey">
<text class="result-label">公钥:</text>
<text selectable="true" class="result-value small">{{ rsaSignKeyPair.publicKey }}</text>
</view>
<view class="result-box" v-if="rsaSignKeyPair.privateKey">
<text class="result-label">私钥:</text>
<text selectable="true" class="result-value small">{{ rsaSignKeyPair.privateKey }}</text>
</view>
<view class="result-box" v-if="rsaSignature">
<text class="result-label">签名结果:</text>
<text selectable="true" class="result-value small">{{ rsaSignature }}</text>
</view>
<button @click="testRsaVerify" :disabled="rsaSignature.length==0" class="btn btn-secondary">验签</button>
<view class="result-box" v-if="rsaVerifyResult !== null">
<text class="result-label">验签结果:</text>
<text class="result-value" :class="rsaVerifyResult ==true? 'success' : 'error'">
{{ rsaVerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}
</text>
</view>
</view>
<view class="order-item" @click="goToDetail(1002, '已完成')">
<view class="order-info">
<text class="order-id">订单编号:1002</text>
<text class="order-status">状态:已完成</text>
</view>
<view class="order-btn">
<text class="go-detail">查看详情</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import {IdcardOcrModel}from "@/uni_modules/sunrains-idcard-ocr/utssdk/interface.uts"
import {Response} from "@/uni_modules/sunrains-utils/index.uts"
import * as SM from "@/uni_modules/sunrains-smutil"
// ==================== SM4 相关数据 ====================
const sm4Content = ref("Hello World")
const sm4Key = ref<SM.Sm4Key>({ key: '', iv: '' })
const sm4Encrypted = ref("")
const sm4Decrypted = ref("")
// ==================== SM2 签名验签相关数据 ====================
const sm2SignContent = ref("123")
const sm2KeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })
const sm2Signature = ref("")
const sm2VerifyResult = ref<boolean | null>(null)
// ==================== SM2 加密解密相关数据 ====================
const sm2EncryptContent = ref("Hello SM2")
const sm2EncryptKeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })
const sm2Encrypted = ref("")
const sm2Decrypted = ref("")
// ==================== SM3 相关数据 ====================
const sm3Content = ref("Hello SM3")
const sm3HashResult = ref("")
const sm3VerifyContent = ref("")
const sm3HashMatchResult = ref<boolean | null>(null)
const sm3HmacContent = ref("Hello HMAC-SM3")
const sm3HmacKey = ref("0123456789abcdef0123456789abcdef") // 默认 HMAC 密钥(32字节HEX)
const sm3HmacHashResult = ref("")
const sm3HmacVerifyContent = ref("")
const sm3HmacVerifyKey = ref("")
const sm3HmacExpectedHash = ref("")
const sm3HmacHashMatchResult = ref<boolean | null>(null)
const sm3VerifyResult = ref<boolean|null>(null)
const sm3FileName = ref("")
const sm3FileHashResult = ref("")
// ==================== AES 相关数据 ====================
const aesContent = ref("Hello AES")
const aesKey = ref<SM.AesKey>({ key: '', iv: '' })
const aesEncrypted = ref("")
const aesDecrypted = ref("")
// ==================== RSA 加密解密相关数据 ====================
const rsaContent = ref("Hello RSA")
const rsaKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })
const rsaEncrypted = ref("")
const rsaDecrypted = ref("")
// ==================== RSA 签名验签相关数据 ====================
const rsaSignContent = ref("需要签名的数据")
const rsaSignKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })
const rsaSignature = ref("")
const rsaVerifyResult = ref<boolean | null>(null)
// ==================== SM4 加密 ====================
async function testSm4Encrypt() {
if (sm4Content.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥
const key = await SM.sm4GenerateKeyAsync()
sm4Key.value = { key: key.key, iv: key.iv }
console.log("SM4 密钥:", key)
// 加密
const encryptResult = await SM.sm4EncryptAsync(sm4Content.value, key.key, key.iv)
sm4Encrypted.value = encryptResult.result
console.log("SM4 加密成功:", encryptResult.result)
uni.showToast({ title: '加密成功', icon: 'success' })
} catch (e) {
console.error("SM4 加密失败:", e)
uni.showToast({ title: '加密失败', icon: 'error' })
}
}
// ==================== SM4 解密 ====================
async function testSm4Decrypt() {
if (sm4Encrypted.value.length==0 || sm4Key.value.key.length==0) {
uni.showToast({ title: '请先加密', icon: 'none' })
return
}
try {
const decryptResult = await SM.sm4DecryptAsync(sm4Encrypted.value, sm4Key.value.key, sm4Key.value.iv)
sm4Decrypted.value = decryptResult.result
console.log("SM4 解密成功:", decryptResult.result)
if (decryptResult.result == sm4Content.value) {
uni.showToast({ title: '解密成功且内容一致', icon: 'success' })
} else {
uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })
}
} catch (e) {
console.error("SM4 解密失败:", e)
uni.showToast({ title: '解密失败', icon: 'error' })
}
}
// ==================== AES 加密 ====================
async function testAesEncrypt() {
if (aesContent.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥和 IV
const key = await SM.aesGenerateKeyAsync()
aesKey.value = { key: key.key, iv: key.iv }
console.log("AES 密钥:", key)
// 加密
const encryptResult = await SM.aesEncryptAsync(aesContent.value, key.key, key.iv)
aesEncrypted.value = encryptResult.result
console.log("AES 加密成功:", encryptResult.result)
uni.showToast({ title: '加密成功', icon: 'success' })
} catch (e) {
console.error("AES 加密失败:", e)
uni.showToast({ title: '加密失败', icon: 'error' })
}
}
// ==================== AES 解密 ====================
async function testAesDecrypt() {
if (aesEncrypted.value.length==0 || aesKey.value.key.length==0) {
uni.showToast({ title: '请先加密', icon: 'none' })
return
}
try {
const decryptResult = await SM.aesDecryptAsync(aesEncrypted.value, aesKey.value.key, aesKey.value.iv)
aesDecrypted.value = decryptResult.result
console.log("AES 解密成功:", decryptResult.result)
if (decryptResult.result == aesContent.value) {
uni.showToast({ title: '解密成功且内容一致', icon: 'success' })
} else {
uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })
}
} catch (e) {
console.error("AES 解密失败:", e)
uni.showToast({ title: '解密失败', icon: 'error' })
}
}
// ==================== RSA 加密 ====================
async function testRsaEncrypt() {
if (rsaContent.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥对
const keyPair = await SM.rsaGenerateKeyPairAsync()
rsaKeyPair.value = {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey
}
console.log("RSA 公钥:", keyPair.publicKey)
console.log("RSA 私钥:", keyPair.privateKey)
// 加密
const encryptResult = await SM.rsaEncryptAsync(rsaContent.value, keyPair.publicKey)
rsaEncrypted.value = encryptResult.result
console.log("RSA 加密结果:", encryptResult.result)
uni.showToast({ title: '加密成功', icon: 'success' })
} catch (e) {
console.error("RSA 加密失败:", e)
uni.showToast({ title: '加密失败', icon: 'error' })
}
}
// ==================== RSA 解密 ====================
async function testRsaDecrypt() {
if (rsaEncrypted.value.length==0 || rsaKeyPair.value.privateKey.length==0) {
uni.showToast({ title: '请先加密', icon: 'none' })
return
}
try {
const decryptResult = await SM.rsaDecryptAsync(rsaEncrypted.value, rsaKeyPair.value.privateKey)
rsaDecrypted.value = decryptResult.result
console.log("RSA 解密结果:", decryptResult.result)
if (decryptResult.result == rsaContent.value) {
uni.showToast({ title: '解密成功且内容一致', icon: 'success' })
} else {
uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })
}
} catch (e) {
console.error("RSA 解密异常:", e)
uni.showToast({ title: '解密异常', icon: 'error' })
}
}
// ==================== RSA 签名 ====================
async function testRsaSign() {
if (rsaSignContent.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥对
const keyPair = await SM.rsaGenerateKeyPairAsync()
rsaSignKeyPair.value = {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey
}
console.log("RSA 公钥:", keyPair.publicKey)
console.log("RSA 私钥:", keyPair.privateKey)
// 签名
const signResult = await SM.rsaSignAsync(rsaSignContent.value, keyPair.privateKey)
rsaSignature.value = signResult.signature
console.log("RSA 签名成功:", signResult.signature)
uni.showToast({ title: '签名成功', icon: 'success' })
} catch (e) {
console.error("RSA 签名失败:", e)
uni.showToast({ title: '签名失败', icon: 'error' })
}
}
// ==================== RSA 验签 ====================
async function testRsaVerify() {
if (rsaSignature.value.length==0 || rsaSignKeyPair.value.publicKey.length==0) {
uni.showToast({ title: '请先签名', icon: 'none' })
return
}
try {
// 验签
const verifyResult = await SM.rsaVerifyAsync(rsaSignContent.value, rsaSignature.value, rsaSignKeyPair.value.publicKey)
rsaVerifyResult.value = verifyResult.valid
console.log("RSA 验签结果:", verifyResult.valid)
if (verifyResult.valid) {
uni.showToast({ title: '验签成功', icon: 'success' })
} else {
uni.showToast({ title: '验签失败', icon: 'error' })
}
} catch (e) {
console.error("RSA 验签异常:", e)
uni.showToast({ title: '验签异常', icon: 'error' })
}
}
// ==================== SM2 签名 ====================
async function testSm2Sign() {
if (sm2SignContent.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥对
const keyPair = await SM.sm2GenerateKeyPairAsync()
sm2KeyPair.value = {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey
}
console.log("SM2 公钥:", keyPair.publicKey)
console.log("SM2 私钥:", keyPair.privateKey)
// 签名
const signResult = await SM.sm2SignAsync(sm2SignContent.value, keyPair.privateKey)
sm2Signature.value = signResult.signature
console.log("SM2 签名成功:", signResult.signature)
uni.showToast({ title: '签名成功', icon: 'success' })
} catch (e) {
console.error("SM2 生成密钥对失败:", e)
uni.showToast({ title: '生成密钥对失败', icon: 'error' })
}
}
// ==================== SM2 验签 ====================
async function testSm2Verify() {
if (sm2Signature.value.length==0 || sm2KeyPair.value.publicKey.length==0) {
uni.showToast({ title: '请先签名', icon: 'none' })
return
}
try {
// 验签
const verifyResult = await SM.sm2VerifyAsync(sm2SignContent.value, sm2Signature.value, sm2KeyPair.value.publicKey)
sm2VerifyResult.value = verifyResult.valid
console.log("SM2 验签结果:", verifyResult.valid)
if (verifyResult.valid) {
uni.showToast({ title: '验签成功', icon: 'success' })
} else {
uni.showToast({ title: '验签失败', icon: 'error' })
}
} catch (e) {
console.error("SM2 验签异常:", e)
uni.showToast({ title: '验签异常', icon: 'error' })
}
}
// ==================== SM2 加密 ====================
async function testSm2Encrypt() {
if (sm2EncryptContent.value.length==0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥对
const keyPair = await SM.sm2GenerateKeyPairAsync()
sm2EncryptKeyPair.value = {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey
}
console.log("SM2 加密公钥:", keyPair.publicKey)
console.log("SM2 加密私钥:", keyPair.privateKey)
// 加密
const encryptResult = await SM.sm2EncryptAsync(sm2EncryptContent.value, keyPair.publicKey)
sm2Encrypted.value = encryptResult.encrypted
console.log("SM2 加密结果:", encryptResult.encrypted)
uni.showToast({ title: '加密成功', icon: 'success' })
} catch (e) {
console.error("SM2 生成密钥对失败:", e)
uni.showToast({ title: '生成密钥对失败', icon: 'error' })
}
}
// ==================== SM2 解密 ====================
async function testSm2Decrypt() {
if (sm2Encrypted.value.length==0 || sm2EncryptKeyPair.value.privateKey.length==0) {
uni.showToast({ title: '请先加密', icon: 'none' })
return
}
try {
const decryptResult = await SM.sm2DecryptAsync(sm2Encrypted.value, sm2EncryptKeyPair.value.privateKey)
sm2Decrypted.value = decryptResult.decrypted
console.log("SM2 解密结果:", decryptResult.decrypted)
if (decryptResult.decrypted == sm2EncryptContent.value) {
uni.showToast({ title: '解密成功且内容一致', icon: 'success' })
} else {
uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })
}
} catch (e) {
console.error("SM2 解密异常:", e)
uni.showToast({ title: '解密异常', icon: 'error' })
}
}
// ==================== SM3 哈希 ====================
async function testSm3Hash() {
if (sm3Content.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
const hashResult = await SM.sm3HashAsync(sm3Content.value)
sm3HashResult.value = hashResult.resultHash
console.log("SM3 哈希成功:", hashResult.resultHash)
uni.showToast({ title: '哈希成功', icon: 'success' })
} catch (e) {
console.error("SM3 哈希失败:", e)
uni.showToast({ title: '哈希失败', icon: 'error' })
}
}
// ==================== HMAC-SM3 哈希 ====================
async function testSm3HmacHash() {
if (sm3HmacContent.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
if (sm3HmacKey.value.length == 0) {
uni.showToast({ title: '请输入HMAC密钥', icon: 'none' })
return
}
try {
const hmacHashResult = await SM.sm3HmacHashAsync(sm3HmacContent.value, sm3HmacKey.value)
sm3HmacHashResult.value = hmacHashResult.resultHash
sm3HmacExpectedHash.value = hmacHashResult.resultHash
sm3HmacVerifyKey.value = sm3HmacKey.value
sm3HmacVerifyContent.value = sm3HmacContent.value
console.log("HMAC-SM3 哈希成功:", hmacHashResult.resultHash)
uni.showToast({ title: 'HMAC-SM3 哈希成功', icon: 'success' })
} catch (e) {
console.error("HMAC-SM3 哈希失败:", e)
uni.showToast({ title: 'HMAC-SM3 哈希失败', icon: 'error' })
}
}
// ==================== HMAC-SM3 验证(输入期望哈希值) ====================
async function testSm3HmacHashWithExpected() {
if (sm3HmacVerifyContent.value.length == 0) {
uni.showToast({ title: '请输入原始内容', icon: 'none' })
return
}
try {
// 计算实际 HMAC-SM3 哈希值
const res = await SM.sm3HmacVerifyAsync(sm3HmacVerifyContent.value, sm3HmacVerifyKey.value,sm3HmacHashResult.value)
if (res) {
uni.showToast({ title: 'HMAC-SM3 哈希值匹配', icon: 'success' })
} else {
uni.showToast({ title: 'HMAC-SM3 哈希值不匹配', icon: 'error' })
}
} catch (e) {
console.error("HMAC-SM3 哈希验证异常:", e)
uni.showToast({ title: '验证异常', icon: 'error' })
}
}
// ==================== SM3 验证 ====================
async function testSm3Verify() {
if (sm3HashResult.value.length == 0 || sm3Content.value.length == 0) {
uni.showToast({ title: '请先计算哈希', icon: 'none' })
return
}
try {
// 重新计算哈希并验证
const res = await SM.sm3VerifyAsync(sm3VerifyContent.value,sm3HashResult.value)
sm3VerifyResult.value = res
console.log("SM3 验证结果:", sm3VerifyResult.value)
if (sm3VerifyResult.value!) {
uni.showToast({ title: '验证成功', icon: 'success' })
} else {
uni.showToast({ title: '验证失败', icon: 'error' })
}
} catch (e) {
console.error("SM3 验证异常:", e)
uni.showToast({ title: '验证异常', icon: 'error' })
}
}
// ==================== 选择文件并计算 SM3 哈希 ====================
async function processFileHash(filePath: string, fileSize?: number) {
// 计算文件 SM3 哈希
uni.showLoading({ title: '计算中...' })
try {
console.log("开始计算文件 SM3 哈希")
console.log("文件路径:", filePath)
const result = await SM.sm3FileHashAsync(filePath)
sm3FileHashResult.value = result.resultHash
console.log("文件 SM3 哈希成功:", result.resultHash)
uni.hideLoading()
uni.showToast({ title: '文件哈希成功', icon: 'success' })
} catch (e) {
console.error("文件 SM3 哈希失败:", e)
uni.hideLoading()
uni.showToast({ title: '文件哈希失败', icon: 'error' })
}
}
function chooseFileAndHash() {
uni.chooseFile({
count: 1,
success: (res) => {
const tempFiles = res.tempFiles
if (tempFiles != null && tempFiles.length > 0) {
const file = tempFiles[0]
const fileName = file.name
const filePath = file.path
const fileSize = file.size
sm3FileName.value = fileName != null ? fileName : "未知文件"
if (filePath != null) {
processFileHash(filePath, fileSize)
}
}
},
fail: (err) => {
console.error("选择文件失败:", err)
if (err.errMsg != null && !err.errMsg.includes('cancel')) {
uni.showToast({ title: '选择文件失败', icon: 'error' })
}
}
})
}
// ==================== 选择相册图片并计算 SM3 哈希 ====================
function chooseImageAndHash() {
uni.chooseImage({
count: 1,
sizeType: ['original'], // 只选择原图,不压缩
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths
if (tempFilePaths != null && tempFilePaths.length > 0) {
const filePath = tempFilePaths[0]
const fileName = "图片文件"
sm3FileName.value = fileName
console.log("选择的图片(原图):", filePath)
if (filePath != null) {
// 调用异步函数处理文件哈希
processFileHash(filePath)
}
}
},
fail: (err) => {
console.error("选择图片失败:", err)
if (err.errMsg != null && !err.errMsg.includes('cancel')) {
uni.showToast({ title: '选择图片失败', icon: 'error' })
}
}
})
}
const goToDetail = (orderId: number, status: string) => {
uni.setStorageSync('fromTabBar', true)
uni.navigateTo({
url: `/pages/detail/detail?orderId=${orderId}&status=${status}`,
success: () => {
console.log(`跳转到order${orderId}的详情页`)
}
})
}
function btn (){
}
</script>
<style scoped>
.order-content {
flex: 1;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.order-header {
height: 48px;
background-color: #fff;
align-items: center;
justify-content: center;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: #eee;
}
.order-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.order-list {
flex: 1;
padding: 10px;
}
/* 区块样式 */
.section {
background-color: #fff;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: #eee;
}
.input-group {
margin-bottom: 15px;
}
.label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.input {
height: 40px;
border-width: 1px;
border-style: solid;
border-color: #ddd;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
background-color: #fafafa;
}
/* 按钮样式 */
.btn {
height: 44px;
border-radius: 4px;
font-size: 15px;
margin-bottom: 10px;
}
.btn-primary {
background-color: #007aff;
color: #fff;
}
.btn-secondary {
background-color: #34c759;
color: #fff;
}
.btn:disabled {
opacity: 0.5;
}
/* 结果展示框 */
.result-box {
margin-top: 10px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
border-left-width: 3px;
border-left-style: solid;
border-left-color: #007aff;
}
.result-label {
font-size: 12px;
color: #999;
margin-bottom: 5px;
}
.result-value {
font-size: 13px;
color: #333;
line-height: 1.5;
}
.result-value.small {
font-size: 11px;
}
.result-value.success {
color: #34c759;
font-weight: bold;
}
.result-value.error {
color: #ff3b30;
font-weight: bold;
}
.divider {
height: 1px;
background-color: #eee;
margin: 15px 0;
}
.sub-title {
font-size: 14px;
font-weight: bold;
color: #666;
margin-bottom: 10px;
}
</style>
服务端(java工具类)
package top.sunrains;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.util.encoders.Hex;
import java.math.BigInteger;
import java.security.SecureRandom;
/**
* <dependency>
* <groupId>org.bouncycastle</groupId>
* <artifactId>bcprov-jdk15to18</artifactId>
* <version>1.69</version>
* </dependency>
*/
public class SM2Util {
private static final ECParameterSpec CURVE = ECNamedCurveTable.getParameterSpec("sm2p256v1");
private static final ECDomainParameters DOMAIN = new ECDomainParameters(
CURVE.getCurve(),
CURVE.getG(),
CURVE.getN(),
CURVE.getH()
);
/**
* 生成SM2密钥对
*
* @return Map包含publicKey和privateKey,均为小写十六进制字符串
*/
public static java.util.Map<String, String> generateKeyPair() {
try {
BigInteger d = new BigInteger(256, new SecureRandom()).mod(DOMAIN.getN());
org.bouncycastle.math.ec.ECPoint q = DOMAIN.getG().multiply(d).normalize();
String x = String.format("4x", q.getAffineXCoord().toBigInteger());
String y = String.format("4x", q.getAffineYCoord().toBigInteger());
String publicKey = "04" + x + y;
String privateKey = String.format("4x", d);
java.util.Map<String, String> keyMap = new java.util.HashMap<>();
keyMap.put("publicKey", publicKey.toLowerCase());
keyMap.put("privateKey", privateKey.toLowerCase());
return keyMap;
} catch (Exception e) {
System.err.println("密钥生成失败: " + e.getMessage());
e.printStackTrace();
java.util.Map<String, String> keyMap = new java.util.HashMap<>();
keyMap.put("publicKey", "");
keyMap.put("privateKey", "");
return keyMap;
}
}
/**
* SM2签名
*
* @param data 待签名的原始数据
* @param privateKeyHex 私钥(64字符十六进制)
* @return 签名结果(DER格式,小写十六进制)
*/
public static String sign(String data, String privateKeyHex) {
try {
byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);
BigInteger d = new BigInteger(privateKeyHex, 16);
ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);
SM2Signer signer = new SM2Signer(new SM3Digest());
signer.init(true, priKey);
signer.update(msgBytes, 0, msgBytes.length);
byte[] sig = signer.generateSignature();
return Hex.toHexString(sig).toLowerCase();
} catch (Exception e) {
System.err.println("签名失败: " + e.getMessage());
e.printStackTrace();
return "";
}
}
/**
* SM2验签
*
* @param data 原始数据
* @param signatureHex 签名(DER格式,十六进制)
* @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)
* @return 验签是否成功
*/
public static boolean verify(String data, String signatureHex, String publicKeyHex) {
try {
byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);
String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;
BigInteger x = new BigInteger(xy.substring(0, 64), 16);
BigInteger y = new BigInteger(xy.substring(64, 128), 16);
org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();
ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);
SM2Signer signer = new SM2Signer(new SM3Digest());
signer.init(false, pubKey);
signer.update(msgBytes, 0, msgBytes.length);
byte[] sigBytes = Hex.decode(signatureHex);
return signer.verifySignature(sigBytes);
} catch (Exception e) {
System.err.println("验签失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* SM2加密
*
* @param data 待加密的原始数据(UTF-8字符串)
* @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)
* @return 密文(HEX字符串,小写)
*/
public static String encrypt(String data, String publicKeyHex) {
try {
byte[] dataBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);
String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;
BigInteger x = new BigInteger(xy.substring(0, 64), 16);
BigInteger y = new BigInteger(xy.substring(64, 128), 16);
org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();
ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);
SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);
engine.init(true, new ParametersWithRandom(pubKey, new SecureRandom()));
byte[] encrypted = engine.processBlock(dataBytes, 0, dataBytes.length);
return Hex.toHexString(encrypted).toLowerCase();
} catch (Exception e) {
System.err.println("加密失败: " + e.getMessage());
e.printStackTrace();
return "";
}
}
/**
* SM2解密
*
* @param encryptedHex 密文(HEX字符串)
* @param privateKeyHex 私钥(64字符十六进制)
* @return 解密后的原始数据(UTF-8字符串)
*/
public static String decrypt(String encryptedHex, String privateKeyHex) {
try {
byte[] encryptedBytes = Hex.decode(encryptedHex);
BigInteger d = new BigInteger(privateKeyHex, 16);
ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);
SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);
engine.init(false, priKey);
byte[] decrypted = engine.processBlock(encryptedBytes, 0, encryptedBytes.length);
return new String(decrypted, java.nio.charset.StandardCharsets.UTF_8);
} catch (Exception e) {
System.err.println("解密失败: " + e.getMessage());
e.printStackTrace();
return "";
}
}
/**
* 测试方法
*/
public static void main(String[] args) {
System.out.println("=== SM2 工具类测试 ===\n");
java.util.Map<String, String> keyPair = generateKeyPair();
String publicKey = keyPair.get("publicKey");
String privateKey = keyPair.get("privateKey");
System.out.println("公钥: " + publicKey);
System.out.println("私钥: " + privateKey);
System.out.println();
String testData = "123";
System.out.println("原始数据: " + testData);
String signature = sign(testData, privateKey);
System.out.println("签名: " + signature);
System.out.println();
boolean isValid = verify(testData, signature, publicKey);
System.out.println("验签结果: " + isValid);
System.out.println();
if (!isValid) {
System.err.println("验签失败!");
} else {
System.out.println("验签成功!");
}
System.out.println("\n=== SM2 加密解密测试 ===\n");
// 加密解密测试
String originalText = "Hello SM2";
System.out.println("原始数据: " + originalText);
String encrypted = encrypt(originalText, publicKey);
System.out.println("原始数据: " + originalText);
String decrypted = decrypt(encrypted, privateKey);
System.out.println("解密结果: " + decrypted);
System.out.println();
if (originalText.equals(decrypted)) {
System.out.println("加密解密成功!");
} else {
System.err.println("加密解密失败!");
}
}
}
package top.sunrains;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.security.Security;
/**
* <dependency>
* <groupId>org.bouncycastle</groupId>
* <artifactId>bcprov-jdk15to18</artifactId>
* <version>1.69</version>
* </dependency>
*
* SM4国密算法工具类
* 使用Bouncy Castle原生API
* 配置信息:
* - 算法:SM4
* - 模式:CBC(密码分组链接)
* - 填充:PKCS5Padding
* - 密钥长度:128位(16字节)
* - IV长度:128位(16字节)
*/
public class Sm4Util {
static {
try {
Security.addProvider(new BouncyCastleProvider());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
// String key = generateKeyHex();
// String iv = generateIvHex();
//String s = encryptHex("123", key, iv);
String key = "7f9a942a1aa85c331d3697a5369a37fe";
String iv = "cbc07750d33d6a3ef6d39fc7e7c4a876";
String s = "09187a80e9f52e7e962a3465ed70669e";
System.out.println(s);
String decryptedHex = decryptHex(s, key, iv);
System.out.println(decryptedHex);
}
/**
* 生成SM4密钥(16字节,128位)
* @return 十六进制编码的密钥字符串(32个字符)
*/
public static String generateKeyHex() {
byte[] keyBytes = new byte[16];
new SecureRandom().nextBytes(keyBytes);
return byteArrayToHex(keyBytes);
}
/**
* 生成IV向量(16字节,128位)
* @return 十六进制编码的IV字符串(32个字符)
*/
public static String generateIvHex() {
byte[] ivBytes = new byte[16];
new SecureRandom().nextBytes(ivBytes);
return byteArrayToHex(ivBytes);
}
/**
* SM4-CBC加密(字符串到HEX)
* 标准加密方式
*
* @param plaintext 明文字符串(UTF-8)
* @param keyHex 十六进制密钥字符串
* @param ivHex 十六进制IV字符串
* @return 加密后的HEX字符串
* @throws Exception 加密异常
*/
public static String encryptHex(String plaintext, String keyHex, String ivHex) throws Exception {
byte[] key = decodeKeyHex(keyHex);
byte[] iv = decodeIvHex(ivHex);
byte[] data = plaintext.getBytes("UTF-8");
byte[] encrypted = encrypt(data, key, iv);
return byteArrayToHex(encrypted);
}
/**
* SM4-CBC加密
*
* @param data 原始数据字节数组
* @param key 密钥字节数组(16字节)
* @param iv IV字节数组(16字节)
* @return 加密后的字节数组
* @throws Exception 加密异常
*/
public static byte[] encrypt(byte[] data, byte[] key, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(key, "SM4"),
new IvParameterSpec(iv));
return cipher.doFinal(data);
}
/**
* byte[] 转 Hex
*
* @param bytes 字节数组
* @return HEX字符串(小写)
*/
public static String byteArrayToHex(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString().toLowerCase();
}
public static String decryptHex(String encryptedHex, String keyHex, String ivHex) throws Exception {
byte[] key = decodeKeyHex(keyHex);
byte[] iv = decodeIvHex(ivHex);
byte[] encryptedData = hexToByteArray(encryptedHex);
byte[] decrypted = decrypt(encryptedData, key, iv);
return new String(decrypted, "UTF-8");
}
/**
* SM4-CBC解密
*
* @param encryptedData 加密数据字节数组
* @param key 密钥字节数组(16字节)
* @param iv IV字节数组(16字节)
* @return 解密后的原始字节数组
* @throws Exception 解密异常
*/
public static byte[] decrypt(byte[] encryptedData, byte[] key, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(key, "SM4"),
new IvParameterSpec(iv));
return cipher.doFinal(encryptedData);
}
/**
* 从十六进制字符串还原密钥字节数组
* @param keyHex 十六进制编码的密钥字符串
* @return 密钥字节数组(16字节)
*/
public static byte[] decodeKeyHex(String keyHex) {
return hexToByteArray(keyHex);
}
/**
* 从十六进制字符串还原IV字节数组
* @param ivHex 十六进制编码的IV字符串
* @return IV字节数组(16字节)
*/
public static byte[] decodeIvHex(String ivHex) {
return hexToByteArray(ivHex);
}
/**
* HEX 转 byte[]
*
* @param hex HEX字符串
* @return 字节数组
*/
public static byte[] hexToByteArray(String hex) {
if (hex == null || hex.length() == 0) {
return new byte[0];
}
int len = hex.length() / 2;
byte[] bytes = new byte[len];
for (int i = 0; i < len; i++) {
int high = Character.digit(hex.charAt(i * 2), 16);
int low = Character.digit(hex.charAt(i * 2 + 1), 16);
bytes[i] = (byte) ((high << 4) | low);
}
return bytes;
}
public static String stringToHex(String str) {
if (str == null || str.isEmpty()) {
return "";
}
byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);
return byteArrayToHex(bytes);
}
}
package top.sunrains;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* AES对称加密算法工具类
* 使用Java原生Crypto API
* 配置信息:
* - 算法:AES-256
* - 模式:CBC(密码分组链接)
* - 填充:PKCS5Padding
* - 密钥长度:256位(32字节)
* - IV长度:128位(16字节)
*/
public class AesUtil {
/**
* 生成AES密钥和IV
* @return Map包含key (32字符) 和 iv (16字符)
*/
public static Map<String, String> generateKey() {
try {
// 生成32字节的随机字符串作为密钥
String key = generateRandomString(32);
// 生成16字节的随机字符串作为IV
String iv = generateRandomString(16);
System.out.println("AES Key: " + key + " (长度: " + key.length() + ")");
System.out.println("AES IV: " + iv + " (长度: " + iv.length() + ")");
Map<String, String> result = new HashMap<>();
result.put("key", key);
result.put("iv", iv);
return result;
} catch (Exception e) {
System.err.println("AES 密钥生成失败: " + e.getMessage());
e.printStackTrace();
return new HashMap<>();
}
}
/**
* 生成指定长度的随机字符串
*/
private static String generateRandomString(int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
SecureRandom random = new SecureRandom();
StringBuilder result = new StringBuilder(length);
for (int i = 0; i < length; i++) {
result.append(chars.charAt(random.nextInt(chars.length())));
}
return result.toString();
}
/**
* AES-CBC加密
*
* @param plaintext 明文字符串(UTF-8)
* @param keyStr 密钥字符串(32字符)
* @param ivStr IV字符串(16字符)
* @return Base64编码的密文
* @throws Exception 加密异常
*/
public static String encrypt(String plaintext, String keyStr, String ivStr) throws Exception {
// 将字符串Key和IV转换为字节数组(UTF-8编码)
byte[] keyBytes = keyStr.getBytes("UTF-8");
byte[] ivBytes = ivStr.getBytes("UTF-8");
// 创建密钥和IV
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
// 创建Cipher对象(AES/CBC/PKCS5Padding)
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
// 执行加密
byte[] encrypted = cipher.doFinal(plaintext.getBytes("UTF-8"));
// 返回Base64编码的密文
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* AES-CBC解密
*
* @param encryptedBase64 Base64编码的密文
* @param keyStr 密钥字符串(32字符)
* @param ivStr IV字符串(16字符)
* @return 解密后的明文字符串
* @throws Exception 解密异常
*/
public static String decrypt(String encryptedBase64, String keyStr, String ivStr) throws Exception {
// 将字符串Key和IV转换为字节数组(UTF-8编码)
byte[] keyBytes = keyStr.getBytes("UTF-8");
byte[] ivBytes = ivStr.getBytes("UTF-8");
// 创建密钥和IV
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
// 创建Cipher对象(AES/CBC/PKCS5Padding)
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 将Base64密文解码为字节
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64);
// 执行解密
byte[] decrypted = cipher.doFinal(encryptedBytes);
// 返回明文字符串
return new String(decrypted, "UTF-8");
}
/**
* 测试方法
*/
public static void main(String[] args) throws Exception {
System.out.println("=== AES 工具类测试 ===\n");
// 生成密钥
Map<String, String> keyMap = generateKey();
String key = keyMap.get("key");
String iv = keyMap.get("iv");
System.out.println("密钥: " + key);
System.out.println("IV: " + iv);
System.out.println();
// 加密测试
String originalText = "Hello AES";
System.out.println("原始数据: " + originalText);
String encrypted = encrypt(originalText, key, iv);
System.out.println("加密结果: " + encrypted);
System.out.println();
// 解密测试
String decrypted = decrypt(encrypted, key, iv);
System.out.println("解密结果: " + decrypted);
System.out.println();
if (originalText.equals(decrypted)) {
System.out.println("✅ 加密解密成功!");
} else {
System.err.println("❌ 加密解密失败!");
}
}
}
package top.sunrains;
import cn.hutool.core.text.CharSequenceUtil;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
/**
* SM3国密哈希算法工具类
* 使用Bouncy Castle原生API
* 功能:
* - 字符串哈希计算
* - 字节数组哈希计算
* - 文件哈希计算
* - HMAC-SM3计算
*/
public class SM3Util {
static {
try {
Security.addProvider(new BouncyCastleProvider());
} catch (Exception e) {
e.printStackTrace();
}
}
private static final int BUFFER_SIZE = 8192;
/**
* 计算字符串的SM3哈希值
*
* @param data 待哈希的字符串
* @return 哈希值(小写十六进制字符串,64个字符)
*/
public static String hash(String data) {
if (data == null) {
throw new IllegalArgumentException("Data cannot be null");
}
return hash(data.getBytes(java.nio.charset.StandardCharsets.UTF_8));
}
/**
* 验证字符串的SM3哈希值
*
* @param data 原始数据
* @param expectedHash 期望的哈希值
* @return 是否匹配
*/
public static boolean verify(String data, String expectedHash) {
String actualHash = hash(data);
return actualHash.equalsIgnoreCase(expectedHash);
}
/**
* 计算字节数组的SM3哈希值
*
* @param data 待哈希的字节数组
* @return 哈希值(小写十六进制字符串,64个字符)
*/
public static String hash(byte[] data) {
SM3Digest digest = new SM3Digest();
digest.update(data, 0, data.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return byteArrayToHex(result);
}
/**
* 计算文件的SM3哈希值(支持大文件)
*
* @param filePath 文件路径
* @return 哈希值(小写十六进制字符串,64个字符)
* @throws IOException 文件读取异常
*/
public static String hashFile(String filePath) throws IOException {
return hashFile(new File(filePath));
}
/**
* 计算文件的SM3哈希值(支持大文件)
*
* @param file 文件对象
* @return 哈希值(小写十六进制字符串,64个字符)
* @throws IOException 文件读取异常
*/
public static String hashFile(File file) throws IOException {
if (!file.exists()) {
throw new IOException("File not found: " + file.getAbsolutePath());
}
SM3Digest digest = new SM3Digest();
byte[] buffer = new byte[BUFFER_SIZE];
try (InputStream is = new FileInputStream(file)) {
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return byteArrayToHex(result);
}
/**
* 计算HMAC-SM3(基于密钥的哈希消息认证码)
*
* @param data 待计算的数据
* @param hexKey 密钥
* @return HMAC-SM3结果(小写十六进制字符串,64个字符)
*/
public static String hmac(String data, String hexKey) {
if (CharSequenceUtil.isBlank(hexKey)) {
throw new IllegalArgumentException("密钥不能为空");
}
return hmac(data.getBytes(java.nio.charset.StandardCharsets.UTF_8),
hexToByteArray(hexKey));
}
/**
* 计算HMAC-SM3(基于密钥的哈希消息认证码)
*
* @param data 待计算的字节数组
* @param hexKey hex密钥字节数组
* @return HMAC-SM3结果(小写十六进制字符串,64个字符)
*/
public static String hmac(byte[] data, byte[] hexKey) {
HMac hmac = new HMac(new SM3Digest());
hmac.init(new KeyParameter(hexKey));
hmac.update(data, 0, data.length);
byte[] result = new byte[hmac.getMacSize()];
hmac.doFinal(result, 0);
return byteArrayToHex(result);
}
/**
* byte[] 转 Hex
*
* @param bytes 字节数组
* @return HEX字符串(小写)
*/
private static String byteArrayToHex(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString().toLowerCase();
}
/**
* 测试方法
*/
public static void main(String[] args) throws IOException {
String testData = "12";
System.out.println("SM3: " + hash(testData));
String key = "1";
key = stringToHex(key);
System.out.println("HMAC-SM3 key: " + key);
String hmacResult = hmac(testData, key);
System.out.println("HMAC-SM3: " + hmacResult);
System.out.println("HMAC长度: " + hmacResult.length() + " 字符");
System.out.println("文件哈希值: " + hashFile("/Users/sun/1/t.jpg"));
}
public static String stringToHex(String str) {
if (str == null || str.isEmpty()) {
return "";
}
byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);
return byteArrayToHex(bytes);
}
/**
* HEX 转 byte[]
*
* @param hex HEX字符串
* @return 字节数组
*/
public static byte[] hexToByteArray(String hex) {
if (hex == null || hex.length() == 0) {
return new byte[0];
}
int len = hex.length() / 2;
byte[] bytes = new byte[len];
for (int i = 0; i < len; i++) {
int high = Character.digit(hex.charAt(i * 2), 16);
int low = Character.digit(hex.charAt(i * 2 + 1), 16);
bytes[i] = (byte) ((high << 4) | low);
}
return bytes;
}
}
各平台实现说明
Android 平台
SM2 实现:
-
依赖库:BouncyCastle (
org.bouncycastle:bcprov-jdk15on:1.70) -
特点:
- 使用
ECKeyPairGenerator生成密钥对 - 使用
SM2Signer进行签名和验签(DER 格式) - 使用
SM2Engine进行加密和解密(C1C3C2 格式) - 不依赖 JCA Provider,避免兼容性问题
- 使用
SM4 实现:
-
依赖库:BouncyCastle
-
特点:
- 使用
SM4Engine进行加密和解密 - CBC 模式 + PKCS7 填充
- 支持自定义密钥和 IV
- 使用
iOS 平台
实现方式:GMObjC 原生库
SM2 特点:
- 使用
GMSm2Utils进行所有操作 - 签名自动转换为 DER 格式以兼容其他平台
- 加密输出 C1C3C2 格式(04 开头)
- 支持跨平台互通(Android/HarmonyOS)
SM4 特点:
- 使用
GMSm4Utils进行加密解密 - CBC 模式 + PKCS7 填充
- 密钥和 IV 为 32 位十六进制字符串
HarmonyOS 平台
实现方式: @ohos.security.cryptoFramework
SM2 特点:
- 使用 HarmonyOS 原生 cryptoFramework API
- 签名使用 DER 格式
- 加密使用 C1C3C2 格式
- 完全符合国密标准
SM3 特点:
- 使用
createMd(MessageDigest) 进行普通哈希 - 使用手动实现 HMAC-SM3 (RFC 2104)
- 支持流式文件哈希处理 (4096 字节缓冲区)
- 不依赖
createMac,避免密钥参数问题
SM4 特点:
- 使用
createSymKeyGenerator生成密钥 - 使用
createCipher进行加密解密 - CBC 模式 + PKCS7 填充
- 支持 SM4_128 算法
AES 特点:
- 使用
crypto.createSymKeyGenerator('AES256')创建密钥生成器 - 使用
crypto.createCipher('AES|CBC|PKCS7')进行加解密 - CBC 模式 + PKCS7 填充
- 密钥和 IV 为随机字符串(UTF-8 编码)
H5 平台
实现方式:sm-crypto + Web Crypto API
特点:
- SM2/SM3/SM4:通过 npm 包
sm-crypto实现 - AES:使用浏览器原生 Web Crypto API 实现
- 所有功能均在浏览器环境中运行
- SM2 签名使用 DER 格式(
der: true) - SM2 默认 userId 为
'1234567812345678'
依赖安装:
npm install --save sm-crypto
微信小程序平台
实现方式:miniprogram-sm-crypto + crypto-js + jsrsasign
特点:
- 专为小程序优化的国密算法库
- 完全支持 SM2、SM4 和 AES
- SM2:密钥生成、签名、验签、加密、解密(使用 miniprogram-sm-crypto)
- SM4:密钥生成、加密、解密(CBC 模式,使用 miniprogram-sm-crypto)
- AES:密钥生成、加密、解密(CBC 模式,使用 crypto-js)
- 需要通过微信开发者工具构建 npm
依赖安装:
npm install --save miniprogram-sm-crypto crypto-js jsrsasign
重要:微信小程序 npm 构建步骤
- 确保项目根目录已安装上述依赖
- 打开微信开发者工具,勾选「详情」→「本地设置」→「使用 npm 模块」
- 点击菜单栏「工具」→「构建 npm」,生成
miniprogram_npm目录 - 重新编译运行小程序
注意事项
1. 密钥格式
SM2 密钥:
- 公钥:以
04开头的 130 位十六进制字符串(非压缩格式) - 私钥:64 位十六进制字符串
SM4 密钥:
- 密钥 (key) :32 位十六进制字符串(128 位)
- 初始向量 (IV) :32 位十六进制字符串(128 位)
AES 密钥:
- 密钥 (key) :32 字符随机字符串(字母+数字,UTF-8 编码后为 256 位)
- 初始向量 (IV) :16 字符随机字符串(字母+数字,UTF-8 编码后为 128 位)
2. 签名格式
- 所有平台的签名输出均为 DER 编码的十六进制字符串
- DER 格式确保跨平台兼容性
3. 哈希格式
SM3 哈希:
- 输出长度:64 位十六进制字符串 (256 位)
- 格式:小写十六进制
- 示例:
a1b2c3d4e5f6...(64 个字符)
HMAC-SM3 哈希:
- 输出长度:64 位十六进制字符串 (256 位)
- 密钥格式:十六进制字符串
- 格式:小写十六进制
4. 加密格式
SM2 加密:
- 输出格式:C1C3C2(以
04开头) - 兼容 Android BouncyCastle、iOS GMObjC、HarmonyOS cryptoFramework
SM4 加密:
- 模式:CBC
- 填充:PKCS7
- 输出:十六进制字符串
AES 加密:
- 模式:CBC
- 填充:PKCS5/PKCS7
- 密钥:32 字符随机字符串(UTF-8 编码后为 256 位)
- IV:16 字符随机字符串(UTF-8 编码后为 128 位)
- 输出:Base64 编码字符串
RSA 加密:
- 算法:RSA-OAEP with SHA-256 (对应 Java 的
RSA/ECB/OAEPWithSHA-256AndMGF1Padding) - 密钥长度:2048 位
- 私钥格式:PKCS#8 (Base64 编码)
- 公钥格式:X.509/SPKI (Base64 编码)
- 输出:Base64 编码字符串
- 跨平台兼容:Android/iOS/HarmonyOS/H5 完全互通
RSA 签名:
- 算法:SHA256withRSA (PKCS#1 v1.5)
- 私钥格式:PKCS#8 (Base64 编码)
- 公钥格式:X.509/SPKI (Base64 编码)
- 签名输出:Base64 编码字符串
- 跨平台兼容:Android/iOS/HarmonyOS/H5/微信小程序 完全互通
5. 跨平台兼容性
✅ 完全兼容:
- 各平台生成的 SM2 密钥对可以互相使用
- Android 平台签名的数据可以在 iOS/Harmony/H5 平台验证
- SM2 加密的数据可以在不同平台间解密
- SM4 加密的数据可以在 Android/iOS/Harmony 平台间加解密
- AES 加密的数据可以在所有平台间加解密(Android/iOS/HarmonyOS/H5/微信小程序)
- SM3 哈希值在所有平台保持一致(相同输入产生相同输出)
- HMAC-SM3 支持跨平台验证
- RSA-OAEP-SHA256 加密的数据可以在 Android/iOS/HarmonyOS/H5 平台间加解密
- SHA256withRSA 签名可以在所有平台间验签(包括微信小程序)
⚠️ 注意事项:
- 确保使用相同的哈希选项(默认启用)
- SM4/AES 加密和解密必须使用相同的密钥和 IV
- 微信小程序需要先构建 npm 才能使用
- SM3 文件哈希需要确保文件内容完全一致
- AES 密钥为 32 字符随机字符串,IV 为 16 字符随机字符串
- RSA 密钥长度为 2048 位,私钥为 PKCS#8 格式,公钥为 X.509/SPKI 格式
- RSA 加密使用 OAEP with SHA-256 填充,与其他平台互通
- RSA 签名使用 SHA256withRSA (PKCS#1 v1.5),所有平台兼容
- 微信小程序端暂不支持 RSA-OAEP-SHA256 加密解密,仅支持签名验签
sunrains-smutil 插件使用指南
本插件提供SM2、SM3 、 SM4、AES、RSA 客户端(ios,安卓,鸿蒙,微信小程序,h5),服务端(java)SM2、SM3 、 SM4、AES、RSA 算法的跨平台实现,支持以下功能:
SM2 非对称加密算法
✅ SM2 密钥对生成
✅ SM2 数字签名
✅ SM2 签名验证
✅ SM2 数据加密
✅ SM2 数据解密
SM3 密码杂凑算法
✅ SM3 哈希计算(字符串)
✅ HMAC-SM3 密钥哈希
✅ 文件 SM3 哈希
✅ 哈希值验证
SM4 对称加密算法
✅ SM4 密钥生成(含 IV)
✅ SM4 数据加密(CBC 模式)
✅ SM4 数据解密(CBC 模式)
AES 对称加密算法
✅ AES 密钥和 IV 生成
✅ AES 数据加密(CBC 模式, PKCS7/PKCS5 填充)
✅ AES 数据解密(CBC 模式, PKCS7/PKCS5 填充)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5/微信小程序)
RSA 非对称加密算法
✅ RSA 密钥对生成(2048位)
✅ RSA-OAEP-SHA256 数据加密
✅ RSA-OAEP-SHA256 数据解密
✅ SHA256withRSA 数字签名(PKCS#1 v1.5)
✅ SHA256withRSA 签名验证(PKCS#1 v1.5)
✅ 跨平台兼容(Android/iOS/HarmonyOS/H5)
⚠️ 微信小程序端暂不支持RSA-OAEP-SHA256加密解密,仅支持签名验签
调用方式
本插件同时支持回调风格和异步风格两种调用方式:
方式一:回调风格(传统方式)
import { sm2Sign } from "@/uni_modules/sunrains-util";
sm2Sign({
data: "需要签名的数据",
privateKey: "你的私钥",
success: (res) => {
console.log("签名结果:", res.signature);
},
fail: (err) => {
console.error("签名失败:", err);
}
});
方式二:异步风格(推荐,更简洁)
import { sm2SignAsync } from "@/uni_modules/sunrains-util";
async function doSign() {
try {
const result = await sm2SignAsync("需要签名的数据", "你的私钥")
console.log("签名结果:", result.signature)
} catch (error) {
console.error("签名失败:", error)
}
}
平台支持
| 平台 | SM2 实现方式 | SM4 实现方式 | AES 实现方式 | RSA 实现方式 | 状态 |
|---|---|---|---|---|---|
| Android | BouncyCastle | BouncyCastle | Java Crypto API | Java Crypto API (RSA-OAEP-SHA256) | ✅ 完全支持 |
| iOS | GMObjC | GMObjC | CommonCrypto | Security Framework (RSA-OAEP-SHA256) | ✅ 完全支持 |
| HarmonyOS | cryptoFramework | cryptoFramework | cryptoFramework | cryptoFramework (RSA-OAEP-SHA256) | ✅ 完全支持 |
| H5 | sm-crypto | sm-crypto | Web Crypto API | Web Crypto API (RSA-OAEP-SHA256) | ✅ 完全支持 |
| 微信小程序 | miniprogram-sm-crypto | miniprogram-sm-crypto | crypto-js | jsrsasign (仅签名验签) | ⚠️ 部分支持 |
使用示例
SM2 相关操作
1. 生成 SM2 密钥对
同步方式:
import { sm2GenerateKeyPair } from "@/uni_modules/sunrains-util";
const result = sm2GenerateKeyPair();
console.log("公钥:", result.publicKey); // 04开头的130位十六进制字符串
console.log("私钥:", result.privateKey); // 64位十六进制字符串
异步方式:
import { sm2GenerateKeyPairAsync } from "@/uni_modules/sunrains-util";
async function generateKeys() {
const result = await sm2GenerateKeyPairAsync()
console.log("公钥:", result.publicKey)
console.log("私钥:", result.privateKey)
}
2. SM2 签名
回调风格:
import { sm2Sign } from "@/uni_modules/sunrains-util";
sm2Sign({
data: "需要签名的数据",
privateKey: "你的私钥",
success: (res) => {
console.log("签名结果:", res.signature);
},
fail: (err) => {
console.error("签名失败:", err);
}
});
异步风格(推荐):
import { sm2SignAsync } from "@/uni_modules/sunrains-util";
async function doSign() {
try {
const result = await sm2SignAsync("需要签名的数据", "你的私钥")
console.log("签名结果:", result.signature)
} catch (error) {
console.error("签名失败:", error)
}
}
3. SM2 验签
回调风格:
import { sm2Verify } from "@/uni_modules/sunrains-util";
sm2Verify({
data: "原始数据",
signature: "签名值",
publicKey: "你的公钥",
success: (res) => {
console.log("验签结果:", res.valid); // true 或 false
},
fail: (err) => {
console.error("验签失败:", err);
}
});
异步风格(推荐):
import { sm2VerifyAsync } from "@/uni_modules/sunrains-util";
async function doVerify() {
try {
const result = await sm2VerifyAsync("原始数据", "签名值", "你的公钥")
console.log("验签结果:", result.valid) // true 或 false
} catch (error) {
console.error("验签失败:", error)
}
}
完整实战示例
以下是一个完整的国密算法测试页面示例,及服务端(java)工具类,展示了所有功能的实际使用:
客户端 Vue 3 Composition API 示例
<template>
<view class="order-content">
<view class="order-header">
<text class="order-title">国密算法测试</text>
</view>
<scroll-view class="order-list" scroll-y="true">
<view class="section">
<view class="section-title">SM4 对称加密</view>
<view class="input-group">
<text class="label">待加密内容:</text>
<input v-model="sm4Content" placeholder="请输入要加密的内容" class="input" />
</view>
<button @click="testSm4Encrypt" class="btn btn-primary">SM4 加密</button>
<view class="result-box" v-if="sm4Key.key">
<text class="result-label">密钥 (key):</text>
<text selectable="true" class="result-value">{{ sm4Key.key }}</text>
</view>
<view class="result-box" v-if="sm4Key.iv">
<text class="result-label">初始向量 (iv):</text>
<text selectable="true" class="result-value">{{ sm4Key.iv }}</text>
</view>
<view class="result-box" v-if="sm4Encrypted">
<text class="result-label">加密结果:</text>
<text selectable="true" class="result-value">{{ sm4Encrypted }}</text>
</view>
<button @click="testSm4Decrypt" :disabled="sm4Encrypted.length==0" class="btn btn-secondary">SM4 解密</button>
<button @click="testSm4Decrypt" :disabled="sm4Encrypted === ''" :class="sm4Encrypted === '' ? 'btn btn-secondary disabled' : 'btn btn-secondary'" class="btn btn-secondary">SM4 解密</button>
<view class="result-box" v-if="sm4Decrypted">
<text class="result-label">解密结果:</text>
<text selectable="true" class="result-value success">{{ sm4Decrypted }}</text>
</view>
</view>
<view class="section">
<view class="section-title">SM2 数字签名</view>
<view class="input-group">
<text class="label">待签名内容:</text>
<input v-model="sm2SignContent" placeholder="请输入要签名的内容" class="input" />
</view>
<button @click="testSm2Sign" class="btn btn-primary">生成密钥对并签名</button>
<view class="result-box" v-if="sm2KeyPair.publicKey">
<text class="result-label">公钥:</text>
<text selectable="true" class="result-value small">{{ sm2KeyPair.publicKey }}</text>
</view>
<view class="result-box" v-if="sm2KeyPair.privateKey">
<text class="result-label">私钥:</text>
<text selectable="true" class="result-value small">{{ sm2KeyPair.privateKey }}</text>
</view>
<view class="result-box" v-if="sm2Signature">
<text class="result-label">签名结果:</text>
<text selectable="true" class="result-value small">{{ sm2Signature }}</text>
</view>
<button @click="testSm2Verify" :disabled="sm2Signature.length==0" class="btn btn-secondary">验签</button>
<view class="result-box" v-if="sm2VerifyResult !== null">
<text class="result-label">验签结果:</text>
<text class="result-value" :class="sm2VerifyResult ==true? 'success' : 'error'">
{{ sm2VerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}
</text>
</view>
</view>
<view class="section">
<view class="section-title">SM2 非对称加密</view>
<view class="input-group">
<text class="label">待加密内容:</text>
<input v-model="sm2EncryptContent" placeholder="请输入要加密的内容" class="input" />
</view>
<button @click="testSm2Encrypt" class="btn btn-primary">SM2 加密</button>
<view class="result-box" v-if="sm2EncryptKeyPair.publicKey">
<text class="result-label">公钥:</text>
<text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.publicKey }}</text>
</view>
<view class="result-box" v-if="sm2EncryptKeyPair.privateKey">
<text class="result-label">私钥:</text>
<text selectable="true" class="result-value small">{{ sm2EncryptKeyPair.privateKey }}</text>
</view>
<view class="result-box" v-if="sm2Encrypted">
<text class="result-label">加密结果:</text>
<text selectable="true" class="result-value small">{{ sm2Encrypted }}</text>
</view>
<button @click="testSm2Decrypt" :disabled="sm2Encrypted.length==0" class="btn btn-secondary">SM2 解密</button>
<view class="result-box" v-if="sm2Decrypted">
<text class="result-label">解密结果:</text>
<text selectable="true" class="result-value success">{{ sm2Decrypted }}</text>
</view>
</view>
<view class="section">
<view class="section-title">SM3 哈希算法</view>
<!-- 1. SM3 哈希 -->
<view class="input-group">
<text class="label">待哈希内容:</text>
<input v-model="sm3Content" placeholder="请输入要哈希的内容" class="input" />
</view>
<button @click="testSm3Hash" class="btn btn-primary">SM3 哈希</button>
<view class="result-box" v-if="sm3HashResult">
<text class="result-label">SM3 哈希值:</text>
<text selectable="true" class="result-value small">{{ sm3HashResult }}</text>
</view>
<!-- 2. SM3 哈希验证 -->
<view class="divider"></view>
<view class="sub-title">SM3 哈希验证</view>
<view class="input-group">
<text class="label">验证 - 原始内容:</text>
<input v-model="sm3VerifyContent" placeholder="请输入原始内容" class="input" />
</view>
<button @click="testSm3Verify" :disabled="sm3VerifyContent.length == 0" class="btn btn-secondary">验证哈希值</button>
<view class="result-box" v-if="sm3HashMatchResult !== null">
<text class="result-label">哈希匹配结果:</text>
<text class="result-value" :class="sm3HashMatchResult == true ? 'success' : 'error'">
{{ sm3HashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}
</text>
</view>
<!-- 3. HMAC-SM3 哈希 -->
<view class="divider"></view>
<view class="sub-title">HMAC-SM3 哈希</view>
<view class="input-group">
<text class="label">HMAC-SM3 待哈希内容:</text>
<input v-model="sm3HmacContent" placeholder="请输入要哈希的内容" class="input" />
</view>
<view class="input-group">
<text class="label">HMAC-SM3 密钥(HEX):</text>
<input v-model="sm3HmacKey" placeholder="请输入HMAC密钥(HEX格式)" class="input" />
</view>
<button @click="testSm3HmacHash" :disabled="sm3HmacContent.length == 0 || sm3HmacKey.length == 0" class="btn btn-primary">HMAC-SM3 哈希</button>
<view class="result-box" v-if="sm3HmacHashResult">
<text class="result-label">HMAC-SM3 哈希值:</text>
<text selectable="true" class="result-value small">{{ sm3HmacHashResult }}</text>
</view>
<!-- 4. HMAC-SM3 哈希验证 -->
<view class="divider"></view>
<view class="sub-title">HMAC-SM3 哈希验证</view>
<view class="input-group">
<text class="label">验证 - 原始内容:</text>
<input v-model="sm3HmacVerifyContent" placeholder="请输入原始内容" class="input" />
</view>
<view class="input-group">
<text class="label">验证 - HMAC 密钥:</text>
<input v-model="sm3HmacVerifyKey" placeholder="请输入HMAC密钥" class="input" />
</view>
<view class="input-group">
<text class="label">验证 - 期望哈希值:</text>
<input v-model="sm3HmacExpectedHash" placeholder="请输入期望的哈希值" class="input" />
</view>
<button @click="testSm3HmacHashWithExpected" :disabled="sm3HmacVerifyContent.length == 0 || sm3HmacVerifyKey.length == 0 || sm3HmacExpectedHash.length == 0" class="btn btn-secondary">验证 HMAC-SM3 哈希值</button>
<view class="result-box" v-if="sm3HmacHashMatchResult !== null">
<text class="result-label">HMAC-SM3 哈希匹配结果:</text>
<text class="result-value" :class="sm3HmacHashMatchResult == true ? 'success' : 'error'">
{{ sm3HmacHashMatchResult == true ? '✅ 哈希值匹配' : '❌ 哈希值不匹配' }}
</text>
</view>
<!-- 5. 文件 SM3 哈希 -->
<view class="divider"></view>
<view class="sub-title">文件 SM3 哈希</view>
<button @click="chooseImageAndHash" class="btn btn-primary">选择图片并计算 SM3</button>
<button @click="chooseFileAndHash" class="btn btn-primary">选择文件并计算 SM3</button>
<view class="result-box" v-if="sm3FileName">
<text class="result-label">文件名:</text>
<text class="result-value">{{ sm3FileName }}</text>
</view>
<view class="result-box" v-if="sm3FileHashResult">
<text class="result-label">文件 SM3 哈希值:</text>
<text selectable="true" class="result-value small">{{ sm3FileHashResult }}</text>
</view>
</view>
<view class="section">
<view class="section-title">AES 对称加密</view>
<view class="input-group">
<text class="label">待加密内容:</text>
<input v-model="aesContent" placeholder="请输入要加密的内容" class="input" />
</view>
<button @click="testAesEncrypt" class="btn btn-primary">AES 加密</button>
<view class="result-box" v-if="aesKey.key">
<text class="result-label">密钥 (key):</text>
<text selectable="true" class="result-value">{{ aesKey.key }}</text>
</view>
<view class="result-box" v-if="aesKey.iv">
<text class="result-label">初始向量 (iv):</text>
<text selectable="true" class="result-value">{{ aesKey.iv }}</text>
</view>
<view class="result-box" v-if="aesEncrypted">
<text class="result-label">加密结果:</text>
<text selectable="true" class="result-value">{{ aesEncrypted }}</text>
</view>
<button @click="testAesDecrypt" :disabled="aesEncrypted.length==0" class="btn btn-secondary">AES 解密</button>
<view class="result-box" v-if="aesDecrypted">
<text class="result-label">解密结果:</text>
<text selectable="true" class="result-value success">{{ aesDecrypted }}</text>
</view>
</view>
<view class="section">
<view class="section-title">RSA 非对称加密</view>
<view class="input-group">
<text class="label">待加密内容:</text>
<input v-model="rsaContent" placeholder="请输入要加密的内容" class="input" />
</view>
<button @click="testRsaEncrypt" class="btn btn-primary">RSA 加密</button>
<view class="result-box" v-if="rsaKeyPair.publicKey">
<text class="result-label">公钥:</text>
<text selectable="true" class="result-value small">{{ rsaKeyPair.publicKey }}</text>
</view>
<view class="result-box" v-if="rsaKeyPair.privateKey">
<text class="result-label">私钥:</text>
<text selectable="true" class="result-value small">{{ rsaKeyPair.privateKey }}</text>
</view>
<view class="result-box" v-if="rsaEncrypted">
<text class="result-label">加密结果:</text>
<text selectable="true" class="result-value small">{{ rsaEncrypted }}</text>
</view>
<button @click="testRsaDecrypt" :disabled="rsaEncrypted.length==0" class="btn btn-secondary">RSA 解密</button>
<view class="result-box" v-if="rsaDecrypted">
<text class="result-label">解密结果:</text>
<text selectable="true" class="result-value success">{{ rsaDecrypted }}</text>
</view>
</view>
<view class="section">
<view class="section-title">RSA 数字签名</view>
<view class="input-group">
<text class="label">待签名内容:</text>
<input v-model="rsaSignContent" placeholder="请输入要签名的内容" class="input" />
</view>
<button @click="testRsaSign" class="btn btn-primary">生成密钥对并签名</button>
<view class="result-box" v-if="rsaSignKeyPair.publicKey">
<text class="result-label">公钥:</text>
<text selectable="true" class="result-value small">{{ rsaSignKeyPair.publicKey }}</text>
</view>
<view class="result-box" v-if="rsaSignKeyPair.privateKey">
<text class="result-label">私钥:</text>
<text selectable="true" class="result-value small">{{ rsaSignKeyPair.privateKey }}</text>
</view>
<view class="result-box" v-if="rsaSignature">
<text class="result-label">签名结果:</text>
<text selectable="true" class="result-value small">{{ rsaSignature }}</text>
</view>
<button @click="testRsaVerify" :disabled="rsaSignature.length==0" class="btn btn-secondary">验签</button>
<view class="result-box" v-if="rsaVerifyResult !== null">
<text class="result-label">验签结果:</text>
<text class="result-value" :class="rsaVerifyResult ==true? 'success' : 'error'">
{{ rsaVerifyResult ==true ? '✅ 验签成功' : '❌ 验签失败' }}
</text>
</view>
</view>
<view class="order-item" @click="goToDetail(1002, '已完成')">
<view class="order-info">
<text class="order-id">订单编号:1002</text>
<text class="order-status">状态:已完成</text>
</view>
<view class="order-btn">
<text class="go-detail">查看详情</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup lang="uts">
import {IdcardOcrModel}from "@/uni_modules/sunrains-idcard-ocr/utssdk/interface.uts"
import {Response} from "@/uni_modules/sunrains-utils/index.uts"
import * as SM from "@/uni_modules/sunrains-smutil"
// ==================== SM4 相关数据 ====================
const sm4Content = ref("Hello World")
const sm4Key = ref<SM.Sm4Key>({ key: '', iv: '' })
const sm4Encrypted = ref("")
const sm4Decrypted = ref("")
// ==================== SM2 签名验签相关数据 ====================
const sm2SignContent = ref("123")
const sm2KeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })
const sm2Signature = ref("")
const sm2VerifyResult = ref<boolean | null>(null)
// ==================== SM2 加密解密相关数据 ====================
const sm2EncryptContent = ref("Hello SM2")
const sm2EncryptKeyPair = ref<SM.Sm2KeyPair>({ publicKey: '', privateKey: '' })
const sm2Encrypted = ref("")
const sm2Decrypted = ref("")
// ==================== SM3 相关数据 ====================
const sm3Content = ref("Hello SM3")
const sm3HashResult = ref("")
const sm3VerifyContent = ref("")
const sm3HashMatchResult = ref<boolean | null>(null)
const sm3HmacContent = ref("Hello HMAC-SM3")
const sm3HmacKey = ref("0123456789abcdef0123456789abcdef") // 默认 HMAC 密钥(32字节HEX)
const sm3HmacHashResult = ref("")
const sm3HmacVerifyContent = ref("")
const sm3HmacVerifyKey = ref("")
const sm3HmacExpectedHash = ref("")
const sm3HmacHashMatchResult = ref<boolean | null>(null)
const sm3VerifyResult = ref<boolean|null>(null)
const sm3FileName = ref("")
const sm3FileHashResult = ref("")
// ==================== AES 相关数据 ====================
const aesContent = ref("Hello AES")
const aesKey = ref<SM.AesKey>({ key: '', iv: '' })
const aesEncrypted = ref("")
const aesDecrypted = ref("")
// ==================== RSA 加密解密相关数据 ====================
const rsaContent = ref("Hello RSA")
const rsaKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })
const rsaEncrypted = ref("")
const rsaDecrypted = ref("")
// ==================== RSA 签名验签相关数据 ====================
const rsaSignContent = ref("需要签名的数据")
const rsaSignKeyPair = ref<SM.RsaKeyPair>({ publicKey: '', privateKey: '' })
const rsaSignature = ref("")
const rsaVerifyResult = ref<boolean | null>(null)
// ==================== SM4 加密 ====================
async function testSm4Encrypt() {
if (sm4Content.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥
const key = await SM.sm4GenerateKeyAsync()
sm4Key.value = { key: key.key, iv: key.iv }
console.log("SM4 密钥:", key)
// 加密
const encryptResult = await SM.sm4EncryptAsync(sm4Content.value, key.key, key.iv)
sm4Encrypted.value = encryptResult.result
console.log("SM4 加密成功:", encryptResult.result)
uni.showToast({ title: '加密成功', icon: 'success' })
} catch (e) {
console.error("SM4 加密失败:", e)
uni.showToast({ title: '加密失败', icon: 'error' })
}
}
// ==================== SM4 解密 ====================
async function testSm4Decrypt() {
if (sm4Encrypted.value.length==0 || sm4Key.value.key.length==0) {
uni.showToast({ title: '请先加密', icon: 'none' })
return
}
try {
const decryptResult = await SM.sm4DecryptAsync(sm4Encrypted.value, sm4Key.value.key, sm4Key.value.iv)
sm4Decrypted.value = decryptResult.result
console.log("SM4 解密成功:", decryptResult.result)
if (decryptResult.result == sm4Content.value) {
uni.showToast({ title: '解密成功且内容一致', icon: 'success' })
} else {
uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })
}
} catch (e) {
console.error("SM4 解密失败:", e)
uni.showToast({ title: '解密失败', icon: 'error' })
}
}
// ==================== AES 加密 ====================
async function testAesEncrypt() {
if (aesContent.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥和 IV
const key = await SM.aesGenerateKeyAsync()
aesKey.value = { key: key.key, iv: key.iv }
console.log("AES 密钥:", key)
// 加密
const encryptResult = await SM.aesEncryptAsync(aesContent.value, key.key, key.iv)
aesEncrypted.value = encryptResult.result
console.log("AES 加密成功:", encryptResult.result)
uni.showToast({ title: '加密成功', icon: 'success' })
} catch (e) {
console.error("AES 加密失败:", e)
uni.showToast({ title: '加密失败', icon: 'error' })
}
}
// ==================== AES 解密 ====================
async function testAesDecrypt() {
if (aesEncrypted.value.length==0 || aesKey.value.key.length==0) {
uni.showToast({ title: '请先加密', icon: 'none' })
return
}
try {
const decryptResult = await SM.aesDecryptAsync(aesEncrypted.value, aesKey.value.key, aesKey.value.iv)
aesDecrypted.value = decryptResult.result
console.log("AES 解密成功:", decryptResult.result)
if (decryptResult.result == aesContent.value) {
uni.showToast({ title: '解密成功且内容一致', icon: 'success' })
} else {
uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })
}
} catch (e) {
console.error("AES 解密失败:", e)
uni.showToast({ title: '解密失败', icon: 'error' })
}
}
// ==================== RSA 加密 ====================
async function testRsaEncrypt() {
if (rsaContent.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥对
const keyPair = await SM.rsaGenerateKeyPairAsync()
rsaKeyPair.value = {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey
}
console.log("RSA 公钥:", keyPair.publicKey)
console.log("RSA 私钥:", keyPair.privateKey)
// 加密
const encryptResult = await SM.rsaEncryptAsync(rsaContent.value, keyPair.publicKey)
rsaEncrypted.value = encryptResult.result
console.log("RSA 加密结果:", encryptResult.result)
uni.showToast({ title: '加密成功', icon: 'success' })
} catch (e) {
console.error("RSA 加密失败:", e)
uni.showToast({ title: '加密失败', icon: 'error' })
}
}
// ==================== RSA 解密 ====================
async function testRsaDecrypt() {
if (rsaEncrypted.value.length==0 || rsaKeyPair.value.privateKey.length==0) {
uni.showToast({ title: '请先加密', icon: 'none' })
return
}
try {
const decryptResult = await SM.rsaDecryptAsync(rsaEncrypted.value, rsaKeyPair.value.privateKey)
rsaDecrypted.value = decryptResult.result
console.log("RSA 解密结果:", decryptResult.result)
if (decryptResult.result == rsaContent.value) {
uni.showToast({ title: '解密成功且内容一致', icon: 'success' })
} else {
uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })
}
} catch (e) {
console.error("RSA 解密异常:", e)
uni.showToast({ title: '解密异常', icon: 'error' })
}
}
// ==================== RSA 签名 ====================
async function testRsaSign() {
if (rsaSignContent.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥对
const keyPair = await SM.rsaGenerateKeyPairAsync()
rsaSignKeyPair.value = {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey
}
console.log("RSA 公钥:", keyPair.publicKey)
console.log("RSA 私钥:", keyPair.privateKey)
// 签名
const signResult = await SM.rsaSignAsync(rsaSignContent.value, keyPair.privateKey)
rsaSignature.value = signResult.signature
console.log("RSA 签名成功:", signResult.signature)
uni.showToast({ title: '签名成功', icon: 'success' })
} catch (e) {
console.error("RSA 签名失败:", e)
uni.showToast({ title: '签名失败', icon: 'error' })
}
}
// ==================== RSA 验签 ====================
async function testRsaVerify() {
if (rsaSignature.value.length==0 || rsaSignKeyPair.value.publicKey.length==0) {
uni.showToast({ title: '请先签名', icon: 'none' })
return
}
try {
// 验签
const verifyResult = await SM.rsaVerifyAsync(rsaSignContent.value, rsaSignature.value, rsaSignKeyPair.value.publicKey)
rsaVerifyResult.value = verifyResult.valid
console.log("RSA 验签结果:", verifyResult.valid)
if (verifyResult.valid) {
uni.showToast({ title: '验签成功', icon: 'success' })
} else {
uni.showToast({ title: '验签失败', icon: 'error' })
}
} catch (e) {
console.error("RSA 验签异常:", e)
uni.showToast({ title: '验签异常', icon: 'error' })
}
}
// ==================== SM2 签名 ====================
async function testSm2Sign() {
if (sm2SignContent.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥对
const keyPair = await SM.sm2GenerateKeyPairAsync()
sm2KeyPair.value = {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey
}
console.log("SM2 公钥:", keyPair.publicKey)
console.log("SM2 私钥:", keyPair.privateKey)
// 签名
const signResult = await SM.sm2SignAsync(sm2SignContent.value, keyPair.privateKey)
sm2Signature.value = signResult.signature
console.log("SM2 签名成功:", signResult.signature)
uni.showToast({ title: '签名成功', icon: 'success' })
} catch (e) {
console.error("SM2 生成密钥对失败:", e)
uni.showToast({ title: '生成密钥对失败', icon: 'error' })
}
}
// ==================== SM2 验签 ====================
async function testSm2Verify() {
if (sm2Signature.value.length==0 || sm2KeyPair.value.publicKey.length==0) {
uni.showToast({ title: '请先签名', icon: 'none' })
return
}
try {
// 验签
const verifyResult = await SM.sm2VerifyAsync(sm2SignContent.value, sm2Signature.value, sm2KeyPair.value.publicKey)
sm2VerifyResult.value = verifyResult.valid
console.log("SM2 验签结果:", verifyResult.valid)
if (verifyResult.valid) {
uni.showToast({ title: '验签成功', icon: 'success' })
} else {
uni.showToast({ title: '验签失败', icon: 'error' })
}
} catch (e) {
console.error("SM2 验签异常:", e)
uni.showToast({ title: '验签异常', icon: 'error' })
}
}
// ==================== SM2 加密 ====================
async function testSm2Encrypt() {
if (sm2EncryptContent.value.length==0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
// 生成密钥对
const keyPair = await SM.sm2GenerateKeyPairAsync()
sm2EncryptKeyPair.value = {
publicKey: keyPair.publicKey,
privateKey: keyPair.privateKey
}
console.log("SM2 加密公钥:", keyPair.publicKey)
console.log("SM2 加密私钥:", keyPair.privateKey)
// 加密
const encryptResult = await SM.sm2EncryptAsync(sm2EncryptContent.value, keyPair.publicKey)
sm2Encrypted.value = encryptResult.encrypted
console.log("SM2 加密结果:", encryptResult.encrypted)
uni.showToast({ title: '加密成功', icon: 'success' })
} catch (e) {
console.error("SM2 生成密钥对失败:", e)
uni.showToast({ title: '生成密钥对失败', icon: 'error' })
}
}
// ==================== SM2 解密 ====================
async function testSm2Decrypt() {
if (sm2Encrypted.value.length==0 || sm2EncryptKeyPair.value.privateKey.length==0) {
uni.showToast({ title: '请先加密', icon: 'none' })
return
}
try {
const decryptResult = await SM.sm2DecryptAsync(sm2Encrypted.value, sm2EncryptKeyPair.value.privateKey)
sm2Decrypted.value = decryptResult.decrypted
console.log("SM2 解密结果:", decryptResult.decrypted)
if (decryptResult.decrypted == sm2EncryptContent.value) {
uni.showToast({ title: '解密成功且内容一致', icon: 'success' })
} else {
uni.showToast({ title: '解密成功但内容不一致', icon: 'error' })
}
} catch (e) {
console.error("SM2 解密异常:", e)
uni.showToast({ title: '解密异常', icon: 'error' })
}
}
// ==================== SM3 哈希 ====================
async function testSm3Hash() {
if (sm3Content.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
try {
const hashResult = await SM.sm3HashAsync(sm3Content.value)
sm3HashResult.value = hashResult.resultHash
console.log("SM3 哈希成功:", hashResult.resultHash)
uni.showToast({ title: '哈希成功', icon: 'success' })
} catch (e) {
console.error("SM3 哈希失败:", e)
uni.showToast({ title: '哈希失败', icon: 'error' })
}
}
// ==================== HMAC-SM3 哈希 ====================
async function testSm3HmacHash() {
if (sm3HmacContent.value.length == 0) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
if (sm3HmacKey.value.length == 0) {
uni.showToast({ title: '请输入HMAC密钥', icon: 'none' })
return
}
try {
const hmacHashResult = await SM.sm3HmacHashAsync(sm3HmacContent.value, sm3HmacKey.value)
sm3HmacHashResult.value = hmacHashResult.resultHash
sm3HmacExpectedHash.value = hmacHashResult.resultHash
sm3HmacVerifyKey.value = sm3HmacKey.value
sm3HmacVerifyContent.value = sm3HmacContent.value
console.log("HMAC-SM3 哈希成功:", hmacHashResult.resultHash)
uni.showToast({ title: 'HMAC-SM3 哈希成功', icon: 'success' })
} catch (e) {
console.error("HMAC-SM3 哈希失败:", e)
uni.showToast({ title: 'HMAC-SM3 哈希失败', icon: 'error' })
}
}
// ==================== HMAC-SM3 验证(输入期望哈希值) ====================
async function testSm3HmacHashWithExpected() {
if (sm3HmacVerifyContent.value.length == 0) {
uni.showToast({ title: '请输入原始内容', icon: 'none' })
return
}
try {
// 计算实际 HMAC-SM3 哈希值
const res = await SM.sm3HmacVerifyAsync(sm3HmacVerifyContent.value, sm3HmacVerifyKey.value,sm3HmacHashResult.value)
if (res) {
uni.showToast({ title: 'HMAC-SM3 哈希值匹配', icon: 'success' })
} else {
uni.showToast({ title: 'HMAC-SM3 哈希值不匹配', icon: 'error' })
}
} catch (e) {
console.error("HMAC-SM3 哈希验证异常:", e)
uni.showToast({ title: '验证异常', icon: 'error' })
}
}
// ==================== SM3 验证 ====================
async function testSm3Verify() {
if (sm3HashResult.value.length == 0 || sm3Content.value.length == 0) {
uni.showToast({ title: '请先计算哈希', icon: 'none' })
return
}
try {
// 重新计算哈希并验证
const res = await SM.sm3VerifyAsync(sm3VerifyContent.value,sm3HashResult.value)
sm3VerifyResult.value = res
console.log("SM3 验证结果:", sm3VerifyResult.value)
if (sm3VerifyResult.value!) {
uni.showToast({ title: '验证成功', icon: 'success' })
} else {
uni.showToast({ title: '验证失败', icon: 'error' })
}
} catch (e) {
console.error("SM3 验证异常:", e)
uni.showToast({ title: '验证异常', icon: 'error' })
}
}
// ==================== 选择文件并计算 SM3 哈希 ====================
async function processFileHash(filePath: string, fileSize?: number) {
// 计算文件 SM3 哈希
uni.showLoading({ title: '计算中...' })
try {
console.log("开始计算文件 SM3 哈希")
console.log("文件路径:", filePath)
const result = await SM.sm3FileHashAsync(filePath)
sm3FileHashResult.value = result.resultHash
console.log("文件 SM3 哈希成功:", result.resultHash)
uni.hideLoading()
uni.showToast({ title: '文件哈希成功', icon: 'success' })
} catch (e) {
console.error("文件 SM3 哈希失败:", e)
uni.hideLoading()
uni.showToast({ title: '文件哈希失败', icon: 'error' })
}
}
function chooseFileAndHash() {
uni.chooseFile({
count: 1,
success: (res) => {
const tempFiles = res.tempFiles
if (tempFiles != null && tempFiles.length > 0) {
const file = tempFiles[0]
const fileName = file.name
const filePath = file.path
const fileSize = file.size
sm3FileName.value = fileName != null ? fileName : "未知文件"
if (filePath != null) {
processFileHash(filePath, fileSize)
}
}
},
fail: (err) => {
console.error("选择文件失败:", err)
if (err.errMsg != null && !err.errMsg.includes('cancel')) {
uni.showToast({ title: '选择文件失败', icon: 'error' })
}
}
})
}
// ==================== 选择相册图片并计算 SM3 哈希 ====================
function chooseImageAndHash() {
uni.chooseImage({
count: 1,
sizeType: ['original'], // 只选择原图,不压缩
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePaths = res.tempFilePaths
if (tempFilePaths != null && tempFilePaths.length > 0) {
const filePath = tempFilePaths[0]
const fileName = "图片文件"
sm3FileName.value = fileName
console.log("选择的图片(原图):", filePath)
if (filePath != null) {
// 调用异步函数处理文件哈希
processFileHash(filePath)
}
}
},
fail: (err) => {
console.error("选择图片失败:", err)
if (err.errMsg != null && !err.errMsg.includes('cancel')) {
uni.showToast({ title: '选择图片失败', icon: 'error' })
}
}
})
}
const goToDetail = (orderId: number, status: string) => {
uni.setStorageSync('fromTabBar', true)
uni.navigateTo({
url: `/pages/detail/detail?orderId=${orderId}&status=${status}`,
success: () => {
console.log(`跳转到order${orderId}的详情页`)
}
})
}
function btn (){
}
</script>
<style scoped>
.order-content {
flex: 1;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.order-header {
height: 48px;
background-color: #fff;
align-items: center;
justify-content: center;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: #eee;
}
.order-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.order-list {
flex: 1;
padding: 10px;
}
/* 区块样式 */
.section {
background-color: #fff;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom-width: 1px;
border-bottom-style: solid;
border-bottom-color: #eee;
}
.input-group {
margin-bottom: 15px;
}
.label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
}
.input {
height: 40px;
border-width: 1px;
border-style: solid;
border-color: #ddd;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
background-color: #fafafa;
}
/* 按钮样式 */
.btn {
height: 44px;
border-radius: 4px;
font-size: 15px;
margin-bottom: 10px;
}
.btn-primary {
background-color: #007aff;
color: #fff;
}
.btn-secondary {
background-color: #34c759;
color: #fff;
}
.btn:disabled {
opacity: 0.5;
}
/* 结果展示框 */
.result-box {
margin-top: 10px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 4px;
border-left-width: 3px;
border-left-style: solid;
border-left-color: #007aff;
}
.result-label {
font-size: 12px;
color: #999;
margin-bottom: 5px;
}
.result-value {
font-size: 13px;
color: #333;
line-height: 1.5;
}
.result-value.small {
font-size: 11px;
}
.result-value.success {
color: #34c759;
font-weight: bold;
}
.result-value.error {
color: #ff3b30;
font-weight: bold;
}
.divider {
height: 1px;
background-color: #eee;
margin: 15px 0;
}
.sub-title {
font-size: 14px;
font-weight: bold;
color: #666;
margin-bottom: 10px;
}
</style>
服务端(java工具类)
package top.sunrains;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.crypto.signers.SM2Signer;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.util.encoders.Hex;
import java.math.BigInteger;
import java.security.SecureRandom;
/**
* <dependency>
* <groupId>org.bouncycastle</groupId>
* <artifactId>bcprov-jdk15to18</artifactId>
* <version>1.69</version>
* </dependency>
*/
public class SM2Util {
private static final ECParameterSpec CURVE = ECNamedCurveTable.getParameterSpec("sm2p256v1");
private static final ECDomainParameters DOMAIN = new ECDomainParameters(
CURVE.getCurve(),
CURVE.getG(),
CURVE.getN(),
CURVE.getH()
);
/**
* 生成SM2密钥对
*
* @return Map包含publicKey和privateKey,均为小写十六进制字符串
*/
public static java.util.Map<String, String> generateKeyPair() {
try {
BigInteger d = new BigInteger(256, new SecureRandom()).mod(DOMAIN.getN());
org.bouncycastle.math.ec.ECPoint q = DOMAIN.getG().multiply(d).normalize();
String x = String.format("4x", q.getAffineXCoord().toBigInteger());
String y = String.format("4x", q.getAffineYCoord().toBigInteger());
String publicKey = "04" + x + y;
String privateKey = String.format("4x", d);
java.util.Map<String, String> keyMap = new java.util.HashMap<>();
keyMap.put("publicKey", publicKey.toLowerCase());
keyMap.put("privateKey", privateKey.toLowerCase());
return keyMap;
} catch (Exception e) {
System.err.println("密钥生成失败: " + e.getMessage());
e.printStackTrace();
java.util.Map<String, String> keyMap = new java.util.HashMap<>();
keyMap.put("publicKey", "");
keyMap.put("privateKey", "");
return keyMap;
}
}
/**
* SM2签名
*
* @param data 待签名的原始数据
* @param privateKeyHex 私钥(64字符十六进制)
* @return 签名结果(DER格式,小写十六进制)
*/
public static String sign(String data, String privateKeyHex) {
try {
byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);
BigInteger d = new BigInteger(privateKeyHex, 16);
ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);
SM2Signer signer = new SM2Signer(new SM3Digest());
signer.init(true, priKey);
signer.update(msgBytes, 0, msgBytes.length);
byte[] sig = signer.generateSignature();
return Hex.toHexString(sig).toLowerCase();
} catch (Exception e) {
System.err.println("签名失败: " + e.getMessage());
e.printStackTrace();
return "";
}
}
/**
* SM2验签
*
* @param data 原始数据
* @param signatureHex 签名(DER格式,十六进制)
* @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)
* @return 验签是否成功
*/
public static boolean verify(String data, String signatureHex, String publicKeyHex) {
try {
byte[] msgBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);
String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;
BigInteger x = new BigInteger(xy.substring(0, 64), 16);
BigInteger y = new BigInteger(xy.substring(64, 128), 16);
org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();
ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);
SM2Signer signer = new SM2Signer(new SM3Digest());
signer.init(false, pubKey);
signer.update(msgBytes, 0, msgBytes.length);
byte[] sigBytes = Hex.decode(signatureHex);
return signer.verifySignature(sigBytes);
} catch (Exception e) {
System.err.println("验签失败: " + e.getMessage());
e.printStackTrace();
return false;
}
}
/**
* SM2加密
*
* @param data 待加密的原始数据(UTF-8字符串)
* @param publicKeyHex 公钥(04开头或不含04前缀的十六进制)
* @return 密文(HEX字符串,小写)
*/
public static String encrypt(String data, String publicKeyHex) {
try {
byte[] dataBytes = data.getBytes(java.nio.charset.StandardCharsets.UTF_8);
String xy = publicKeyHex.startsWith("04") ? publicKeyHex.substring(2) : publicKeyHex;
BigInteger x = new BigInteger(xy.substring(0, 64), 16);
BigInteger y = new BigInteger(xy.substring(64, 128), 16);
org.bouncycastle.math.ec.ECPoint pubPoint = DOMAIN.getCurve().createPoint(x, y).normalize();
ECPublicKeyParameters pubKey = new ECPublicKeyParameters(pubPoint, DOMAIN);
SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);
engine.init(true, new ParametersWithRandom(pubKey, new SecureRandom()));
byte[] encrypted = engine.processBlock(dataBytes, 0, dataBytes.length);
return Hex.toHexString(encrypted).toLowerCase();
} catch (Exception e) {
System.err.println("加密失败: " + e.getMessage());
e.printStackTrace();
return "";
}
}
/**
* SM2解密
*
* @param encryptedHex 密文(HEX字符串)
* @param privateKeyHex 私钥(64字符十六进制)
* @return 解密后的原始数据(UTF-8字符串)
*/
public static String decrypt(String encryptedHex, String privateKeyHex) {
try {
byte[] encryptedBytes = Hex.decode(encryptedHex);
BigInteger d = new BigInteger(privateKeyHex, 16);
ECPrivateKeyParameters priKey = new ECPrivateKeyParameters(d, DOMAIN);
SM2Engine engine = new SM2Engine(new SM3Digest(), SM2Engine.Mode.C1C3C2);
engine.init(false, priKey);
byte[] decrypted = engine.processBlock(encryptedBytes, 0, encryptedBytes.length);
return new String(decrypted, java.nio.charset.StandardCharsets.UTF_8);
} catch (Exception e) {
System.err.println("解密失败: " + e.getMessage());
e.printStackTrace();
return "";
}
}
/**
* 测试方法
*/
public static void main(String[] args) {
System.out.println("=== SM2 工具类测试 ===\n");
java.util.Map<String, String> keyPair = generateKeyPair();
String publicKey = keyPair.get("publicKey");
String privateKey = keyPair.get("privateKey");
System.out.println("公钥: " + publicKey);
System.out.println("私钥: " + privateKey);
System.out.println();
String testData = "123";
System.out.println("原始数据: " + testData);
String signature = sign(testData, privateKey);
System.out.println("签名: " + signature);
System.out.println();
boolean isValid = verify(testData, signature, publicKey);
System.out.println("验签结果: " + isValid);
System.out.println();
if (!isValid) {
System.err.println("验签失败!");
} else {
System.out.println("验签成功!");
}
System.out.println("\n=== SM2 加密解密测试 ===\n");
// 加密解密测试
String originalText = "Hello SM2";
System.out.println("原始数据: " + originalText);
String encrypted = encrypt(originalText, publicKey);
System.out.println("原始数据: " + originalText);
String decrypted = decrypt(encrypted, privateKey);
System.out.println("解密结果: " + decrypted);
System.out.println();
if (originalText.equals(decrypted)) {
System.out.println("加密解密成功!");
} else {
System.err.println("加密解密失败!");
}
}
}
package top.sunrains;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.security.Security;
/**
* <dependency>
* <groupId>org.bouncycastle</groupId>
* <artifactId>bcprov-jdk15to18</artifactId>
* <version>1.69</version>
* </dependency>
*
* SM4国密算法工具类
* 使用Bouncy Castle原生API
* 配置信息:
* - 算法:SM4
* - 模式:CBC(密码分组链接)
* - 填充:PKCS5Padding
* - 密钥长度:128位(16字节)
* - IV长度:128位(16字节)
*/
public class Sm4Util {
static {
try {
Security.addProvider(new BouncyCastleProvider());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
// String key = generateKeyHex();
// String iv = generateIvHex();
//String s = encryptHex("123", key, iv);
String key = "7f9a942a1aa85c331d3697a5369a37fe";
String iv = "cbc07750d33d6a3ef6d39fc7e7c4a876";
String s = "09187a80e9f52e7e962a3465ed70669e";
System.out.println(s);
String decryptedHex = decryptHex(s, key, iv);
System.out.println(decryptedHex);
}
/**
* 生成SM4密钥(16字节,128位)
* @return 十六进制编码的密钥字符串(32个字符)
*/
public static String generateKeyHex() {
byte[] keyBytes = new byte[16];
new SecureRandom().nextBytes(keyBytes);
return byteArrayToHex(keyBytes);
}
/**
* 生成IV向量(16字节,128位)
* @return 十六进制编码的IV字符串(32个字符)
*/
public static String generateIvHex() {
byte[] ivBytes = new byte[16];
new SecureRandom().nextBytes(ivBytes);
return byteArrayToHex(ivBytes);
}
/**
* SM4-CBC加密(字符串到HEX)
* 标准加密方式
*
* @param plaintext 明文字符串(UTF-8)
* @param keyHex 十六进制密钥字符串
* @param ivHex 十六进制IV字符串
* @return 加密后的HEX字符串
* @throws Exception 加密异常
*/
public static String encryptHex(String plaintext, String keyHex, String ivHex) throws Exception {
byte[] key = decodeKeyHex(keyHex);
byte[] iv = decodeIvHex(ivHex);
byte[] data = plaintext.getBytes("UTF-8");
byte[] encrypted = encrypt(data, key, iv);
return byteArrayToHex(encrypted);
}
/**
* SM4-CBC加密
*
* @param data 原始数据字节数组
* @param key 密钥字节数组(16字节)
* @param iv IV字节数组(16字节)
* @return 加密后的字节数组
* @throws Exception 加密异常
*/
public static byte[] encrypt(byte[] data, byte[] key, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(key, "SM4"),
new IvParameterSpec(iv));
return cipher.doFinal(data);
}
/**
* byte[] 转 Hex
*
* @param bytes 字节数组
* @return HEX字符串(小写)
*/
public static String byteArrayToHex(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString().toLowerCase();
}
public static String decryptHex(String encryptedHex, String keyHex, String ivHex) throws Exception {
byte[] key = decodeKeyHex(keyHex);
byte[] iv = decodeIvHex(ivHex);
byte[] encryptedData = hexToByteArray(encryptedHex);
byte[] decrypted = decrypt(encryptedData, key, iv);
return new String(decrypted, "UTF-8");
}
/**
* SM4-CBC解密
*
* @param encryptedData 加密数据字节数组
* @param key 密钥字节数组(16字节)
* @param iv IV字节数组(16字节)
* @return 解密后的原始字节数组
* @throws Exception 解密异常
*/
public static byte[] decrypt(byte[] encryptedData, byte[] key, byte[] iv) throws Exception {
Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(key, "SM4"),
new IvParameterSpec(iv));
return cipher.doFinal(encryptedData);
}
/**
* 从十六进制字符串还原密钥字节数组
* @param keyHex 十六进制编码的密钥字符串
* @return 密钥字节数组(16字节)
*/
public static byte[] decodeKeyHex(String keyHex) {
return hexToByteArray(keyHex);
}
/**
* 从十六进制字符串还原IV字节数组
* @param ivHex 十六进制编码的IV字符串
* @return IV字节数组(16字节)
*/
public static byte[] decodeIvHex(String ivHex) {
return hexToByteArray(ivHex);
}
/**
* HEX 转 byte[]
*
* @param hex HEX字符串
* @return 字节数组
*/
public static byte[] hexToByteArray(String hex) {
if (hex == null || hex.length() == 0) {
return new byte[0];
}
int len = hex.length() / 2;
byte[] bytes = new byte[len];
for (int i = 0; i < len; i++) {
int high = Character.digit(hex.charAt(i * 2), 16);
int low = Character.digit(hex.charAt(i * 2 + 1), 16);
bytes[i] = (byte) ((high << 4) | low);
}
return bytes;
}
public static String stringToHex(String str) {
if (str == null || str.isEmpty()) {
return "";
}
byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);
return byteArrayToHex(bytes);
}
}
package top.sunrains;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
/**
* AES对称加密算法工具类
* 使用Java原生Crypto API
* 配置信息:
* - 算法:AES-256
* - 模式:CBC(密码分组链接)
* - 填充:PKCS5Padding
* - 密钥长度:256位(32字节)
* - IV长度:128位(16字节)
*/
public class AesUtil {
/**
* 生成AES密钥和IV
* @return Map包含key (32字符) 和 iv (16字符)
*/
public static Map<String, String> generateKey() {
try {
// 生成32字节的随机字符串作为密钥
String key = generateRandomString(32);
// 生成16字节的随机字符串作为IV
String iv = generateRandomString(16);
System.out.println("AES Key: " + key + " (长度: " + key.length() + ")");
System.out.println("AES IV: " + iv + " (长度: " + iv.length() + ")");
Map<String, String> result = new HashMap<>();
result.put("key", key);
result.put("iv", iv);
return result;
} catch (Exception e) {
System.err.println("AES 密钥生成失败: " + e.getMessage());
e.printStackTrace();
return new HashMap<>();
}
}
/**
* 生成指定长度的随机字符串
*/
private static String generateRandomString(int length) {
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
SecureRandom random = new SecureRandom();
StringBuilder result = new StringBuilder(length);
for (int i = 0; i < length; i++) {
result.append(chars.charAt(random.nextInt(chars.length())));
}
return result.toString();
}
/**
* AES-CBC加密
*
* @param plaintext 明文字符串(UTF-8)
* @param keyStr 密钥字符串(32字符)
* @param ivStr IV字符串(16字符)
* @return Base64编码的密文
* @throws Exception 加密异常
*/
public static String encrypt(String plaintext, String keyStr, String ivStr) throws Exception {
// 将字符串Key和IV转换为字节数组(UTF-8编码)
byte[] keyBytes = keyStr.getBytes("UTF-8");
byte[] ivBytes = ivStr.getBytes("UTF-8");
// 创建密钥和IV
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
// 创建Cipher对象(AES/CBC/PKCS5Padding)
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
// 执行加密
byte[] encrypted = cipher.doFinal(plaintext.getBytes("UTF-8"));
// 返回Base64编码的密文
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* AES-CBC解密
*
* @param encryptedBase64 Base64编码的密文
* @param keyStr 密钥字符串(32字符)
* @param ivStr IV字符串(16字符)
* @return 解密后的明文字符串
* @throws Exception 解密异常
*/
public static String decrypt(String encryptedBase64, String keyStr, String ivStr) throws Exception {
// 将字符串Key和IV转换为字节数组(UTF-8编码)
byte[] keyBytes = keyStr.getBytes("UTF-8");
byte[] ivBytes = ivStr.getBytes("UTF-8");
// 创建密钥和IV
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
// 创建Cipher对象(AES/CBC/PKCS5Padding)
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
// 将Base64密文解码为字节
byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64);
// 执行解密
byte[] decrypted = cipher.doFinal(encryptedBytes);
// 返回明文字符串
return new String(decrypted, "UTF-8");
}
/**
* 测试方法
*/
public static void main(String[] args) throws Exception {
System.out.println("=== AES 工具类测试 ===\n");
// 生成密钥
Map<String, String> keyMap = generateKey();
String key = keyMap.get("key");
String iv = keyMap.get("iv");
System.out.println("密钥: " + key);
System.out.println("IV: " + iv);
System.out.println();
// 加密测试
String originalText = "Hello AES";
System.out.println("原始数据: " + originalText);
String encrypted = encrypt(originalText, key, iv);
System.out.println("加密结果: " + encrypted);
System.out.println();
// 解密测试
String decrypted = decrypt(encrypted, key, iv);
System.out.println("解密结果: " + decrypted);
System.out.println();
if (originalText.equals(decrypted)) {
System.out.println("✅ 加密解密成功!");
} else {
System.err.println("❌ 加密解密失败!");
}
}
}
package top.sunrains;
import cn.hutool.core.text.CharSequenceUtil;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
/**
* SM3国密哈希算法工具类
* 使用Bouncy Castle原生API
* 功能:
* - 字符串哈希计算
* - 字节数组哈希计算
* - 文件哈希计算
* - HMAC-SM3计算
*/
public class SM3Util {
static {
try {
Security.addProvider(new BouncyCastleProvider());
} catch (Exception e) {
e.printStackTrace();
}
}
private static final int BUFFER_SIZE = 8192;
/**
* 计算字符串的SM3哈希值
*
* @param data 待哈希的字符串
* @return 哈希值(小写十六进制字符串,64个字符)
*/
public static String hash(String data) {
if (data == null) {
throw new IllegalArgumentException("Data cannot be null");
}
return hash(data.getBytes(java.nio.charset.StandardCharsets.UTF_8));
}
/**
* 验证字符串的SM3哈希值
*
* @param data 原始数据
* @param expectedHash 期望的哈希值
* @return 是否匹配
*/
public static boolean verify(String data, String expectedHash) {
String actualHash = hash(data);
return actualHash.equalsIgnoreCase(expectedHash);
}
/**
* 计算字节数组的SM3哈希值
*
* @param data 待哈希的字节数组
* @return 哈希值(小写十六进制字符串,64个字符)
*/
public static String hash(byte[] data) {
SM3Digest digest = new SM3Digest();
digest.update(data, 0, data.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return byteArrayToHex(result);
}
/**
* 计算文件的SM3哈希值(支持大文件)
*
* @param filePath 文件路径
* @return 哈希值(小写十六进制字符串,64个字符)
* @throws IOException 文件读取异常
*/
public static String hashFile(String filePath) throws IOException {
return hashFile(new File(filePath));
}
/**
* 计算文件的SM3哈希值(支持大文件)
*
* @param file 文件对象
* @return 哈希值(小写十六进制字符串,64个字符)
* @throws IOException 文件读取异常
*/
public static String hashFile(File file) throws IOException {
if (!file.exists()) {
throw new IOException("File not found: " + file.getAbsolutePath());
}
SM3Digest digest = new SM3Digest();
byte[] buffer = new byte[BUFFER_SIZE];
try (InputStream is = new FileInputStream(file)) {
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return byteArrayToHex(result);
}
/**
* 计算HMAC-SM3(基于密钥的哈希消息认证码)
*
* @param data 待计算的数据
* @param hexKey 密钥
* @return HMAC-SM3结果(小写十六进制字符串,64个字符)
*/
public static String hmac(String data, String hexKey) {
if (CharSequenceUtil.isBlank(hexKey)) {
throw new IllegalArgumentException("密钥不能为空");
}
return hmac(data.getBytes(java.nio.charset.StandardCharsets.UTF_8),
hexToByteArray(hexKey));
}
/**
* 计算HMAC-SM3(基于密钥的哈希消息认证码)
*
* @param data 待计算的字节数组
* @param hexKey hex密钥字节数组
* @return HMAC-SM3结果(小写十六进制字符串,64个字符)
*/
public static String hmac(byte[] data, byte[] hexKey) {
HMac hmac = new HMac(new SM3Digest());
hmac.init(new KeyParameter(hexKey));
hmac.update(data, 0, data.length);
byte[] result = new byte[hmac.getMacSize()];
hmac.doFinal(result, 0);
return byteArrayToHex(result);
}
/**
* byte[] 转 Hex
*
* @param bytes 字节数组
* @return HEX字符串(小写)
*/
private static String byteArrayToHex(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString().toLowerCase();
}
/**
* 测试方法
*/
public static void main(String[] args) throws IOException {
String testData = "12";
System.out.println("SM3: " + hash(testData));
String key = "1";
key = stringToHex(key);
System.out.println("HMAC-SM3 key: " + key);
String hmacResult = hmac(testData, key);
System.out.println("HMAC-SM3: " + hmacResult);
System.out.println("HMAC长度: " + hmacResult.length() + " 字符");
System.out.println("文件哈希值: " + hashFile("/Users/sun/1/t.jpg"));
}
public static String stringToHex(String str) {
if (str == null || str.isEmpty()) {
return "";
}
byte[] bytes = str.getBytes(java.nio.charset.StandardCharsets.UTF_8);
return byteArrayToHex(bytes);
}
/**
* HEX 转 byte[]
*
* @param hex HEX字符串
* @return 字节数组
*/
public static byte[] hexToByteArray(String hex) {
if (hex == null || hex.length() == 0) {
return new byte[0];
}
int len = hex.length() / 2;
byte[] bytes = new byte[len];
for (int i = 0; i < len; i++) {
int high = Character.digit(hex.charAt(i * 2), 16);
int low = Character.digit(hex.charAt(i * 2 + 1), 16);
bytes[i] = (byte) ((high << 4) | low);
}
return bytes;
}
}
各平台实现说明
Android 平台
SM2 实现:
-
依赖库:BouncyCastle (
org.bouncycastle:bcprov-jdk15on:1.70) -
特点:
- 使用
ECKeyPairGenerator生成密钥对 - 使用
SM2Signer进行签名和验签(DER 格式) - 使用
SM2Engine进行加密和解密(C1C3C2 格式) - 不依赖 JCA Provider,避免兼容性问题
- 使用
SM4 实现:
-
依赖库:BouncyCastle
-
特点:
- 使用
SM4Engine进行加密和解密 - CBC 模式 + PKCS7 填充
- 支持自定义密钥和 IV
- 使用
iOS 平台
实现方式:GMObjC 原生库
SM2 特点:
- 使用
GMSm2Utils进行所有操作 - 签名自动转换为 DER 格式以兼容其他平台
- 加密输出 C1C3C2 格式(04 开头)
- 支持跨平台互通(Android/HarmonyOS)
SM4 特点:
- 使用
GMSm4Utils进行加密解密 - CBC 模式 + PKCS7 填充
- 密钥和 IV 为 32 位十六进制字符串
HarmonyOS 平台
实现方式: @ohos.security.cryptoFramework
SM2 特点:
- 使用 HarmonyOS 原生 cryptoFramework API
- 签名使用 DER 格式
- 加密使用 C1C3C2 格式
- 完全符合国密标准
SM3 特点:
- 使用
createMd(MessageDigest) 进行普通哈希 - 使用手动实现 HMAC-SM3 (RFC 2104)
- 支持流式文件哈希处理 (4096 字节缓冲区)
- 不依赖
createMac,避免密钥参数问题
SM4 特点:
- 使用
createSymKeyGenerator生成密钥 - 使用
createCipher进行加密解密 - CBC 模式 + PKCS7 填充
- 支持 SM4_128 算法
AES 特点:
- 使用
crypto.createSymKeyGenerator('AES256')创建密钥生成器 - 使用
crypto.createCipher('AES|CBC|PKCS7')进行加解密 - CBC 模式 + PKCS7 填充
- 密钥和 IV 为随机字符串(UTF-8 编码)
H5 平台
实现方式:sm-crypto + Web Crypto API
特点:
- SM2/SM3/SM4:通过 npm 包
sm-crypto实现 - AES:使用浏览器原生 Web Crypto API 实现
- 所有功能均在浏览器环境中运行
- SM2 签名使用 DER 格式(
der: true) - SM2 默认 userId 为
'1234567812345678'
依赖安装:
npm install --save sm-crypto
微信小程序平台
实现方式:miniprogram-sm-crypto + crypto-js + jsrsasign
特点:
- 专为小程序优化的国密算法库
- 完全支持 SM2、SM4 和 AES
- SM2:密钥生成、签名、验签、加密、解密(使用 miniprogram-sm-crypto)
- SM4:密钥生成、加密、解密(CBC 模式,使用 miniprogram-sm-crypto)
- AES:密钥生成、加密、解密(CBC 模式,使用 crypto-js)
- 需要通过微信开发者工具构建 npm
依赖安装:
npm install --save miniprogram-sm-crypto crypto-js jsrsasign
重要:微信小程序 npm 构建步骤
- 确保项目根目录已安装上述依赖
- 打开微信开发者工具,勾选「详情」→「本地设置」→「使用 npm 模块」
- 点击菜单栏「工具」→「构建 npm」,生成
miniprogram_npm目录 - 重新编译运行小程序
注意事项
1. 密钥格式
SM2 密钥:
- 公钥:以
04开头的 130 位十六进制字符串(非压缩格式) - 私钥:64 位十六进制字符串
SM4 密钥:
- 密钥 (key) :32 位十六进制字符串(128 位)
- 初始向量 (IV) :32 位十六进制字符串(128 位)
AES 密钥:
- 密钥 (key) :32 字符随机字符串(字母+数字,UTF-8 编码后为 256 位)
- 初始向量 (IV) :16 字符随机字符串(字母+数字,UTF-8 编码后为 128 位)
2. 签名格式
- 所有平台的签名输出均为 DER 编码的十六进制字符串
- DER 格式确保跨平台兼容性
3. 哈希格式
SM3 哈希:
- 输出长度:64 位十六进制字符串 (256 位)
- 格式:小写十六进制
- 示例:
a1b2c3d4e5f6...(64 个字符)
HMAC-SM3 哈希:
- 输出长度:64 位十六进制字符串 (256 位)
- 密钥格式:十六进制字符串
- 格式:小写十六进制
4. 加密格式
SM2 加密:
- 输出格式:C1C3C2(以
04开头) - 兼容 Android BouncyCastle、iOS GMObjC、HarmonyOS cryptoFramework
SM4 加密:
- 模式:CBC
- 填充:PKCS7
- 输出:十六进制字符串
AES 加密:
- 模式:CBC
- 填充:PKCS5/PKCS7
- 密钥:32 字符随机字符串(UTF-8 编码后为 256 位)
- IV:16 字符随机字符串(UTF-8 编码后为 128 位)
- 输出:Base64 编码字符串
RSA 加密:
- 算法:RSA-OAEP with SHA-256 (对应 Java 的
RSA/ECB/OAEPWithSHA-256AndMGF1Padding) - 密钥长度:2048 位
- 私钥格式:PKCS#8 (Base64 编码)
- 公钥格式:X.509/SPKI (Base64 编码)
- 输出:Base64 编码字符串
- 跨平台兼容:Android/iOS/HarmonyOS/H5 完全互通
RSA 签名:
- 算法:SHA256withRSA (PKCS#1 v1.5)
- 私钥格式:PKCS#8 (Base64 编码)
- 公钥格式:X.509/SPKI (Base64 编码)
- 签名输出:Base64 编码字符串
- 跨平台兼容:Android/iOS/HarmonyOS/H5/微信小程序 完全互通
5. 跨平台兼容性
✅ 完全兼容:
- 各平台生成的 SM2 密钥对可以互相使用
- Android 平台签名的数据可以在 iOS/Harmony/H5 平台验证
- SM2 加密的数据可以在不同平台间解密
- SM4 加密的数据可以在 Android/iOS/Harmony 平台间加解密
- AES 加密的数据可以在所有平台间加解密(Android/iOS/HarmonyOS/H5/微信小程序)
- SM3 哈希值在所有平台保持一致(相同输入产生相同输出)
- HMAC-SM3 支持跨平台验证
- RSA-OAEP-SHA256 加密的数据可以在 Android/iOS/HarmonyOS/H5 平台间加解密
- SHA256withRSA 签名可以在所有平台间验签(包括微信小程序)
⚠️ 注意事项:
- 确保使用相同的哈希选项(默认启用)
- SM4/AES 加密和解密必须使用相同的密钥和 IV
- 微信小程序需要先构建 npm 才能使用
- SM3 文件哈希需要确保文件内容完全一致
- AES 密钥为 32 字符随机字符串,IV 为 16 字符随机字符串
- RSA 密钥长度为 2048 位,私钥为 PKCS#8 格式,公钥为 X.509/SPKI 格式
- RSA 加密使用 OAEP with SHA-256 填充,与其他平台互通
- RSA 签名使用 SHA256withRSA (PKCS#1 v1.5),所有平台兼容
- 微信小程序端暂不支持 RSA-OAEP-SHA256 加密解密,仅支持签名验签
成品微信药店商城小程序,合规开具电子处方,处方水印,二级分销,开源代码
**元岳科技自2021年起,专注药店商城小程序的研发与技术服务。已经服务全国上百家实体药店均有真实案例,欢迎联系客服咨询。
我们给您搭建的线上商城,已经经历过众多客户的使用与验证,市场反应良好,客户运营稳定,我们熟悉从准备材料到上架成功的全流程。
根据客户的反馈,不断完善系统功能自我进化,我们始终深耕药店的技术开发领域,做到专精。
主要功能:1,线上商城,已对接互联网医院,合规开具电子处方,提供处方药服务。
2,主图水印,自动给产品主图打上水印,合规不显示药品的功能和用量等信息
3,药品标签,分类别展示不同的产品标签,区分处方药,甲类otc,乙类otc.
4,合规要求,处方药设置营销活动,自动阻断,甲类OTC参加部分营销活动自动阻断。
5,小票打印,电子面单打印,快递发货,物流轨迹同步。
6,可提供渠道办理《互联网药品信息服务备案凭证》,全程协助指导,包上架成功。
。。。。。更多功能。。。。
技术栈:前端使用uniapp开发,后端php,thinkphp框架,前后端分离,二开更便捷。提供全靠开源代码,可定制开发。
电话:18678829759,同V.
**元岳科技自2021年起,专注药店商城小程序的研发与技术服务。已经服务全国上百家实体药店均有真实案例,欢迎联系客服咨询。
我们给您搭建的线上商城,已经经历过众多客户的使用与验证,市场反应良好,客户运营稳定,我们熟悉从准备材料到上架成功的全流程。
根据客户的反馈,不断完善系统功能自我进化,我们始终深耕药店的技术开发领域,做到专精。
主要功能:1,线上商城,已对接互联网医院,合规开具电子处方,提供处方药服务。
2,主图水印,自动给产品主图打上水印,合规不显示药品的功能和用量等信息
3,药品标签,分类别展示不同的产品标签,区分处方药,甲类otc,乙类otc.
4,合规要求,处方药设置营销活动,自动阻断,甲类OTC参加部分营销活动自动阻断。
5,小票打印,电子面单打印,快递发货,物流轨迹同步。
6,可提供渠道办理《互联网药品信息服务备案凭证》,全程协助指导,包上架成功。
。。。。。更多功能。。。。
技术栈:前端使用uniapp开发,后端php,thinkphp框架,前后端分离,二开更便捷。提供全靠开源代码,可定制开发。
电话:18678829759,同V.
苹果审核 3.2 被拒?大多数情况都离不开这三个原因,最后终于解决了,亲测有用
# 苹果审核 3.2 被拒,很多人其实从一开始就找错了方向
做 App 上架这些年,我发现一个很有意思的现象。
同样收到苹果 3.2 的反馈,有的人改一次就过了,有的人连续改了十几个版本还是过不了。
为什么?
因为大部分人收到 3.2 后,第一反应就是改代码。
实际上,3.2 往往不是代码问题,而是苹果在审核过程中对你的账号、业务、产品真实性产生了怀疑。
换句话说,苹果审核的不是某个按钮,也不是某个页面,而是在判断:
“你这个 App,到底是不是你描述的那个 App。”
从这个角度去理解,3.2 的问题其实可以归纳成三个层面。
---
# 第一种:苹果不相信你的账号
很多开发者觉得账号只是一个提交工具。
但在苹果眼里,账号本身就是产品的一部分。
如果一个账号曾经出现过违规记录,或者账号背后的开发环境、设备环境、网络环境存在异常,苹果首先怀疑的不是 App,而是开发者。
这就像一家店铺。
顾客进门之前,先看的是招牌。
如果招牌本身就有问题,后面卖什么已经不重要了。
所以很多项目明明功能正常,却总是卡在审核阶段。
真正的问题不在产品,而在账号信誉。
---
# 第二种:苹果不相信你的业务
很多团队会认为:
“这个业务别人能做,我为什么不能做?”
实际上苹果审核并不是参考别人。
苹果审核的是:
这个业务是否符合苹果对 App Store 的定位。
有些产品从运营角度没有问题,但从苹果的角度来看,可能存在诱导、营销过重、功能价值不明确或者商业模式风险较高的问题。
这时候开发者会不断修改界面、调整文案。
但审核结果依旧一样。
原因很简单。
苹果关注的不是你首页长什么样,而是你的业务本质是什么。
如果业务表达方式和产品定位出现偏差,再漂亮的 UI 也解决不了问题。
---
# 第三种:苹果不相信你提交的版本
这是近两年越来越常见的一种情况。
很多开发者认为审核是考试。
于是提交一个版本给审核,审核通过后再把真正的功能放出来。
但苹果认为审核不是考试。
审核是验货。
你送过去的是什么,用户下载到的就应该是什么。
如果审核版本和实际运营版本存在明显差异,无论通过后台配置、远程参数还是功能开关实现,本质上都会让苹果觉得:
“你给我看的和你准备卖给用户的不是同一个东西。”
一旦形成这种判断,后面的问题就不再是功能问题,而是信任问题。
---
# 在我看来,3.2 本质上只有一个原因
很多文章都在分析苹果审核规则。
但这些年处理下来,我越来越觉得:
3.2 本质上只有一个核心逻辑。
那就是苹果没有建立起对产品的信任。
账号有风险,信任降低。
业务表达不清晰,信任降低。
功能前后不一致,信任降低。
最终都会汇聚到同一个审核结果。
所以与其研究苹果为什么拒绝你,不如先思考:
苹果为什么不相信你。
当这个问题想明白了,很多整改方向自然就清晰了。
---
# 为什么我们的处理思路和别人不一样
很多服务商接到 3.2 项目后,第一步是改代码。
而我们的第一步通常是判断:
这个问题到底属于账号、业务还是产品。
因为这三种问题对应的是三套完全不同的解决方案。
账号问题看环境。
业务问题看定位。
功能问题看逻辑。
方向判断错了,再多修改都是无效工作。
这些年我们内部陆续搭建了上架档案系统、审核记录系统、代码检测系统、相似度检测系统以及环境管理体系。
这些工具最大的作用不是帮客户修改代码,而是帮助我们快速找到问题真正出现在哪个环节。
因为在苹果审核里面,找到问题,往往比解决问题更重要。
很多团队卡审几个月,最后发现只是一开始判断错了方向。
而这,恰恰是 3.2 最容易被忽略的地方。
# 苹果审核 3.2 被拒,很多人其实从一开始就找错了方向
做 App 上架这些年,我发现一个很有意思的现象。
同样收到苹果 3.2 的反馈,有的人改一次就过了,有的人连续改了十几个版本还是过不了。
为什么?
因为大部分人收到 3.2 后,第一反应就是改代码。
实际上,3.2 往往不是代码问题,而是苹果在审核过程中对你的账号、业务、产品真实性产生了怀疑。
换句话说,苹果审核的不是某个按钮,也不是某个页面,而是在判断:
“你这个 App,到底是不是你描述的那个 App。”
从这个角度去理解,3.2 的问题其实可以归纳成三个层面。
---
# 第一种:苹果不相信你的账号
很多开发者觉得账号只是一个提交工具。
但在苹果眼里,账号本身就是产品的一部分。
如果一个账号曾经出现过违规记录,或者账号背后的开发环境、设备环境、网络环境存在异常,苹果首先怀疑的不是 App,而是开发者。
这就像一家店铺。
顾客进门之前,先看的是招牌。
如果招牌本身就有问题,后面卖什么已经不重要了。
所以很多项目明明功能正常,却总是卡在审核阶段。
真正的问题不在产品,而在账号信誉。
---
# 第二种:苹果不相信你的业务
很多团队会认为:
“这个业务别人能做,我为什么不能做?”
实际上苹果审核并不是参考别人。
苹果审核的是:
这个业务是否符合苹果对 App Store 的定位。
有些产品从运营角度没有问题,但从苹果的角度来看,可能存在诱导、营销过重、功能价值不明确或者商业模式风险较高的问题。
这时候开发者会不断修改界面、调整文案。
但审核结果依旧一样。
原因很简单。
苹果关注的不是你首页长什么样,而是你的业务本质是什么。
如果业务表达方式和产品定位出现偏差,再漂亮的 UI 也解决不了问题。
---
# 第三种:苹果不相信你提交的版本
这是近两年越来越常见的一种情况。
很多开发者认为审核是考试。
于是提交一个版本给审核,审核通过后再把真正的功能放出来。
但苹果认为审核不是考试。
审核是验货。
你送过去的是什么,用户下载到的就应该是什么。
如果审核版本和实际运营版本存在明显差异,无论通过后台配置、远程参数还是功能开关实现,本质上都会让苹果觉得:
“你给我看的和你准备卖给用户的不是同一个东西。”
一旦形成这种判断,后面的问题就不再是功能问题,而是信任问题。
---
# 在我看来,3.2 本质上只有一个原因
很多文章都在分析苹果审核规则。
但这些年处理下来,我越来越觉得:
3.2 本质上只有一个核心逻辑。
那就是苹果没有建立起对产品的信任。
账号有风险,信任降低。
业务表达不清晰,信任降低。
功能前后不一致,信任降低。
最终都会汇聚到同一个审核结果。
所以与其研究苹果为什么拒绝你,不如先思考:
苹果为什么不相信你。
当这个问题想明白了,很多整改方向自然就清晰了。
---
# 为什么我们的处理思路和别人不一样
很多服务商接到 3.2 项目后,第一步是改代码。
而我们的第一步通常是判断:
这个问题到底属于账号、业务还是产品。
因为这三种问题对应的是三套完全不同的解决方案。
账号问题看环境。
业务问题看定位。
功能问题看逻辑。
方向判断错了,再多修改都是无效工作。
这些年我们内部陆续搭建了上架档案系统、审核记录系统、代码检测系统、相似度检测系统以及环境管理体系。
这些工具最大的作用不是帮客户修改代码,而是帮助我们快速找到问题真正出现在哪个环节。
因为在苹果审核里面,找到问题,往往比解决问题更重要。
很多团队卡审几个月,最后发现只是一开始判断错了方向。
而这,恰恰是 3.2 最容易被忽略的地方。
收起阅读 »
为什么很多 AI 写出来的代码,更容易收到苹果 4.3 拒绝?
最近接触了不少客户,发现一个比较明显的现象。
很多客户使用 AI 工具开发 App 后,测试运行都没问题,但一提交 App Store,没几天就收到苹果 4.3 的拒绝邮件。
不少人第一反应是:
"是不是苹果针对 AI 开发的 App?"
实际上并不是。
从我们这些年处理苹果上架的经验来看,苹果并不会因为你用了 AI 开发就拒绝你的应用,它关注的始终是应用本身的质量、独立性和价值。
但是,AI 生成代码确实更容易踩到 4.3 的一些风险点。
## 4.3 到底在审核什么?
很多开发者觉得 4.3 是代码问题。
实际上并不完全是。
苹果 4.3 更多是在判断:
你的 App 是不是一个真正独立的产品。
如果审核人员认为:
- 功能和市场上大量应用差不多
- 产品结构高度相似
- 页面布局没有明显区别
- 代码特征存在大量复用痕迹
那么就有可能被归类到 4.3。
简单理解就是:
苹果不希望 App Store 里面出现大量"换个名字就重新提交"的应用。
## 为什么 AI 开发更容易出现这种情况?
因为目前大部分 AI 生成代码的逻辑都比较接近。
比如让 AI 开发一个工具类 App。
它给出的方案通常都是:
首页
功能页
个人中心
设置页
再配上常见的网络请求封装、数据存储方案以及标准化目录结构。
从开发角度来说没有问题。
但从苹果审核角度来看,这种项目往往缺少自己的特点。
我们实际处理过不少案例。
客户觉得自己是重新开发的产品,但审核人员看到的却是:
又一个差不多的工具 App。
又一个差不多的资讯 App。
又一个差不多的商城 App。
产品价值没有体现出来,自然更容易进入人工深度审核。
## 代码相似度其实也是审核重点之一
很多人觉得只要界面改了就行。
实际上这些年苹果对于代码层面的检测能力一直在提升。
特别是一些批量生成的项目。
经常会出现:
- 相同的目录结构
- 相同的类名命名习惯
- 相同的资源组织方式
- 相同的业务逻辑流程
虽然不一定完全一样,但整体特征非常接近。
尤其是现在很多 AI 工具生成出来的项目,本身就带有固定模板特征。
如果再叠加多个账号、多次提交,就容易被系统识别出来。
## 很多问题其实出在产品包装上
这些年遇到的 4.3 案例里面,有相当一部分代码本身并没有问题。
问题出在产品包装。
比如:
应用名称没有特色;
截图内容过于普通;
功能介绍写得模糊;
审核备注过于简单;
产品场景表达不清楚;
用户价值无法体现。
开发者知道自己的产品是做什么的。
但审核人员并不知道。
当审核人员无法快速理解产品价值的时候,4.3 的概率就会明显增加。
## 为什么同样的项目,有的人能过,有的人过不了?
因为苹果审核从来不是只看代码。
它会综合判断:
- 产品定位
- 功能完整度
- 用户价值
- 账号历史
- 提交资料
- 代码结构
- 应用差异化程度
很多开发者把精力都放在开发上。
但实际上,上架审核本身也是一个专业环节。
## 我们这些年处理 4.3 的经验
这些年帮客户处理苹果审核的时候,我们发现真正有效的方法并不是单纯修改代码。
而是从多个维度一起优化:
首先检查代码结构是否存在明显模板化特征;
其次检查产品功能是否具备独立价值;
然后重新梳理应用定位和审核说明;
同时排查截图、元数据、隐私配置等细节问题;
最后再结合账号历史和提交环境进行整体评估。
很多客户反复提交十几次都过不了。
调整完这些内容之后,反而一次就通过了。
## 总结
AI 可以提高开发效率,这是毋庸置疑的。
但苹果审核看的从来不是代码是谁写的,而是你的 App 是否像一个真正独立、有价值的产品。
如果只是利用 AI 快速拼出一个应用框架,再简单修改一下界面就提交审核,那么收到 4.3 的概率确实会更高。
而如果在开发完成后,能够做好代码优化、产品差异化设计、审核资料整理以及风险排查,那么 AI 开发出来的项目同样可以顺利通过审核。
从我们这些年的上架经验来看,很多 4.3 并不是技术问题,而是产品和审核思路的问题。
最近接触了不少客户,发现一个比较明显的现象。
很多客户使用 AI 工具开发 App 后,测试运行都没问题,但一提交 App Store,没几天就收到苹果 4.3 的拒绝邮件。
不少人第一反应是:
"是不是苹果针对 AI 开发的 App?"
实际上并不是。
从我们这些年处理苹果上架的经验来看,苹果并不会因为你用了 AI 开发就拒绝你的应用,它关注的始终是应用本身的质量、独立性和价值。
但是,AI 生成代码确实更容易踩到 4.3 的一些风险点。
## 4.3 到底在审核什么?
很多开发者觉得 4.3 是代码问题。
实际上并不完全是。
苹果 4.3 更多是在判断:
你的 App 是不是一个真正独立的产品。
如果审核人员认为:
- 功能和市场上大量应用差不多
- 产品结构高度相似
- 页面布局没有明显区别
- 代码特征存在大量复用痕迹
那么就有可能被归类到 4.3。
简单理解就是:
苹果不希望 App Store 里面出现大量"换个名字就重新提交"的应用。
## 为什么 AI 开发更容易出现这种情况?
因为目前大部分 AI 生成代码的逻辑都比较接近。
比如让 AI 开发一个工具类 App。
它给出的方案通常都是:
首页
功能页
个人中心
设置页
再配上常见的网络请求封装、数据存储方案以及标准化目录结构。
从开发角度来说没有问题。
但从苹果审核角度来看,这种项目往往缺少自己的特点。
我们实际处理过不少案例。
客户觉得自己是重新开发的产品,但审核人员看到的却是:
又一个差不多的工具 App。
又一个差不多的资讯 App。
又一个差不多的商城 App。
产品价值没有体现出来,自然更容易进入人工深度审核。
## 代码相似度其实也是审核重点之一
很多人觉得只要界面改了就行。
实际上这些年苹果对于代码层面的检测能力一直在提升。
特别是一些批量生成的项目。
经常会出现:
- 相同的目录结构
- 相同的类名命名习惯
- 相同的资源组织方式
- 相同的业务逻辑流程
虽然不一定完全一样,但整体特征非常接近。
尤其是现在很多 AI 工具生成出来的项目,本身就带有固定模板特征。
如果再叠加多个账号、多次提交,就容易被系统识别出来。
## 很多问题其实出在产品包装上
这些年遇到的 4.3 案例里面,有相当一部分代码本身并没有问题。
问题出在产品包装。
比如:
应用名称没有特色;
截图内容过于普通;
功能介绍写得模糊;
审核备注过于简单;
产品场景表达不清楚;
用户价值无法体现。
开发者知道自己的产品是做什么的。
但审核人员并不知道。
当审核人员无法快速理解产品价值的时候,4.3 的概率就会明显增加。
## 为什么同样的项目,有的人能过,有的人过不了?
因为苹果审核从来不是只看代码。
它会综合判断:
- 产品定位
- 功能完整度
- 用户价值
- 账号历史
- 提交资料
- 代码结构
- 应用差异化程度
很多开发者把精力都放在开发上。
但实际上,上架审核本身也是一个专业环节。
## 我们这些年处理 4.3 的经验
这些年帮客户处理苹果审核的时候,我们发现真正有效的方法并不是单纯修改代码。
而是从多个维度一起优化:
首先检查代码结构是否存在明显模板化特征;
其次检查产品功能是否具备独立价值;
然后重新梳理应用定位和审核说明;
同时排查截图、元数据、隐私配置等细节问题;
最后再结合账号历史和提交环境进行整体评估。
很多客户反复提交十几次都过不了。
调整完这些内容之后,反而一次就通过了。
## 总结
AI 可以提高开发效率,这是毋庸置疑的。
但苹果审核看的从来不是代码是谁写的,而是你的 App 是否像一个真正独立、有价值的产品。
如果只是利用 AI 快速拼出一个应用框架,再简单修改一下界面就提交审核,那么收到 4.3 的概率确实会更高。
而如果在开发完成后,能够做好代码优化、产品差异化设计、审核资料整理以及风险排查,那么 AI 开发出来的项目同样可以顺利通过审核。
从我们这些年的上架经验来看,很多 4.3 并不是技术问题,而是产品和审核思路的问题。
收起阅读 »
uni-app路由管理神器:vue-router风格体验
@meng-xi/uni-router
为 uni-app 提供类似 vue-router 风格的路由管理系统(uni_modules 版本)。
特性
- vue-router 风格 API - 熟悉的
push/replace/back导航方式,零学习成本 - 路由守卫 - 全局前置守卫
beforeEach、解析守卫beforeResolve、后置钩子afterEach、路由独享守卫beforeEnter - 守卫超时保护 - 守卫未调用
next()时自动中止导航,超时时间可配置(guardTimeout) - 命名路由 - 通过
name进行导航,无需硬编码路径字符串 - 路由元信息 -
meta字段支持页面标题、权限标记、TabBar 标识等自定义数据 - uni API 拦截 - 拦截
uni.navigateTo等原生导航 API,确保守卫始终生效(interceptUniApi) - 路由状态同步 -
syncRoute()将路由状态与实际页面栈同步,处理物理返回键等非路由器导航 - 路由变化监听 -
onRouteChange()订阅路由状态变化,包括导航完成和状态同步 - RouterLink 组件 - 声明式导航组件,支持
push/replace模式和@error事件 - TypeScript 类型提示 - 通过模块增强为路由名称和路径提供自动补全和类型检查
- 错误处理 - 完整的
RouterError/NavigationFailure体系,支持onError全局捕获 - 组合式 API -
useRouter()/useRoute()在组件中便捷访问路由器 - uni_modules 集成 - 通过 uni_modules 方式安装,无需 npm,开箱即用
📖 完整文档:https://mengxi-studio.github.io/uni-router/
安装
uni_modules(推荐)
将 mxuni-router 目录复制到项目的 uni_modules 目录下:
src/
└── uni_modules/
└── mxuni-router/
├── js_sdk/
│ ├── index.js
│ ├── index.cjs
│ ├── index.d.ts
│ └── index.d.cts
├── components/
│ └── mxuni-router/
│ └── mxuni-router.vue
├── package.json
└── readme.md
npm
pnpm add @meng-xi/uni-router
npm 方式需将导入路径改为
@meng-xi/uni-router。
快速开始
1. 创建路由器
// main.ts
import { createSSRApp } from 'vue'
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'
import App from './App.vue'
const router = createRouter({
routes: [
{ path: 'pages/index/index', name: 'home', meta: { title: '首页' } },
{ path: 'pages/about/about', name: 'about', meta: { title: '关于', requireAuth: true } },
{ path: 'pages/user/user', name: 'user', meta: { title: '我的', isTab: true } }
],
strict: true
})
export function createApp() {
const app = createSSRApp(App)
app.use(router)
return { app }
}
2. 路由导航
import { useRouter, useRoute } from './uni_modules/mxuni-router/js_sdk/index.js'
// 在组件 setup 中使用
const router = useRouter()
const route = useRoute()
// 路径导航
await router.push('/pages/about/about')
await router.push({ path: '/pages/about/about', query: { id: '1' } })
// 命名导航
await router.push({ name: 'about' })
// 返回
await router.back()
await router.back(2) // 返回两级
3. 路由守卫
// 全局前置守卫 - 登录验证
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isLoggedIn()) {
next({ name: 'login', query: { redirect: to.fullPath } })
} else {
next()
}
})
// 全局后置钩子
router.afterEach((to, from) => {
console.log(`导航完成: ${from.path} → ${to.path}`)
})
4. 自动生成路由配置(推荐)
配合 @meng-xi/vite-plugin 的 generateRouter 插件,可从 pages.json 自动生成路由配置和类型声明:
pnpm add @meng-xi/vite-plugin -D
// vite.config.ts
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import { generateRouter } from '@meng-xi/vite-plugin'
export default defineConfig({
plugins: [
uni(),
generateRouter({
pagesJsonPath: 'src/pages.json',
outputPath: 'src/router.config.ts',
dts: true,
metaMapping: {
navigationBarTitleText: 'title',
requireAuth: 'requireAuth'
}
})
]
})
然后在 main.ts 中导入生成的路由配置:
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'
import routes from './router.config'
const router = createRouter({ routes })
API 概览
核心
| API | 说明 |
|---|---|
createRouter(options) |
创建路由器实例 |
useRouter() |
获取路由器实例(组合式 API) |
useRoute() |
获取当前路由位置(组合式 API) |
Router 实例方法
| 方法 | 说明 |
|---|---|
router.push(location) |
导航到新页面 |
router.replace(location) |
替换当前页面 |
router.back(delta?) |
返回上一页或多级页面 |
router.beforeEach(guard) |
注册全局前置守卫 |
router.beforeResolve(guard) |
注册全局解析守卫 |
router.afterEach(guard) |
注册全局后置钩子 |
router.onError(handler) |
注册错误处理回调 |
router.resolve(location) |
解析路由位置(不导航) |
router.getRoutes() |
获取所有路由配置 |
router.hasRoute(name) |
检查路由是否存在 |
router.isReady() |
等待路由器初始化完成 |
router.onRouteChange(listener) |
注册路由变化监听器 |
router.syncRoute() |
同步路由状态与实际页面栈 |
错误码
| 错误码 | 说明 |
|---|---|
NAVIGATION_ABORTED |
导航被守卫中止 |
NAVIGATION_CANCELLED |
导航被取消(守卫异常或重定向超限) |
NAVIGATION_DUPLICATED |
重复导航到当前位置 |
ROUTE_NOT_FOUND |
未找到匹配的路由 |
NAVIGATION_API_ERROR |
uni 导航 API 调用失败 |
SETUP_ERROR |
路由器初始化或使用方式错误 |
RouterOptions 配置项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
routes |
RouteConfig[] |
- | 路由配置列表,需与 pages.json 中的页面声明保持一致 |
strict |
boolean |
true |
是否启用严格模式,启用后未匹配的命名路由将抛出异常 |
interceptUniApi |
boolean |
false |
是否拦截 uni.navigateTo 等原生导航 API,启用后直接调用 uni API 将转由路由器处理,确保守卫生效 |
guardTimeout |
number |
10000 |
守卫超时时间(毫秒),超时后自动中止导航并输出警告,设为 0 可禁用 |
RouterLink 组件
声明式导航组件,对应 uni-app 的 <navigator>,自动通过路由器执行导航。
<!-- 路径导航 -->
<mxuni-router to="/pages/about/about">
<view>跳转到关于页</view>
</mxuni-router>
<!-- replace 模式 -->
<mxuni-router to="/pages/about/about" replace>
<view>替换当前页</view>
</mxuni-router>
<!-- 捕获导航失败 -->
<mxuni-router to="/pages/about/about" @error="onNavError">
<view>跳转</view>
</mxuni-router>
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
to |
RouteLocationRaw |
- | 目标路由位置 |
replace |
boolean |
false |
是否使用替换模式导航 |
hoverClass |
string |
'navigator-hover' |
按下时的样式类 |
hoverStopPropagation |
boolean |
false |
是否阻止祖先节点的点击态 |
hoverStartTime |
number |
50 |
按住后多久出现点击态(ms) |
hoverStayTime |
number |
600 |
手指松开后点击态保留时间(ms) |
| 事件 | 参数 | 说明 |
|---|---|---|
error |
NavigationFailure |
导航失败时触发 |
TypeScript 类型提示
启用 dts: true 后,generateRouter 插件自动生成类型声明文件,为路由导航提供类型安全:
// 路由名称自动补全
router.push({ name: 'pagesIndexIndex' }) // ✅ 自动补全
router.push({ name: 'invalidName' }) // ❌ 类型错误
// 路径自动补全
router.push({ path: '/pages/index/index' }) // ✅ 自动补全
router.push({ path: '/invalid/path' }) // ❌ 类型错误
与 pages.json 的关系
Uni Router 不替代 pages.json,而是与之配合使用:
| 职责 | pages.json | Uni Router |
|---|---|---|
| 页面注册 | 必须声明 | 不负责 |
| 路由导航 | uni.navigateTo 等 | push / replace / back |
| 路由守卫 | 不支持 | beforeEach 等 |
| 路由元信息 | 不支持 | meta 字段 |
| 命名路由 | 不支持 | name 字段 |
License
@meng-xi/uni-router
为 uni-app 提供类似 vue-router 风格的路由管理系统(uni_modules 版本)。
特性
- vue-router 风格 API - 熟悉的
push/replace/back导航方式,零学习成本 - 路由守卫 - 全局前置守卫
beforeEach、解析守卫beforeResolve、后置钩子afterEach、路由独享守卫beforeEnter - 守卫超时保护 - 守卫未调用
next()时自动中止导航,超时时间可配置(guardTimeout) - 命名路由 - 通过
name进行导航,无需硬编码路径字符串 - 路由元信息 -
meta字段支持页面标题、权限标记、TabBar 标识等自定义数据 - uni API 拦截 - 拦截
uni.navigateTo等原生导航 API,确保守卫始终生效(interceptUniApi) - 路由状态同步 -
syncRoute()将路由状态与实际页面栈同步,处理物理返回键等非路由器导航 - 路由变化监听 -
onRouteChange()订阅路由状态变化,包括导航完成和状态同步 - RouterLink 组件 - 声明式导航组件,支持
push/replace模式和@error事件 - TypeScript 类型提示 - 通过模块增强为路由名称和路径提供自动补全和类型检查
- 错误处理 - 完整的
RouterError/NavigationFailure体系,支持onError全局捕获 - 组合式 API -
useRouter()/useRoute()在组件中便捷访问路由器 - uni_modules 集成 - 通过 uni_modules 方式安装,无需 npm,开箱即用
📖 完整文档:https://mengxi-studio.github.io/uni-router/
安装
uni_modules(推荐)
将 mxuni-router 目录复制到项目的 uni_modules 目录下:
src/
└── uni_modules/
└── mxuni-router/
├── js_sdk/
│ ├── index.js
│ ├── index.cjs
│ ├── index.d.ts
│ └── index.d.cts
├── components/
│ └── mxuni-router/
│ └── mxuni-router.vue
├── package.json
└── readme.md
npm
pnpm add @meng-xi/uni-router
npm 方式需将导入路径改为
@meng-xi/uni-router。
快速开始
1. 创建路由器
// main.ts
import { createSSRApp } from 'vue'
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'
import App from './App.vue'
const router = createRouter({
routes: [
{ path: 'pages/index/index', name: 'home', meta: { title: '首页' } },
{ path: 'pages/about/about', name: 'about', meta: { title: '关于', requireAuth: true } },
{ path: 'pages/user/user', name: 'user', meta: { title: '我的', isTab: true } }
],
strict: true
})
export function createApp() {
const app = createSSRApp(App)
app.use(router)
return { app }
}
2. 路由导航
import { useRouter, useRoute } from './uni_modules/mxuni-router/js_sdk/index.js'
// 在组件 setup 中使用
const router = useRouter()
const route = useRoute()
// 路径导航
await router.push('/pages/about/about')
await router.push({ path: '/pages/about/about', query: { id: '1' } })
// 命名导航
await router.push({ name: 'about' })
// 返回
await router.back()
await router.back(2) // 返回两级
3. 路由守卫
// 全局前置守卫 - 登录验证
router.beforeEach((to, from, next) => {
if (to.meta.requireAuth && !isLoggedIn()) {
next({ name: 'login', query: { redirect: to.fullPath } })
} else {
next()
}
})
// 全局后置钩子
router.afterEach((to, from) => {
console.log(`导航完成: ${from.path} → ${to.path}`)
})
4. 自动生成路由配置(推荐)
配合 @meng-xi/vite-plugin 的 generateRouter 插件,可从 pages.json 自动生成路由配置和类型声明:
pnpm add @meng-xi/vite-plugin -D
// vite.config.ts
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import { generateRouter } from '@meng-xi/vite-plugin'
export default defineConfig({
plugins: [
uni(),
generateRouter({
pagesJsonPath: 'src/pages.json',
outputPath: 'src/router.config.ts',
dts: true,
metaMapping: {
navigationBarTitleText: 'title',
requireAuth: 'requireAuth'
}
})
]
})
然后在 main.ts 中导入生成的路由配置:
import { createRouter } from './uni_modules/mxuni-router/js_sdk/index.js'
import routes from './router.config'
const router = createRouter({ routes })
API 概览
核心
| API | 说明 |
|---|---|
createRouter(options) |
创建路由器实例 |
useRouter() |
获取路由器实例(组合式 API) |
useRoute() |
获取当前路由位置(组合式 API) |
Router 实例方法
| 方法 | 说明 |
|---|---|
router.push(location) |
导航到新页面 |
router.replace(location) |
替换当前页面 |
router.back(delta?) |
返回上一页或多级页面 |
router.beforeEach(guard) |
注册全局前置守卫 |
router.beforeResolve(guard) |
注册全局解析守卫 |
router.afterEach(guard) |
注册全局后置钩子 |
router.onError(handler) |
注册错误处理回调 |
router.resolve(location) |
解析路由位置(不导航) |
router.getRoutes() |
获取所有路由配置 |
router.hasRoute(name) |
检查路由是否存在 |
router.isReady() |
等待路由器初始化完成 |
router.onRouteChange(listener) |
注册路由变化监听器 |
router.syncRoute() |
同步路由状态与实际页面栈 |
错误码
| 错误码 | 说明 |
|---|---|
NAVIGATION_ABORTED |
导航被守卫中止 |
NAVIGATION_CANCELLED |
导航被取消(守卫异常或重定向超限) |
NAVIGATION_DUPLICATED |
重复导航到当前位置 |
ROUTE_NOT_FOUND |
未找到匹配的路由 |
NAVIGATION_API_ERROR |
uni 导航 API 调用失败 |
SETUP_ERROR |
路由器初始化或使用方式错误 |
RouterOptions 配置项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
routes |
RouteConfig[] |
- | 路由配置列表,需与 pages.json 中的页面声明保持一致 |
strict |
boolean |
true |
是否启用严格模式,启用后未匹配的命名路由将抛出异常 |
interceptUniApi |
boolean |
false |
是否拦截 uni.navigateTo 等原生导航 API,启用后直接调用 uni API 将转由路由器处理,确保守卫生效 |
guardTimeout |
number |
10000 |
守卫超时时间(毫秒),超时后自动中止导航并输出警告,设为 0 可禁用 |
RouterLink 组件
声明式导航组件,对应 uni-app 的 <navigator>,自动通过路由器执行导航。
<!-- 路径导航 -->
<mxuni-router to="/pages/about/about">
<view>跳转到关于页</view>
</mxuni-router>
<!-- replace 模式 -->
<mxuni-router to="/pages/about/about" replace>
<view>替换当前页</view>
</mxuni-router>
<!-- 捕获导航失败 -->
<mxuni-router to="/pages/about/about" @error="onNavError">
<view>跳转</view>
</mxuni-router>
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
to |
RouteLocationRaw |
- | 目标路由位置 |
replace |
boolean |
false |
是否使用替换模式导航 |
hoverClass |
string |
'navigator-hover' |
按下时的样式类 |
hoverStopPropagation |
boolean |
false |
是否阻止祖先节点的点击态 |
hoverStartTime |
number |
50 |
按住后多久出现点击态(ms) |
hoverStayTime |
number |
600 |
手指松开后点击态保留时间(ms) |
| 事件 | 参数 | 说明 |
|---|---|---|
error |
NavigationFailure |
导航失败时触发 |
TypeScript 类型提示
启用 dts: true 后,generateRouter 插件自动生成类型声明文件,为路由导航提供类型安全:
// 路由名称自动补全
router.push({ name: 'pagesIndexIndex' }) // ✅ 自动补全
router.push({ name: 'invalidName' }) // ❌ 类型错误
// 路径自动补全
router.push({ path: '/pages/index/index' }) // ✅ 自动补全
router.push({ path: '/invalid/path' }) // ❌ 类型错误
与 pages.json 的关系
Uni Router 不替代 pages.json,而是与之配合使用:
| 职责 | pages.json | Uni Router |
|---|---|---|
| 页面注册 | 必须声明 | 不负责 |
| 路由导航 | uni.navigateTo 等 | push / replace / back |
| 路由守卫 | 不支持 | beforeEach 等 |
| 路由元信息 | 不支持 | meta 字段 |
| 命名路由 | 不支持 | name 字段 |
License
收起阅读 »PC端微信小程序,切换tabBar卡死问题
初始排查方向:
- 微信API兼容性问题
- 组件兼容性问题
- 数据更新机制问题
排查过程:
在初步测试中,我们发现该小程序仅能在体验版环境下通过电脑端进行查看,而开发版本则无法正常打开,或使用自动预览功能,点击 预览->自动预览,可以选择启动 PC 自动预览,点击编译并预览,成功的话将在微信 PC 版上自动拉起小程序。
进一步的诊断显示,即使移除了微信API相关的更新代码,应用仍然出现卡顿现象。为精确定位问题源头,我们采取了逐页注释与标签级注释的方法逐步排除,最终确认问题是由于某公共状态管理中的数据引起。
具体而言,所有使用TabBar导航模式的页面以及登录页面均频繁调用同一接口,并基于此更新上述提及的公共状态管理中的数据,导致了性能瓶颈。
解决方案:
● 在短时间内减少对特定接口的请求频率。
● 在更新公共状态管理的数据时引入一致性检查逻辑,即只有当新获取的数据与现有数据存在差异时才执行更新操作。
● 同时该公共状态管理数据appConfig嵌套过深,如需使用appConfig中某个单一数据,请在store文件中的getters声明再引用。
初始排查方向:
- 微信API兼容性问题
- 组件兼容性问题
- 数据更新机制问题
排查过程:
在初步测试中,我们发现该小程序仅能在体验版环境下通过电脑端进行查看,而开发版本则无法正常打开,或使用自动预览功能,点击 预览->自动预览,可以选择启动 PC 自动预览,点击编译并预览,成功的话将在微信 PC 版上自动拉起小程序。
进一步的诊断显示,即使移除了微信API相关的更新代码,应用仍然出现卡顿现象。为精确定位问题源头,我们采取了逐页注释与标签级注释的方法逐步排除,最终确认问题是由于某公共状态管理中的数据引起。
具体而言,所有使用TabBar导航模式的页面以及登录页面均频繁调用同一接口,并基于此更新上述提及的公共状态管理中的数据,导致了性能瓶颈。
解决方案:
● 在短时间内减少对特定接口的请求频率。
● 在更新公共状态管理的数据时引入一致性检查逻辑,即只有当新获取的数据与现有数据存在差异时才执行更新操作。
● 同时该公共状态管理数据appConfig嵌套过深,如需使用appConfig中某个单一数据,请在store文件中的getters声明再引用。
【解决】oppo上架应用市场提示套用马甲,相似度过高的问题
一、开发者困境:遭遇“代码相似度过高”上架应用商店驳回
最近,我遇到了一件非常棘手的事情。自己精心开发的一款Android APP在提交到某主流应用商店进行审核时,被无情驳回了。审核反馈的理由非常刺眼:“代码相似度过高”或“代码相似度极高”、“疑似马甲”。
二、解决办法:使用“问顶安全”的Android应用加固顺利过审
在寻求解决方案的过程中,我接触到了深圳问顶安全科技有限公司(asktopsec.com)的APP加固服务。抱着试一试的心态,我使用了他们的Android APP加固产品对APK进行了加固处理,随后重新申请上架。
结果令人惊喜,再次提交审核后,应用商店顺利通过了审核,之前的“代码相似度过高”提示彻底消失。APP成功上架!
后面我咨询客服才了解到,这家公司的团队成员数十年安全经验,其独有的2大核心技术成功帮我上架成功:
1.随机生成加固特征
根据每个APP/版本 随机生成独一无二的加固特征。有效防止加固检测、自动化脱壳工具、病毒误报,为阻断特征识别场景而生。
2.APP矩阵防护
多维度、矩阵式对APP进行增强型加密或混淆,增加逆向分析难度。可以达到千人千面的防护效果。
三、总结
面对应用市场上架的严苛审核,选择专业的安全服务至关重要。如果你也面临代码相似度高、被误判定为马甲包或需要高等级的安全防护,深圳问顶安全科技有限公司是一个值得信赖的选择。他们的官网:asktopsec.com ,进去后就能看到醒目的“Android应用加固与安全检测平台”了,点进去开始加固你的App吧~
一、开发者困境:遭遇“代码相似度过高”上架应用商店驳回
最近,我遇到了一件非常棘手的事情。自己精心开发的一款Android APP在提交到某主流应用商店进行审核时,被无情驳回了。审核反馈的理由非常刺眼:“代码相似度过高”或“代码相似度极高”、“疑似马甲”。
二、解决办法:使用“问顶安全”的Android应用加固顺利过审
在寻求解决方案的过程中,我接触到了深圳问顶安全科技有限公司(asktopsec.com)的APP加固服务。抱着试一试的心态,我使用了他们的Android APP加固产品对APK进行了加固处理,随后重新申请上架。
结果令人惊喜,再次提交审核后,应用商店顺利通过了审核,之前的“代码相似度过高”提示彻底消失。APP成功上架!
后面我咨询客服才了解到,这家公司的团队成员数十年安全经验,其独有的2大核心技术成功帮我上架成功:
1.随机生成加固特征
根据每个APP/版本 随机生成独一无二的加固特征。有效防止加固检测、自动化脱壳工具、病毒误报,为阻断特征识别场景而生。
2.APP矩阵防护
多维度、矩阵式对APP进行增强型加密或混淆,增加逆向分析难度。可以达到千人千面的防护效果。
三、总结
面对应用市场上架的严苛审核,选择专业的安全服务至关重要。如果你也面临代码相似度高、被误判定为马甲包或需要高等级的安全防护,深圳问顶安全科技有限公司是一个值得信赖的选择。他们的官网:asktopsec.com ,进去后就能看到醒目的“Android应用加固与安全检测平台”了,点进去开始加固你的App吧~
收起阅读 »【开源】windows上传ipa、管理证书和描述文件,数据无需上传云端
Github:https://github.com/friend-nicen/appuploader
App Store Connect GUI
一款基于 Wails 和 Vue 3 开发的跨平台(macOS / Windows / Linux)App Store Connect 桌面可视化工具。
它通过直接调用 App Store Connect API 的底层机制,实现了本地化、图形化的证书、设备、Bundle ID 和配置文件的管理,提供了现代化的 SaaS Dashboard 体验。
特性
- 现代 UI: 基于 Vue 3 + TailwindCSS 实现的流畅响应式桌面端界面。
- 本地存储: 使用无 CGO 依赖的纯 Go SQLite 驱动
github.com/glebarez/sqlite本地加密存储 API Keys 等配置信息。 - 多账户管理: 支持配置多个 Issuer ID、Key ID 和 Private Key,支持无缝切换。
- 核心功能:
- Bundle ID 管理: 查看现有的 App IDs
- 证书管理 (Certificates): 查看各类证书信息
- 描述文件管理 (Profiles): 浏览 Provisioning Profiles
- 设备管理 (Devices): 查看和管理测试设备
技术栈
- 后端 (Go): Go 1.21+, Wails v2, GORM,
golang-jwt/jwt/v5 - 前端 (Web): Vue 3 (Composition API), Vue Router, TailwindCSS 3
- 数据库: SQLite (
github.com/glebarez/sqlite)
API 密钥申请指南
使用本工具前,需要先在 Apple App Store Connect 中创建 API 密钥。
1. 前置条件
- 拥有有效的 Apple Developer 账号($99/年)
- 登录 App Store Connect
2. 创建 API 密钥
- 打开 App Store Connect → 右上角 "我的账户" → "API 密钥"
- 点击 "生成 API 密钥"
- 勾选 "开发人员" 权限(
Developer Role),这是管理证书、描述文件等所需的最低权限 - 立即下载
.p8私钥文件(页面关闭后将无法再次下载,只能重新生成)
3. 获取三个关键信息
| 信息 | 位置 | 说明 |
|---|---|---|
| Issuer ID (发行者 ID) | API 密钥页面顶部 发行者 ID 字段 |
同一账户下所有密钥共享,格式为 xxxx-xxxx-xxxx-xxxx-xxxx |
| Key ID (密钥 ID) | 密钥列表中的 密钥 ID 列 |
每个密钥唯一,格式为 XXXXXXXXXX |
| Private Key (私钥) | 下载的 .p8 文件内容 |
以 -----BEGIN PRIVATE KEY----- 开头,-----END PRIVATE KEY----- 结尾 |
4. 权限说明
API 密钥支持以下角色,本工具需要 至少 开发人员 角色:
| 角色 | 可用功能 |
|---|---|
| 开发人员 | 查看和管理证书、Bundle ID、设备、描述文件 |
| 管理员 | 上述全部 + 管理 App、用户、财务等 |
| 财务 | 仅查看财务报告 |
5. 安全注意事项
- 私钥文件(.p8)仅下载一次,请妥善保管
- 建议为不同环境创建不同的 API 密钥(如开发 / 生产)
- 可在 App Store Connect 中随时 撤销 泄露的密钥
- 本工具将私钥存储在本地 SQLite 数据库中,不会上传到任何远程服务器
- 导出数据时导出的 JSON 文件包含完整私钥,请勿将导出的 JSON 暴露给他人
开发指南
本项目代码包含详尽的中英文注释,方便进行二次开发和功能扩展。
1. 环境准备
- 安装 Go 1.26+
- 安装 Node.js 18+
- 安装 Wails CLI:
go install github.com/wailsapp/wails/v2/cmd/wails@latest
2. 本地开发 (Dev Mode)
开发模式下,Wails 会启动一个本地 Web 服务器,并提供热重载 (Hot Reload) 支持。
# 确保在项目根目录运行
wails dev
前端代码位于 frontend/ 目录中,所有的 Go 暴露方法在 frontend/wailsjs/go/main/App.js 中自动生成,可以直接在 Vue 组件中以 Promise 的方式调用。
3. 编译打包 (Build)
编译生产版本时,Wails 会将前端代码打包并嵌入到最终的二进制执行文件中。
# 编译当前平台的应用
wails build
# 交叉编译 macOS (如果你在 Windows/Linux 上)
wails build -platform darwin/amd64,darwin/arm64
# 交叉编译 Windows
wails build -platform windows/amd64
编译成功后,产物会输出到 build/bin/ 目录下。
项目结构
/backend: Go 后端逻辑,包含 API 客户端和 SQLite 数据库模型。/frontend: Vue 3 前端代码。src/views: 各大功能模块的 Vue 页面组件。src/router: 页面路由配置。
app.go: Wails 的主生命周期文件,包含了绑定到前端的 Go 方法。main.go: Wails 应用启动入口。
License
MIT License
Github:https://github.com/friend-nicen/appuploader
App Store Connect GUI
一款基于 Wails 和 Vue 3 开发的跨平台(macOS / Windows / Linux)App Store Connect 桌面可视化工具。
它通过直接调用 App Store Connect API 的底层机制,实现了本地化、图形化的证书、设备、Bundle ID 和配置文件的管理,提供了现代化的 SaaS Dashboard 体验。
特性
- 现代 UI: 基于 Vue 3 + TailwindCSS 实现的流畅响应式桌面端界面。
- 本地存储: 使用无 CGO 依赖的纯 Go SQLite 驱动
github.com/glebarez/sqlite本地加密存储 API Keys 等配置信息。 - 多账户管理: 支持配置多个 Issuer ID、Key ID 和 Private Key,支持无缝切换。
- 核心功能:
- Bundle ID 管理: 查看现有的 App IDs
- 证书管理 (Certificates): 查看各类证书信息
- 描述文件管理 (Profiles): 浏览 Provisioning Profiles
- 设备管理 (Devices): 查看和管理测试设备
技术栈
- 后端 (Go): Go 1.21+, Wails v2, GORM,
golang-jwt/jwt/v5 - 前端 (Web): Vue 3 (Composition API), Vue Router, TailwindCSS 3
- 数据库: SQLite (
github.com/glebarez/sqlite)
API 密钥申请指南
使用本工具前,需要先在 Apple App Store Connect 中创建 API 密钥。
1. 前置条件
- 拥有有效的 Apple Developer 账号($99/年)
- 登录 App Store Connect
2. 创建 API 密钥
- 打开 App Store Connect → 右上角 "我的账户" → "API 密钥"
- 点击 "生成 API 密钥"
- 勾选 "开发人员" 权限(
Developer Role),这是管理证书、描述文件等所需的最低权限 - 立即下载
.p8私钥文件(页面关闭后将无法再次下载,只能重新生成)
3. 获取三个关键信息
| 信息 | 位置 | 说明 |
|---|---|---|
| Issuer ID (发行者 ID) | API 密钥页面顶部 发行者 ID 字段 |
同一账户下所有密钥共享,格式为 xxxx-xxxx-xxxx-xxxx-xxxx |
| Key ID (密钥 ID) | 密钥列表中的 密钥 ID 列 |
每个密钥唯一,格式为 XXXXXXXXXX |
| Private Key (私钥) | 下载的 .p8 文件内容 |
以 -----BEGIN PRIVATE KEY----- 开头,-----END PRIVATE KEY----- 结尾 |
4. 权限说明
API 密钥支持以下角色,本工具需要 至少 开发人员 角色:
| 角色 | 可用功能 |
|---|---|
| 开发人员 | 查看和管理证书、Bundle ID、设备、描述文件 |
| 管理员 | 上述全部 + 管理 App、用户、财务等 |
| 财务 | 仅查看财务报告 |
5. 安全注意事项
- 私钥文件(.p8)仅下载一次,请妥善保管
- 建议为不同环境创建不同的 API 密钥(如开发 / 生产)
- 可在 App Store Connect 中随时 撤销 泄露的密钥
- 本工具将私钥存储在本地 SQLite 数据库中,不会上传到任何远程服务器
- 导出数据时导出的 JSON 文件包含完整私钥,请勿将导出的 JSON 暴露给他人
开发指南
本项目代码包含详尽的中英文注释,方便进行二次开发和功能扩展。
1. 环境准备
- 安装 Go 1.26+
- 安装 Node.js 18+
- 安装 Wails CLI:
go install github.com/wailsapp/wails/v2/cmd/wails@latest
2. 本地开发 (Dev Mode)
开发模式下,Wails 会启动一个本地 Web 服务器,并提供热重载 (Hot Reload) 支持。
# 确保在项目根目录运行
wails dev
前端代码位于 frontend/ 目录中,所有的 Go 暴露方法在 frontend/wailsjs/go/main/App.js 中自动生成,可以直接在 Vue 组件中以 Promise 的方式调用。
3. 编译打包 (Build)
编译生产版本时,Wails 会将前端代码打包并嵌入到最终的二进制执行文件中。
# 编译当前平台的应用
wails build
# 交叉编译 macOS (如果你在 Windows/Linux 上)
wails build -platform darwin/amd64,darwin/arm64
# 交叉编译 Windows
wails build -platform windows/amd64
编译成功后,产物会输出到 build/bin/ 目录下。
项目结构
/backend: Go 后端逻辑,包含 API 客户端和 SQLite 数据库模型。/frontend: Vue 3 前端代码。src/views: 各大功能模块的 Vue 页面组件。src/router: 页面路由配置。
app.go: Wails 的主生命周期文件,包含了绑定到前端的 Go 方法。main.go: Wails 应用启动入口。
License
MIT License
收起阅读 »我是怎么实现ios内购后恢复购买的
// 在客户端app,定义一个获取历史苹果收据的方法如下:
function getIapOrders() {
return new Promise(async (resolve, reject) => {
try {
// 1. 导入 iOS 原生类
const NSBundle = plus.ios.importClass("NSBundle");
const NSData = plus.ios.importClass("NSData");
// 2. 获取收据 URL
const url = NSBundle.mainBundle().appStoreReceiptURL();
if (!url) {
uni.hideLoading();
uni.showToast({
title: '未找到收据路径',
icon: 'none'
});
reject()
return;
}
// 3. 读取收据数据
const receiptData = NSData.dataWithContentsOfURL(url);
if (receiptData) {
// 4. 【核心修改】使用 plus.ios.invoke 显式调用 base64EncodedStringWithOptions: 方法
// 注意:方法名后面的冒号 ":" 必须保留,代表这是一个带参数的方法
const base64Receipt = plus.ios.invoke(receiptData, "base64EncodedStringWithOptions:", 0);
if (base64Receipt) {
//加密过的,它是苹果官方为你在这个 App 里发生的所有成功交易出具的“电子发票”和“资产清单”。
console.log("成功获取到本地收据 Base64 数据", base64Receipt);
// 5. 发送给您的服务器,解密取出数据
uni.vk.callFunction({
url: 'client/order/pub/verifyReceipt',
data: {
transaction_receipt: base64Receipt
},
success(res) {
// 返回苹果给的交易记录
resolve(res.rows)
},
complete(res) {
}
});
} else {
uni.hideLoading();
uni.showToast({
title: '收据转换 Base64 失败',
icon: 'none'
});
reject();
}
} else {
uni.hideLoading();
uni.showToast({
title: '收据内容为空,请重试',
icon: 'none'
});
reject()
}
} catch (e) {
uni.hideLoading();
console.error("读取收据失败: ", e);
uni.showToast({
title: '读取凭证失败: ' + e.message,
icon: 'none'
});
reject()
}
})
}
'use strict';
const uniPay = require("uni-pay");
module.exports = {
/**
* 加密数据,它是苹果官方为你在这个 App 里发生的所有成功交易出具的“电子发票”和“资产清单”。
* 通过verifyReceipt数据校验后返回json交易数据
* @url client/order/pub/verifyReceipt 前端调用的url参数地址
* data 请求参数
* @param {String} params1 参数1
*/
main: async (event) => {
let { data = {}, userInfo, util, filterResponse, originalParam } = event;
let { customUtil, uniID, config, pubFun, vk, db, _, $ } = util;
let { uid } = data;
let res = { code: 0, msg: "" };
// 业务逻辑开始-----------------------------------------------------------
// 测式时的沙箱模式下
let uniPayInstance = uniPay.initAppleIapPayment({ provider: "appleiap", provider_pay_type: "app" ,sandbox:true});
let tradeRes = await uniPayInstance.verifyReceipt({
receiptData: data.transaction_receipt
});
// console.log("************ uniPayInstance **************",JSON.stringify(tradeRes))
res.rows=[];
if(tradeRes.tradeState == "SUCCESS"){
/**
* original_purchase_date、 original_purchase_date_ms、 original_transaction_id
* product_id、purchase_date、purchase_date_ms
* quantity
* transaction_id
*/
res.rows = tradeRes.receipt.in_app
}
debugger
// 业务逻辑结束-----------------------------------------------------------
return res;
}
}
// 在客户端app,定义一个获取历史苹果收据的方法如下:
function getIapOrders() {
return new Promise(async (resolve, reject) => {
try {
// 1. 导入 iOS 原生类
const NSBundle = plus.ios.importClass("NSBundle");
const NSData = plus.ios.importClass("NSData");
// 2. 获取收据 URL
const url = NSBundle.mainBundle().appStoreReceiptURL();
if (!url) {
uni.hideLoading();
uni.showToast({
title: '未找到收据路径',
icon: 'none'
});
reject()
return;
}
// 3. 读取收据数据
const receiptData = NSData.dataWithContentsOfURL(url);
if (receiptData) {
// 4. 【核心修改】使用 plus.ios.invoke 显式调用 base64EncodedStringWithOptions: 方法
// 注意:方法名后面的冒号 ":" 必须保留,代表这是一个带参数的方法
const base64Receipt = plus.ios.invoke(receiptData, "base64EncodedStringWithOptions:", 0);
if (base64Receipt) {
//加密过的,它是苹果官方为你在这个 App 里发生的所有成功交易出具的“电子发票”和“资产清单”。
console.log("成功获取到本地收据 Base64 数据", base64Receipt);
// 5. 发送给您的服务器,解密取出数据
uni.vk.callFunction({
url: 'client/order/pub/verifyReceipt',
data: {
transaction_receipt: base64Receipt
},
success(res) {
// 返回苹果给的交易记录
resolve(res.rows)
},
complete(res) {
}
});
} else {
uni.hideLoading();
uni.showToast({
title: '收据转换 Base64 失败',
icon: 'none'
});
reject();
}
} else {
uni.hideLoading();
uni.showToast({
title: '收据内容为空,请重试',
icon: 'none'
});
reject()
}
} catch (e) {
uni.hideLoading();
console.error("读取收据失败: ", e);
uni.showToast({
title: '读取凭证失败: ' + e.message,
icon: 'none'
});
reject()
}
})
}
'use strict';
const uniPay = require("uni-pay");
module.exports = {
/**
* 加密数据,它是苹果官方为你在这个 App 里发生的所有成功交易出具的“电子发票”和“资产清单”。
* 通过verifyReceipt数据校验后返回json交易数据
* @url client/order/pub/verifyReceipt 前端调用的url参数地址
* data 请求参数
* @param {String} params1 参数1
*/
main: async (event) => {
let { data = {}, userInfo, util, filterResponse, originalParam } = event;
let { customUtil, uniID, config, pubFun, vk, db, _, $ } = util;
let { uid } = data;
let res = { code: 0, msg: "" };
// 业务逻辑开始-----------------------------------------------------------
// 测式时的沙箱模式下
let uniPayInstance = uniPay.initAppleIapPayment({ provider: "appleiap", provider_pay_type: "app" ,sandbox:true});
let tradeRes = await uniPayInstance.verifyReceipt({
receiptData: data.transaction_receipt
});
// console.log("************ uniPayInstance **************",JSON.stringify(tradeRes))
res.rows=[];
if(tradeRes.tradeState == "SUCCESS"){
/**
* original_purchase_date、 original_purchase_date_ms、 original_transaction_id
* product_id、purchase_date、purchase_date_ms
* quantity
* transaction_id
*/
res.rows = tradeRes.receipt.in_app
}
debugger
// 业务逻辑结束-----------------------------------------------------------
return res;
}
}
收起阅读 »













