更新记录
1.2(2026-08-13) 下载此版本
注释部分日志打印
1.1.9(2026-08-11) 下载此版本
文档添加安卓端体验apk链接
1.1.8(2026-08-10) 下载此版本
get请求 请求头添加content-type
查看更多平台兼容性
uni-app x(4.0)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| √ | √ | √ | √ | √ | √ |
sunrains-request
客户端与服务端(java)之间通信数据加密、签名,保证数据安全与防篡改 支持平台:微信小程序、H5、Android、iOS、鸿蒙 请求方式支持:GET、POST(application/json、application/x-www-form-urlencoded、multipart/form-data)
体验h5端
点击下载体验安卓端
在使用插件过程中遇到问题 请进群交流
服务端示例支持 1:网络请求解解密,验签,返回结果加密;2:身份证ocr识别 3:人脸注册,识别,人证比对 4:活体检测 5:人脸核身 6:车牌号识别
点击查看H5、IOS、鸿蒙、微信小程序端录屏效果v1.1
模块目录结构
sunrains-request/
├── index.uts # 模块入口,统一导出
├── js_sdk/
│ ├── inutils/
│ │ └── requestApi.uts # 底层网络请求封装(uni.request / uni.uploadFile)
│ └── openutil/
│ ├── base64Util.uts # Base64 编解码工具
│ ├── randomUtil.uts # 随机字符串生成
│ └── request/
│ ├── Request.uts # 请求入口(ReqApi)
│ └── reqHandle/
│ ├── ReqHandle.uts # 请求处理抽象基类
│ ├── handle.uts # 策略工厂(根据 reqFlag 选择 Handler)
│ └── handle/
│ ├── VoidHandle.uts # 无加密无签名模式
│ ├── SignHandle.uts # 仅签名模式
│ ├── EncryptHandle.uts # 仅SM4加密模式
│ └── SignWithEncryptHandle.uts # 签名+SM4加密模式
导出 API 一览
| 导出函数 | 来源文件 | 说明 |
|---|---|---|
ReqApi |
request/Request.uts |
高层请求入口,自动根据 reqFlag 完成签名/加密/发送/解密 |
RequestApi |
inutils/requestApi.uts |
底层请求入口,直接发起 uni.request 并返回 Promise\<Response> |
uploadFile |
inutils/requestApi.uts |
文件上传,返回 Promise\<Response> |
stringToBase64 |
openutil/base64Util.uts |
字符串 → Base64 编码 |
base64ToString |
openutil/base64Util.uts |
Base64 → 字符串解码 |
random |
openutil/randomUtil.uts |
生成指定长度的随机字符串(字母+数字),默认16位 |
网络请求架构(核心)
整体流程
调用方
│
▼
ReqApi(frontVo: FrontRequestVo) ← 高层入口
│
├─ 1. selectHandle(reqFlag) ← 根据模式选择 Handler
│
├─ 2. handle.handleHeaderAndBody(vo) ← 构造请求头 + 处理请求体(签名/加密)
│
├─ 3. RequestApi(req) ← 底层发起 HTTP 请求
│
└─ 4. handle.handleRes(req, res) ← 处理响应(解密/验签)
│
▼
返回 Response
四种请求模式(ReqFlag)
通过 FrontRequestVo.reqFlag 指定,决定请求的安全处理策略:
| reqFlag | Handler | 签名 | 请求体SM4加密 | 响应SM4解密 | 需要priKey | 需要pubKey |
|---|---|---|---|---|---|---|
"void" |
VoidHandle | ✗ | ✗ | ✗ | ✗ | ✗ |
"sign" |
SignHandle | ✓ SM2签名 | ✗ | ✗ | ✓ | ✗ |
"sm4Encrypt" |
EncryptHandle | ✗ | ✓ SM4加密 | ✓ SM4解密 | ✗ | ✓ |
"signwithenSM2-Sm4Encrypt" |
SignWithEncryptHandle | ✓ SM2签名 | ✓ SM4加密 | ✓ SM4解密 | ✓ | ✓ |
请求头处理(所有模式共用)
无论哪种模式,handleSignHeader 都会执行以下操作:
- SM2加密SM4密钥:生成随机 SM4 key + iv,用服务端公钥(pubKey)通过 SM2 加密后放入
header["key"] - 条件签名(sign / signwithenSM2-Sm4Encrypt 模式):
- 生成
timestamp(时间戳)和nonceStr(16位随机串) - 将所有业务参数 + timestamp + nonceStr 按 key 字典序排列,拼接为
key=value&格式 - 使用客户端私钥(priKey)对拼接字符串进行 SM2 签名
- 签名结果放入
header["sign"],同时放入header["timestamp"]和header["nonceStr"]
- 生成
请求体处理(按 HTTP Method + contentType)
GET 请求:
void/sign模式:参数拼接到 URL query stringsm4Encrypt/signwithenSM2-Sm4Encrypt模式:参数 JSON 序列化后 SM4 加密,拼接到?data=加密结果
POST 请求(按 contentType):
| contentType | 处理方式 |
|---|---|
"json" |
Content-Type: application/json,加密模式下 body 整体 SM4 加密为 {"data":"加密结果"} |
"urlencoded" |
Content-Type: application/x-www-form-urlencoded,加密模式下 data=加密结果 |
"form-data" |
Content-Type: multipart/form-data |
响应处理
- 无加密模式(void / sign):直接返回 Response
- 加密模式(sm4Encrypt / signwithenSM2-Sm4Encrypt):
- 先校验外层
res.code == resSuccessCode()(当前定义为0) - 取出
res.data(SM4 加密的字符串) - 用本次请求生成的 SM4 key + iv 解密
- 解密结果为内层 Response,返回给调用方
- 先校验外层
底层请求(RequestApi)
RequestApi(req: RequestVo) → Promise<Response>
- 基于
uni.request封装 - 超时时间:
req.timeout ?? 6000ms - 成功(statusCode=200):resolve Response 对象
- Android 平台特殊处理:通过
JSON.parse(JSON.stringify(res.data))转换 - 其他平台:直接
res.data as Response
- Android 平台特殊处理:通过
- 失败(非200):reject
CustOtherFailError(包含 code + errorMsg) - 网络异常:reject
CustOtherFailError(包含 errCode + errMsg)
文件上传(uploadFile)
uploadFile(req: UploadFileOptions) → Promise<Response>
- 基于
uni.uploadFile封装 - 超时时间:
req.timeout ?? 12000ms - 成功时解析
res.data为 Response - 失败时 reject
err.errMsg
类型定义(依赖 sunrains-common-type)
FrontRequestVo(前置请求参数)
type FrontRequestVo = {
method: 'GET' | 'POST', // HTTP 方法
url: string, // 请求地址
data: UTSJSONObject | null, // 请求数据
header: UTSJSONObject | null, // 自定义请求头(会与签名/加密头合并)
reqFlag: ReqFlag, // 安全模式标识
priKey: string, // SM2 私钥(签名时使用)
pubKey: string, // SM2 公钥(加密SM4密钥时使用)
contentType: "json" | "urlencoded" | "form-data" | null, // POST内容类型
timeout: number | null, // 超时时间(ms)
filePathParamName: string | null // 上传文件时,文件路径的参数名
}
Response(统一响应)
type Response = {
success: boolean | null, // 是否成功
msg: string | null, // 提示信息
code: number, // 状态码(0=成功)
errorMsg: string | null, // 错误详情
data: any | null // 业务数据(加密模式下为密文字符串)
}
RequestVo(底层请求参数)
type RequestVo = {
url: string,
method?: 'GET' | 'POST' | null,
header: UTSJSONObject,
timeout?: number | null,
data: any
}
Sm4Key(SM4密钥)
type Sm4Key = {
key: string,
iv: string
}
错误处理(依赖 sunrains-common-err)
所有请求错误统一通过 UniError 抛出:
| 错误来源 | errSubject | errCode | message |
|---|---|---|---|
| SM2私钥为空 | SM2_PRI_KEY_BLANK | 预定义码 | 预定义信息 |
| SM2公钥为空 | SM2_PUB_KEY_BLANK | 预定义码 | 预定义信息 |
| SM2签名失败 | SM2_SIGN_FAIL | 预定义码 | 预定义信息 |
| HTTP非200 | "请求失败" | 服务端code | errorMsg / msg |
| 网络异常 | "请求失败" | err.errCode | err.errMsg |
| 加密模式响应码非0 | "请求失败" | res.code | errorMsg / msg |
调用方 catch 示例:
try {
const res = await ReqApi(frontVo)
} catch (err) {
// 跨平台安全取值(避免 iOS 上 err.message 崩溃)
console.log("请求异常:", `${err}`)
// 或通过 err.message 获取错误信息(err 为 UniError 实例)
}
工具函数
Base64 编解码
import { stringToBase64, base64ToString } from "@/uni_modules/sunrains-request"
const encoded = stringToBase64("Hello") // "SGVsbG8="
const decoded = base64ToString("SGVsbG8=") // "Hello"
随机字符串
import { random } from "@/uni_modules/sunrains-request"
const nonce = random(16) // 生成16位随机字符串
依赖模块
| 模块 | 用途 |
|---|---|
sunrains-smutil |
SM2签名/加解密、SM4加解密 |
sunrains-common-type |
类型定义(FrontRequestVo、Response、RequestVo、Sm4Key、ReqFlag) |
sunrains-common-err |
统一错误类(CustFailError、CustOtherFailError) |
使用示例
1. 无加密请求(void)
import { ReqApi } from "@/uni_modules/sunrains-request"
const frontVo: FrontRequestVo = {
method: "POST",
url: "http://your-api.com/test",
data: { name: "张三", age: 18 },
header: null,
reqFlag: "void",
priKey: "",
pubKey: "",
contentType: "json",
timeout: null,
filePathParamName: null
}
const res = await ReqApi(frontVo)
2. SM2签名请求(sign)
const frontVo: FrontRequestVo = {
method: "POST",
url: "http://your-api.com/test",
data: { name: "张三", age: 18 },
header: null,
reqFlag: "sign",
priKey: "客户端SM2私钥",
pubKey: "服务端SM2公钥",
contentType: "json",
timeout: null,
filePathParamName: null
}
const res = await ReqApi(frontVo)
3. 签名 + SM4加密请求(signwithenSM2-Sm4Encrypt)
const frontVo: FrontRequestVo = {
method: "POST",
url: "http://your-api.com/test",
data: { name: "张三", age: 18 },
header: null,
reqFlag: "signwithenSM2-Sm4Encrypt",
priKey: "客户端SM2私钥",
pubKey: "服务端SM2公钥",
contentType: "json",
timeout: null,
filePathParamName: null
}
const res = await ReqApi(frontVo)
// res.data 已自动解密
- 文件上传(为了全平台统一,只支持但文件上传(可以非文件路径参数进行 加密 签名 注:非文件参数禁止使用参数名 cufilePath))
let params : FrontRequestVo = {
method: "POST",
url: props.ocrUrl,
header: props.header,
reqFlag: props.reqFlag,
priKey:props.priKey,
pubKey:props.pubKey,
contentType:"form-data",
timeout:8000,
filePathParamName:'uri',//文件上传必填参数
data:types[i]
}
文件上传请求完整示例
<template>
<view class="layout" v-if="visible">
<view class="modal-mask">
<view class="modal-header">
<view class="modal-title">
<text class="title-text">身份证上传</text>
</view>
<view class="modal-close" @click="handleMaskClick">
<text class="close-icon">×</text>
</view>
</view>
<view class="modal-body">
<view class="upload-section">
<view class="upload-item">
<text class="upload-label">身份证正面</text>
<view class="upload-box" @click="chooseImage('front')">
<image v-if="frontImage" :src="frontImage" mode="aspectFill" class="preview-image"
@click.stop="previewImage('front')" />
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-text">点击上传</text>
</view>
<view v-if="frontImage" class="delete-btn" @click.stop="deleteImage('front')">
<text class="delete-icon">×</text>
</view>
</view>
</view>
<view class="upload-item">
<text class="upload-label">身份证反面</text>
<view class="upload-box" @click="chooseImage('back')">
<image v-if="backImage" :src="backImage" mode="aspectFill" class="preview-image"
@click.stop="previewImage('back')" />
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-text">点击上传</text>
</view>
<view v-if="backImage" class="delete-btn" @click.stop="deleteImage('back')">
<text class="delete-icon">×</text>
</view>
</view>
</view>
<button class="upload-btn" type="primary" @click="submitImages"
:disabled="disabled==true || submit==true">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, onUnmounted } from 'vue'
import { IdcardOcrModel, IdCardOCR } from './interface.uts'
import {
ReqApi
} from '@/uni_modules/sunrains-request'
import { Response, FrontRequestVo } from '@/uni_modules/sunrains-common-type'
const visible = ref<boolean>(false)
const frontImage = ref<string>('')
const backImage = ref<string>('')
let ocrData : IdcardOcrModel = {}
const flag = ref<boolean>(false)
const submit = ref<boolean>(false)
const disabled = computed(() => {
return frontImage.value.length < 2 || backImage.value.length < 2
})
const emit = defineEmits<{
(e : 'success', data : IdcardOcrModel) : void
}>()
const props = defineProps<IdCardOCR>()
function getGenderByIdNumber(idNumber : string) : string {
if (idNumber.length == 18) {
const genderCode = parseInt(idNumber.substring(16, 17))
return genderCode % 2 == 1 ? '男' : '女'
}
return ''
}
function open() {
visible.value = true
frontImage.value = ''
backImage.value = ''
ocrData = {} as IdcardOcrModel
flag.value = false
submit.value = false
}
onMounted(() => {
uni.$off('sunrains-idcard-ocr-open')
uni.$on('sunrains-idcard-ocr-open', () => {
open()
})
})
onUnmounted(() => {
uni.$off('sunrains-idcard-ocr-open')
})
function handleMaskClick() {
visible.value = false
frontImage.value = ''
backImage.value = ''
ocrData = {} as IdcardOcrModel
flag.value = false
}
function chooseImage(type : string) {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
if (type === 'front') {
frontImage.value = tempFilePath
} else {
backImage.value = tempFilePath
}
// console.log(`选择${type === 'front' ? '正面' : '反面'}图片成功:`, tempFilePath)
},
fail: (err) => {
console.error('选择图片失败:', err)
uni.showToast({
title: '选择图片失败',
icon: 'none',
mask: true
})
}
})
}
function previewImage(type : string) {
const imageUrl = type === 'front' ? frontImage.value : backImage.value
if (imageUrl.length == 0) {
return
}
uni.previewImage({
urls: [imageUrl],
current: imageUrl
})
}
function deleteImage(type : string) {
uni.showModal({
title: '提示',
content: '确定删除该图片吗?',
success: (res) => {
if (res.confirm) {
if (type === 'front') {
frontImage.value = ''
} else {
backImage.value = ''
}
console.log(`删除${type === 'front' ? '正面' : '反面'}图片成功`)
}
}
})
}
async function submitImages() {
const frontEmpty = frontImage.value.length == 0
const backEmpty = backImage.value.length == 0
if (frontEmpty || backEmpty) {
uni.showToast({
title: '请上传身份证正反面',
icon: 'none',
mask: true
})
return
}
uni.showLoading({
title: '识别中...'
})
let errNum = 0
try {
submit.value = true
const types = [{ type: "front", uri: frontImage.value }, { type: "back", uri: backImage.value }]
for (let i = 0; i < types.length; i++) {
let params : FrontRequestVo = {
method: "POST",
url: props.ocrUrl,
header: props.header,
reqFlag: props.reqFlag,
priKey: props.priKey,
pubKey: props.pubKey,
contentType: "form-data",
timeout: 20000,
filePathParamName: 'uri',
data: types[i]
}
//下面请求服务端依赖付费插件 可自己实现请求网络请求
await ReqApi(params).then((res : Response) => {
if (res.code == props.successCode) {
if (res.data != null) {
let rd = JSON.parseObject(JSON.stringify(res.data))!
ocrData = { ...ocrData, ...rd };
}
} else {
setTimeout(() => uni.showToast({
title: res.msg ?? '',
icon: 'none',
duration: 3000,
mask: true
}), 100)
errNum++
}
})
if (errNum > 0) {
submit.value = false
break
}
}
if (errNum == 0) {
flag.value = true
}
} catch (err) {
console.log("err==", err)
submit.value = false
uni.showToast({
title: '识别失败',
icon: 'none',
mask: true
})
} finally {
uni.hideLoading();
}
if (flag.value) {
emit('success', ocrData)
visible.value = false
flag.value = false
}
}
</script>
<style scoped lang="scss">
.layout {
width: 100%;
height: 1350rpx;
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 10;
.modal-mask {
width: 100%;
top: 0;
height: 100%;
background: #b5def3;
border-radius: 12px;
.modal-header {
height: 80rpx;
display: flex;
align-items: center;
flex-direction: row;
justify-content: space-between;
padding: 0 15px;
.modal-title {
flex: 3;
.title-text {
font-size: 20px;
font-weight: bold;
text-align: center;
}
}
.modal-close {
margin: 10px;
width: 30px;
height: 30px;
.close-icon {
font-size: 34px;
color: red;
text-align: center;
line-height: 1;
}
}
}
.modal-body {
flex: 1;
min-height: 0;
.upload-section {
width: 100%;
padding: 15px;
.upload-item {
margin-bottom: 20px;
.upload-label {
font-size: 14px;
color: #060e73;
margin-bottom: 10px;
}
.upload-box {
width: 100%;
height: 200px;
border: 2px dashed #ddd;
border-radius: 8px;
overflow: hidden;
background: #fafafa;
position: relative;
.preview-image {
width: 100%;
height: 100%;
}
.upload-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
.upload-icon {
font-size: 48px;
color: #999;
margin-bottom: 8px;
}
.upload-text {
font-size: 14px;
color: #999;
}
}
.delete-btn {
position: absolute;
top: 8px;
right: 8px;
width: 32px;
height: 32px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 16px;
display: flex;
align-items: center;
.delete-icon {
font-size: 24px;
color: #fff;
}
}
}
}
.upload-btn {
margin-top: 10px;
border-radius: 12px;
padding: 16px 0;
font-size: 16px;
}
}
}
}
}
/* #ifdef H5 */
uni-toast {
z-index: 9999 !important;
}
/* #endif */
</style>

收藏人数:
下载插件并导入HBuilderX
下载插件ZIP
赞赏(0)
下载 38
赞赏 0
下载 12502876
赞赏 1941
赞赏
京公网安备:11010802035340号