App 上架包預檢

一、 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} 參數缺失`)
      }
    }
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章