一、 iOS 端常見被拒原因彙總
- App 內包含分發下載分發功能(引導用戶下載 App 等功能)。
- 提供的測試賬號無法查看實際功能
- 通過接口返回布爾值判斷 App 是否升級,但審覈期間該接口不請求
- 審覈賬號,任何時候在任何 ip 登錄看到的都是審覈版。
- 提供的登陸賬號和密碼不對,登陸不上
- 運營填寫的營銷關鍵字有問題
- 元數據問題,iPhoneX 截圖中 iPhone 殼子是 iPhone7 的,應該是 iPhoneX
- 說明隱私權限的作用。
- 營銷文字,某些能力需要資質。此類功能在審覈期間都關閉
- 修改隱私權限相關的文案,做到讓審覈人員看得懂,做到「信達雅」
- App 無法登陸進去,屬於 bug 級別
- App 沒有適配 ipad。
- Privacy - Data Collection and Storage,說明 App 沒有做隱私權限的收集。
- 訪問 h5 頁面出現問題。 屬於 bug 級別
- App 集成了設備指紋 SDK, 會上傳用戶設備安裝應用列表。 解決:移除設備指紋SDK, 成功上架
二、 App 被拒原因彙總
從 Android 和 iOS 2端 App 被駁回的一些信息來看,駁回原因一般劃分爲下面幾類:
- 審覈期間,資源和配置都應該調節爲審覈模式
- App 包含某些關鍵字
- 審覈相關的元數據問題(截圖與實際內容不匹配、機型和截圖不匹配、提供給審覈的賬號和密碼登陸不上)
- 使用的隱私權限必須說明,文案描述必須清晰
- App 存在 bug (賬號無法登陸、沒有適配 ipad、訪問 h5 打不開 )
- 誘導用戶打開查看更多 App
- Android 應用未加固
- 應用缺乏相關的資質和證書
三、 方案
常見審覈失敗的原因很多,很大比重一個就是代碼或者文本里面存在一些敏感詞,所以本文的側重點在於關鍵詞掃描。像上架設置的截圖和當前設備不匹配、提供的賬號無法使用功能 😂 這種情況打一頓就好了,非主流行爲不在本文範圍內
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.rb
、PostInstallHook.rb
,允許我們在不同的階段爲工程做一些自定義的操作,所以我們的 iOS 模版設計也參考了這個思想,在打包構建前、構建中、構建後提供了鉤子:prebuild
、build
、postbuild
。定位好了問題,要做的就是在 prebuild 裏面進行關鍵詞掃描的編碼工作。
確定了什麼時候做什麼事情,接下來就要討論怎麼做才合適。
3.3 技術方案選擇
字符串匹配算法 KMP 是一開始想到的內容,針對某個 App 進行時機測試,發現50多個敏感詞的情況下,代碼掃描耗時60秒鐘,覺得非常不理想,看 KMP 算法沒有啥問題,所以換個思路走下去。
因爲模版本質上 Node 項目,所以 Node 下的 glob 模塊正好提供根據正則匹配到合適的文件,也可以匹配文件裏面的字符串。然後繼續做實驗,數據如下:9個銘感詞語、代碼文件5967個,耗時3.5秒
3.4 完整方案
- 業務線需要自定義敏感詞雲(因爲每條業務線的關鍵詞雲都不一樣)
- 敏感詞需要劃分等級:error、warning。掃描到 error 需要馬上停止構建,並提示「已掃描到你的源碼中存在敏感詞***,可能存在提交審覈失敗的可能,請修改後再次構建」。warning 的情況不需要馬上停止構建,等任務全部結束後彙總給出提示「已掃描到你的源碼中存在敏感詞***、***…,可能存在提交審覈失敗的可能,請開發者自己確認」
- 銘感詞雲的格式
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
- 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} 參數缺失`)
}
}
}
}