uni-app App 开发配置

MuYan2022-11-04VueVueuni-app

基础配置

  1. 默认先清除所有 android APP 权限配置,后面根据需求再配置对应的权限
  2. 配置 Android 平台启动时的基础权限配置为 none
  3. 配置支持的 CPU 类型
    • armeabi-v7a 第 7 代及以上的 ARM 处理器(ARM32 位),市面上大多数手机使用此 CPU 类型。
    • arm64-v8a 第 8 代、64 位 ARM 处理器(ARM64 位),最近两年新发的设备使用此 CPU 类型,可以兼容使用 armeabi-v7a 的 so 库。
  4. 配置 Android 平台隐私与政策提示框
  5. 禁用启动界面等待雪花
  6. 国际化配置
// manifest.json
{
  "app-plus": {
    "distribute": {
      /* android打包配置 */
      "android": {
        // 默认清除所有权限
        "permissions": [],
        // 不自动添加第三方SDK需要的Android权限
        "autoSdkPermissions": false,
        // 支持的CPU类型
        "abiFilters": ["armeabi-v7a", "arm64-v8a"],
        // Android平台应用启动时申请读写手机存储权限策略
        "permissionExternalStorage": {
          "request": "none",
          "prompt": "应用保存运行状态等信息,需要获取读写手机存储(系统提示为访问设备上的照片、媒体内容和文件)权限,请允许。"
        },
        // Android平台应用启动时申请读取设备信息权限配置
        "permissionPhoneState": {
          "request": "none",
          "prompt": "为保证您正常、安全地使用,需要获取设备识别码(部分手机提示为获取手机号码)使用权限,请允许。"
        }
      }
    },
    "splashscreen": {
      // 禁用启动界面等待雪花
      "waiting": false
    }
  },
  // 默认返回语言
  "fallbackLocale": "zh-Hans",
  // 默认语言
  "locale": "zh-Hans"
}

Android 平台隐私与政策提示框

{
  "version": "1",
  "prompt": "template",
  "title": "服务协议和隐私政策",
  "message": "  请你务必审慎阅读、充分理解“服务协议”和“隐私政策”各条款,包括但不限于:为了更好的向你提供服务,我们需要收集你的设备标识、操作日志等信息用于分析、优化应用性能。<br/>  你可阅读<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>了解详细信息。如果你同意,请点击下面按钮开始接受我们的服务。",
  "buttonAccept": "同意并接受",
  "buttonRefuse": "暂不同意",
  "hrefLoader": "system|default",
  "second": {
    "title": "确认提示",
    "message": "  进入应用前,你需先同意<a href=\"\">《服务协议》</a>和<a href=\"\">《隐私政策》</a>,否则将退出应用。",
    "buttonAccept": "同意并继续",
    "buttonRefuse": "退出应用"
  },
  "disagreeMode": {
    "visitorEntry": true
  },
  "styles": {
    "backgroundColor": "#00FF00",
    "borderRadius": "5px",
    "title": {
      "color": "#ff00ff"
    },
    "buttonAccept": {
      "color": "#ffff00"
    },
    "buttonRefuse": {
      "color": "#00ffff"
    },
    "buttonVisitor": {
      "color": "#00ffff"
    }
  }
}

游客模式

  • Android 平台隐私与政策提示框 中配置了 visitorEntry( HX 3.6.7 版本后支持)后,会有一个游客模式的入口,后续登录等情况就需要引导用户同意隐私政策
  • disagreeMode 模式限制 uni API 和组件open in new window
  • 可以使用 plus.runtime.isAgreePrivacy() 判断用户是否同意隐私政策
  • 可以在需要的地方(如登录)引入下列代码引导用户同意隐私政策
// showPrivacyModal.js
export default () => {
  return new Promise((resolve, reject) => {
    // #ifdef APP-PLUS
    // 用户已同意隐私政策,IOS不需要隐私与政策提示框
    if (
      uni.getSystemInfoSync().osName === "ios" ||
      plus.runtime.isAgreePrivacy()
    ) {
      resolve(true);
      return;
    }
    const options = {
      success: (response) => {
        // 用户同意隐私政策
        if (response.code === 1) {
          // 如果项目中使用了 map、push、Statistic,或者设置loadNativePlugins为false时,用户选择同意隐私政策协议后需要调用plus.runtime.restart重启应用才能生效!
          uni.showModal({
            title: "提示",
            content: "请手动重启应用,并授权相关权限!",
            showCancel: false,
            confirmText: "重启应用",
            success: (res) => {
              if (res.confirm) {
                plus.runtime.restart();
              }
            },
          });
        } else if (response.code === -1) {
          // 用户选择退出应用
          uni.showModal({
            title: "提示",
            content: "如需退出应用,请手动退出!",
            showCancel: false,
            success: () => {
              if (getCurrentPages().length > 1) {
                // 返回上一页
                uni.navigateBack({
                  delta: 1,
                });
              } else {
                // 回到首页
                uni.reLaunch({
                  url: "",
                });
              }
            },
          });
        } else {
          //用户未同意隐私政策,进入游客模式
          // plus.runtime.restart();
          // 回到首页
          uni.reLaunch({
            url: "",
          });
        }
      },
      fail: (response) => {
        console.log("fail  " + JSON.stringify(response));
      },
    };
    //弹出隐私政策协议框,引导用户同意隐私政策
    plus.runtime.showPrivacyDialog(options);
    reject(false);
    // #endif
    // #ifndef APP-PLUS
    resolve(true);
    // #endif
  });
};

// 使用示例
import showPrivacyModal from './showPrivacyModal';

async demo(){
  await showPrivacyModal()
}

安全问题

manifest.json 相关安全问题修复

  • 由于 App 的安装包都可以解压。前端资源,一般都是明文存放在安装包中,为防止解压后泄露敏感信息或被利用漏洞信息,所以需要进行安全处理与配置。
  1. Android 配置 minSdkVersion 最低版本为 21,对应Android5.0 版本,5.0 以下版本有一些基础的隐性安全漏洞,例如:WebView远程代码执行漏洞
  2. iOS js/nvue文件的原生混淆 支持配置,只开发 Android 平台的应用可以不用配置以下两点配置,直接配置 resources 信息
    • 配置 supportWKWebviewtrue,用于支持 WKWebview
    • 配置 iOS 支持的最低版本 deploymentTarget11.0(iOS 平台 WKWebview 需 iOS11+系统才支持原生混淆)
  3. Webview 绕过证书校验漏洞 及 Android 主机名\证书弱校验风险的问题解决,配置 untrustedcarefuse,untrustedca 属性值域说明如下
    • accept: 接受此非受信证书,继续访问
    • refuse: 拒绝此非受信证书,停止访问
    • warning: 弹出警告提示框提醒用户,由用户确定是否继续访问,仅针对 webview 内部请求
// manifest.json
{
  "app-plus": {
    "optimization": {
      // 是否开启分包优化
      "subPackages": true
    },
    // 开启分包优化后,必须配置资源释放模式
    "runmode": "liberate",
    // Webview绕过证书校验漏洞 及 Android主机名\证书弱校验风险修复
    "ssl": {
      "untrustedca": "refuse"
    },
    // js/nvue文件的原生混淆
    "confusion": {
      "description": "原生混淆",
      "supportWKWebview": true,
      // resource下的键名为js文件路径(相对于应用根目录),值为空JSON对象(大括号)
      "resources": {
        // 示例,项目根目录下,js 文件夹内的 common.js 文件
        "js/common.js": {},
        // cli项目的根目录为 src 文件夹,例如 src/js/test.js 文件,加密时从js文件夹开始算
        "js/test.js": {}
      }
    },
    "distribute": {
      /* android打包配置 */
      "android": {
        // 配置 Android平台最低支持版本为 Android5.0
        "minSdkVersion": 21
      },
      /* ios打包配置 */
      "ios": {
        // 设置应用仅支持iOS11及以上设备
        "deploymentTarget": "11.0"
      }
    }
  }
}

应用签名未校验风险修复

  • 风险描述

签名证书是对 App 开发者身份的唯一标识,如果程序未对签名证书进行校验,可能被反编译后进行二次打包使用其它签名证书重新签名。如重新签名的 App 可以正常启动,则可能导致 App 被仿冒盗版,影响其合法收入,甚至可能被添加钓鱼代码、病毒代码、恶意代码,导致用户敏感信息泄露或者恶意攻击。

  • 修复方案

HBuilderX3.0.0+版本新增plus.navigator.getSignature (opens new window)方法获取 Android 平台签名证书的 SHA-1 指纹信息,在应用启动或运行时进行校验判断

  • 为了防止 js 检验代码被反编译篡改,建议将签名校验代码放到独立 js 文件中并配置 js/nvue 文件原生混淆加密
// App.vue
export default {
  onLaunch: function(inf) {
    console.log("App Launch");
    // #ifdef APP-PLUS
    // 签名证书检验
    var platform = uni.getSystemInfoSync.platform;
    var sign = plus.navigator.getSignature();
    if ("android" == platform) {
      //Android平台
      var sha1 = "baad093a82829fb432a7b28cb4ccf0e9f37dae58"; //修改为自己应用签名证书SHA-1值,是全小写并且中间不包含“:”符号
      if (sha1 != sign) {
        //证书不对时退出应用
        plus.runtime.quit();
      }
    } else {
      //iOS平台
      var md5 = "a2e629f0ea915b4ed11e296a059c9a12"; //修改为自己应用Apple Bunld ID(AppID)的md5值
      if (md5 != sign) {
        //不进入应用或循环弹出提示框
        console.log("应用被破坏,无法正常运行!");
        uni.showModal({
          title: "错误",
          content: "应用被破坏,无法正常运行!",
          showCancel: false,
        });
      }
    }
    // #endif
  },
};

禁止使用模拟器

  • 使用 plus.navigator.isSimulator (opens new window)用于判断当前应用是否运行在模拟器中。

iOS 系统由于苹果限制了正式打包后不能在模拟器上运行,一般不存在这种情况;Android 系统是开源的,底层代码都是公开的,因此市面上有很多 Android 模拟器,此问题比较严重。

模拟器通常是运行在 PC 上,可以利用一些自动化工具自动操作使用 App,另外模拟器是一个虚拟操作系统,可能会破坏原生系统的安全性,导致用户敏感信息泄露。

// App.vue
export default {
  onLaunch: function(inf) {
    console.log("App Launch");
    // #ifdef APP-PLUS
    // 模拟器检验
    if (plus.navigator.isSimulator()) {
      //弹出提示框
      uni.showModal({
        title: "错误",
        content: "应用被不能运行到模拟器!",
        showCancel: false,
        complete: () => {
          // 退出应用程序
          plus.runtime.quit();
        },
      });
    }
    // #endif
  },
};

Android apk 应用加固

为了提升 App 的安全性,推荐对安装包进行加固处理(如腾讯、360 等加固平台),对安全性有更高要求的 App 建议使用商用加固解决方案。

  • 注:加固后需要重新签名,签名需要安装jdkjre:[安装与使用](/blogs/web/Android APPzhengshu.html#安装jdk环境)
# 重新签名命令
jarsigner -verbose -keystore filename.keystore -signedjar outputfile.apk inputfile.apk alias

命令含义

  • verbose:输出签名过程的详细信息
  • -keystore:密匙证书文件位置
  • filename:密匙证书文件名
  • -signedjar:指定输入输出文件名
  • outputfile.apk:签名后文件
  • inputfile.apk:未签名文件
  • alias:密匙证书文件的别名

APP 热更新

升级中心 uni-upgrade-center - Appopen in new window

  • 版本号比对
/**
 * 对比版本号
 * 支持比对	("3.0.0.0.0.1.0.1", "3.0.0.0.0.1")	("3.0.0.1", "3.0")	("3.1.1", "3.1.1.1") 之类的
 * @param v1
 * @param v2
 * @returns
 * v1 > v2 return 1
 * v1 < v2 return -1
 * v1 == v2 return 0
 */
export const compare = (v1: string = "0", v2: string = "0") => {
  const normalizeVersion = (v: string) => v.split(".").map(Number);

  const newV1 = normalizeVersion(v1);
  const newV2 = normalizeVersion(v2);

  const minLength = Math.min(newV1.length, newV2.length);

  for (let i = 0; i < minLength; i++) {
    if (newV1[i] > newV2[i]) return 1;
    if (newV1[i] < newV2[i]) return -1;
  }

  if (newV1.length === newV2.length) return 0;

  const longerVersion = newV1.length > newV2.length ? newV1 : newV2;
  const rest = longerVersion.slice(minLength);
  if (rest.some((n: number) => n > 0)) {
    return newV1.length > newV2.length ? 1 : -1;
  }
  return 0;
};
上次更新 2026/6/23 11:49:15
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.8