更新记录

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 都会执行以下操作:

  1. SM2加密SM4密钥:生成随机 SM4 key + iv,用服务端公钥(pubKey)通过 SM2 加密后放入 header["key"]
  2. 条件签名(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 string
  • sm4Encrypt / 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):
    1. 先校验外层 res.code == resSuccessCode()(当前定义为 0
    2. 取出 res.data(SM4 加密的字符串)
    3. 用本次请求生成的 SM4 key + iv 解密
    4. 解密结果为内层 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
  • 失败(非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 已自动解密
  1. 文件上传(为了全平台统一,只支持但文件上传(可以非文件路径参数进行 加密 签名 注:非文件参数禁止使用参数名 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>

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

许可协议

MIT协议