上架包预检

一、 iOS 端常见被拒原因汇总

  1. App 内包含分发下载分发功能(引导用户下载 App 等功能)。
  2. 提供的测试账号无法查看实际功能
  3. 通过接口返回布尔值判断 App 是否升级,但审核期间该接口不请求
  4. 审核账号,任何时候在任何 ip 登录看到的都是审核版。
  5. 提供的登陆账号和密码不对,登陆不上
  6. 运营填写的营销关键字有问题
  7. 元数据问题,iPhoneX 截图中 iPhone 壳子是 iPhone7 的,应该是 iPhoneX
  8. 说明隐私权限的作用。
  9. 营销文字,某些能力需要资质。此类功能在审核期间都关闭
  10. 修改隐私权限相关的文案,做到让审核人员看得懂,做到「信达雅」
  11. App 无法登陆进去,属于 bug 级别
  12. App 没有适配 ipad。
  13. Privacy - Data Collection and Storage,说明 App 没有做隐私权限的收集。
  14. 访问 h5 页面出现问题。 属于 bug 级别
  15. App 集成了设备指纹 SDK, 会上传用户设备安装应用列表。 解决:移除设备指纹SDK, 成功上架

二、 App 被拒原因汇总

从 Android 和 iOS 2端 App 被驳回的一些信息来看,驳回原因一般划分为下面几类:

  1. 审核期间,资源和配置都应该调节为审核模式
  2. App 包含某些关键字
  3. 审核相关的元数据问题(截图与实际内容不匹配、机型和截图不匹配、提供给审核的账号和密码登陆不上)
  4. 使用的隐私权限必须说明,文案描述必须清晰
  5. App 存在 bug (账号无法登陆、没有适配 ipad、访问 h5 打不开 )
  6. 诱导用户打开查看更多 App
  7. Android 应用未加固
  8. 应用缺乏相关的资质和证书

三、 方案

常见审核失败的原因很多,很大比重一个就是代码或者文本里面存在一些敏感词,所以本文的侧重点在于关键词扫描。像上架设置的截图和当前设备不匹配、提供的账号无法使用功能 😂 这种情况打一顿就好了,非主流行为不在本文范围内

3.1 词云谁去收集?

每个公司一般来说都不止一条业务线,所以每个业务线的 App 情况和内容也不一样,所以敏感词也是千差万别。敏感词收集这个事情,应该由业务线主要负责 App 的开发者来收集,根据平时的上架情况,苹果的驳回的邮件来整理。

3.2 方案设计

公司自研工具 cli(iOS SDK、iOS App、Android SDK、Android App、RN、Node、React 依赖分析、构建、打包、测试、热修复、埋点、构建),各个端都是通过「模版」来提供能力。包含若干子项目,每个子项目就是所谓的 “模版”,每个模版其实就是一个 Node 工程,一个 npm 模块,主要负责以下功能:特定项目类型的目录结构、自定义命令供开发、构建等使用、模版持续更新及 patch 等。

所以可以在打包构建(各个端将项目提交到打包系统,打包系统根据项目语言、平台调度打包机)的时候,拿到源代码进行扫描。基于这个现状,所以方案是「扫描是基于源代码出发的扫描的」。

按照 iOS 端 pod install 这个过程,cocoapods 为我们预留了钩子:PreInstallHook.rbPostInstallHook.rb,允许我们在不同的阶段为工程做一些自定义的操作,所以我们的 iOS 模版设计也参考了这个思想,在打包构建前、构建中、构建后提供了钩子:prebuildbuildpostbuild。定位好了问题,要做的就是在 prebuild 里面进行关键词扫描的编码工作。

确定了什么时候做什么事情,接下来就要讨论怎么做才合适。

3.3 技术方案选择

字符串匹配算法 KMP 是一开始想到的内容,针对某个 App 进行时机测试,发现50多个敏感词的情况下,代码扫描耗时60秒钟,觉得非常不理想,看 KMP 算法没有啥问题,所以换个思路走下去。

因为模版本质上 Node 项目,所以 Node 下的 glob 模块正好提供根据正则匹配到合适的文件,也可以匹配文件里面的字符串。然后继续做实验,数据如下:9个铭感词语、代码文件5967个,耗时3.5秒

3.4 完整方案

  1. 业务线需要自定义敏感词云(因为每条业务线的关键词云都不一样)
  2. 敏感词需要划分等级:error、warning。扫描到 error 需要马上停止构建,并提示「已扫描到你的源码中存在敏感词***,可能存在提交审核失败的可能,请修改后再次构建」。warning 的情况不需要马上停止构建,等任务全部结束后汇总给出提示「已扫描到你的源码中存在敏感词***、***...,可能存在提交审核失败的可能,请开发者自己确认」
  3. 铭感词云的格式 scaner.yml 文件。
  • error: 数组的格式。后面写需要扫描的关键词,且等级为 error,表示扫描到 error 则马上停止构建
  • warning:数组的格式。后面写需要扫描的关键词,且等级为 warning,扫描结果不影响构建,最终只是展示出来
  • searchPath:字符串格式。可以让业务线自定义需要进行扫描的路径。
  • fileType:数组格式。可以让业务线自定义需要扫描的文件类型。默认为 sh|pch|json|xcconfig|mm|cpp|h|m
  • warningkeywordsScan:布尔值。业务线可以设置是否需要扫描 warning 级别的关键词。
  • errorKeywordsScan:布尔值。业务线可以设置是否需要扫描 error 级别的关键词。
error:
  - checkSwitch
warning:
  - loan
  - online
  - ischeck
searchPath:
  ../fixtures
fileType:
  - h
  - m
  - cpp
  - mm
  - js
warningkeywordsScan: true
errorKeywordsScan: true
  1. iOS 端存在私有 api 的情况,Android 端不存在该问题 私有 api 70111个文件,每个文件假设10个方法,则共70万个 api。所以计划找出 top 100.去扫描匹配,支持业务线是否开启的选项

其实这些问题都是业界标准的做法,肯定需要预留这样的能力,所以自定义规则的格式可以查看上面 yml 文件的各个字段所确定。明确了做什么事,以及做事情的标准,那就可以很快的开展并落地实现。

'use strict'

const { Error, logger } = require('@company/BFF-utils')
const fs = require('fs-extra')
const glob = require('glob')
const YAML = require('yamljs')

module.exports = class PreBuildCommand {
  constructor(ctx) {
    this.ctx = ctx
    this.projectPath = ''
    this.fileNum = 0
    this.isExist = false
    this.errorFiles = []
    this.warningFiles = []
    this.keywordsObject = {}
    this.errorReg = null
    this.warningReg = null
    this.warningkeywordsScan = false
    this.errorKeywordsScan = false
    this.scanFileTypes = ''
  }

  async fetchCodeFiles(dirPath, fileType = 'sh|pch|json|xcconfig|mm|cpp|h|m') {
    return new Promise((resolve, reject) => {
      glob(`**/*.?(${fileType})`, { root: dirPath, cwd: dirPath, realpath: true }, (err, files) => {
        if (err) reject(err)
        resolve(files)
      })
    })
  }

  async scanConfigurationReader(keywordsPath) {
    return new Promise((resolve, reject) => {
      fs.readFile(keywordsPath, 'UTF-8', (err, data) => {
        if (!err) {
          let keywords = YAML.parse(data)
          resolve(keywords)
        } else {
          reject(err)
        }
      })
    })
  }

  async run() {
    const { argv } = this.ctx
    const buildParam = {
      scheme: argv.opts.scheme,
      cert: argv.opts.cert,
      env: argv.opts.env
    }

    // 处理包关键词扫描(敏感词汇 + 私有 api)
    this.keywordsObject = (await this.scanConfigurationReader(this.ctx.cwd + '/.scaner.yml')) || {}
    this.warningkeywordsScan = this.keywordsObject.warningkeywordsScan || false
    this.errorKeywordsScan = this.keywordsObject.errorKeywordsScan || false
    if (Array.isArray(this.keywordsObject.fileType)) {
      this.scanFileTypes = this.keywordsObject.fileType.join('|')
    }
    if (Array.isArray(this.keywordsObject.error)) {
      this.errorReg = this.keywordsObject.error.join('|')
    }
    if (Array.isArray(this.keywordsObject.warning)) {
      this.warningReg = this.keywordsObject.warning.join('|')
    }

    // 从指定目录下获取所有文件
    this.projectPath = this.keywordsObject ? this.keywordsObject.searchPath : this.ctx.cwd
    const files = await this.fetchCodeFiles(this.projectPath, this.scanFileTypes)

    if (this.errorReg && this.errorKeywordsScan) {
      await Promise.all(
        files.map(async file => {
          try {
            const content = await fs.readFile(file, 'utf-8')
            const result = await content.match(new RegExp(`(${this.errorReg})`, 'g'))
            if (result) {
              if (result.length > 0) {
                this.isExist = true
                this.fileNum++
                this.errorFiles.push(
                  `编号: ${this.fileNum}, 所在文件: ${file},  出现次数: ${result &&
                    (result.length || 0)}`
                )
              }
            }
          } catch (error) {
            throw error
          }
        })
      )
    }

    if (this.errorFiles.length > 0) {
      throw new Error(
        `从你的项目中扫描到了 error 级别的敏感词,建议你修改方法名称、属性名、方法注释、文档描述。\n敏感词有 「${
          this.errorReg
        }」\n存在问题的文件有 ${JSON.stringify(this.errorFiles, null, 2)}`
      )
    }

    // warning
    if (this.warningReg && !this.isExist && this.fileNum === 0 && this.warningkeywordsScan) {
      await Promise.all(
        files.map(async file => {
          try {
            const content = await fs.readFile(file, 'utf-8')
            const result = await content.match(new RegExp(`(${this.warningReg})`, 'g'))
            if (result) {
              if (result.length > 0) {
                this.isExist = true
                this.fileNum++
                this.warningFiles.push(
                  `编号: ${this.fileNum}, 所在文件: ${file},  出现次数: ${result &&
                    (result.length || 0)}`
                )
              }
            }
          } catch (error) {
            throw error
          }
        })
      )

      if (this.warningFiles.length > 0) {
        logger.info(
          `从你的项目中扫描到了 warning 级别的敏感词,建议你修改方法名称、属性名、方法注释、文档描述。\n敏感词有 「${
            this.warningReg
          }」。有问题的文件有${JSON.stringify(this.warningFiles, null, 2)}`
        )
      }
    }

    for (const key in buildParam) {
      if (!buildParam[key]) {
        throw new Error(`build: ${key} 参数缺失`)
      }
    }
  }
}
优秀的个人博客,低调大师

微信关注我们

原文链接:https://my.oschina.net/u/1778933/blog/3186840

转载内容版权归作者及来源网站所有!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

相关文章

发表评论

资源下载

更多资源
Mario,低调大师唯一一个Java游戏作品

Mario,低调大师唯一一个Java游戏作品

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Oracle Database,又名Oracle RDBMS

Oracle Database,又名Oracle RDBMS

Oracle Database,又名Oracle RDBMS,或简称Oracle。是甲骨文公司的一款关系数据库管理系统。它是在数据库领域一直处于领先地位的产品。可以说Oracle数据库系统是目前世界上流行的关系数据库管理系统,系统可移植性好、使用方便、功能强,适用于各类大、中、小、微机环境。它是一种高效率、可靠性好的、适应高吞吐量的数据库方案。

Apache Tomcat7、8、9(Java Web服务器)

Apache Tomcat7、8、9(Java Web服务器)

Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。

Eclipse(集成开发环境)

Eclipse(集成开发环境)

Eclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。幸运的是,Eclipse 附带了一个标准的插件集,包括Java开发工具(Java Development Kit,JDK)。