HarmonyOS 7 / API 26 HiLog 日志怎么防泄漏:手机号、token 和发布包调试日志怎么挡住

日志是开发时最顺手的排查工具,但也是最容易被忽略的隐私风险。HarmonyOS 7.0.0 / API 26 项目里,如果把手机号、token、完整接口参数直接打进 HiLog,开发阶段看起来很方便,到了发布包和上架审核前就会变成风险点。
这篇只讲一个具体问题:日志怎么写,才能既能定位问题,又不把敏感信息带出去。
| 项目 | 取值 |
|---|---|
| 系统版本 | HarmonyOS 7.0.0,满足 HarmonyOS 5.0.0 及以上要求 |
| API 版本 | API 26 |
| 工程模型 | Stage 模型 |
| 语言与框架 | ArkTS + ArkUI |
| 关注能力 | HiLog 日志、隐私合规、发布包检查、上架审核 |
| 验证目标 | 开发态可排查,发布态不泄漏手机号、token 和完整请求参数 |
这个主题适合上架前自查,也适合团队代码提交前做静态检查。它不是为了把所有日志都删掉,而是把日志分级、脱敏和发布态关闭这三件事做成固定规则。
坏例子通常长这样:
hilog.info(0x0000, 'Login', 'phone=' + phone + ', token=' + token)
这段代码的问题不是 HiLog 本身,而是日志内容没经过处理。手机号、token、完整请求体这些字段一旦出现在日志里,后面再靠人工检查就很难完全挡住。
我会把风险拆成三类:
- 敏感字段没有脱敏;
- 所有环境都打同一套日志;
- 发布包里还保留 debug 级别日志。
下面这个脚本模拟三种日志策略:裸日志、开发态脱敏日志、发布态日志。坏例子必须失败,好例子必须通过。
const cases = [
{ name: 'bad-raw-log', usesLevelSwitch: false, masksPhone: false, masksToken: false, blocksReleaseDebug: false },
{ name: 'good-dev-log', usesLevelSwitch: true, masksPhone: true, masksToken: true, blocksReleaseDebug: false },
{ name: 'good-release-log', usesLevelSwitch: true, masksPhone: true, masksToken: true, blocksReleaseDebug: true },
];
function inspect(item) {
const errors = [];
if (!item.usesLevelSwitch) errors.push('missing log level switch');
if (!item.masksPhone) errors.push('phone number is not masked');
if (!item.masksToken) errors.push('token is not masked');
if (item.name.includes('release') && !item.blocksReleaseDebug) errors.push('debug log is still enabled in release build');
return { ...item, passed: errors.length === 0, errors };
}
const result = cases.map(inspect);
console.log(JSON.stringify({
total: result.length,
passed: result.filter((item) => item.passed).length,
failed: result.filter((item) => !item.passed).length,
result,
}, null, 2));
本地结果是 3 个用例里 2 个通过、1 个失败。失败的是裸日志,这正好说明检查能挡住最危险的写法。
{
"total": 3,
"passed": 2,
"failed": 1
}
不要在每个调用点临时想要不要脱敏。更稳的方式是先封一个统一方法,所有日志都从这里过。
function maskPhone(value: string): string {
return value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
function maskToken(value: string): string {
if (!value || value.length < 10) return '***'
return value.slice(0, 4) + '***' + value.slice(-4)
}
function sanitizeLog(input: Record<string, string>): Record<string, string> {
return {
...input,
phone: input.phone ? maskPhone(input.phone) : '',
token: input.token ? maskToken(input.token) : '',
}
}
这样调用侧不需要记住每个字段怎么处理,只要先把日志参数交给 sanitizeLog。
开发包需要更多信息,发布包需要更严格。我的习惯是把日志开关放在统一入口,不让业务代码自己判断。
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
const isRelease = true
function canPrint(level: LogLevel): boolean {
if (isRelease && level === 'debug') return false
return true
}
function safeLog(level: LogLevel, tag: string, data: Record<string, string>) {
if (!canPrint(level)) return
const safeData = sanitizeLog(data)
hilog.info(0x0000, tag, JSON.stringify(safeData))
}
这里重点不是 isRelease 这个变量怎么来,而是日志策略要集中管理。发布包不应该依赖每个开发者手动删 debug 日志。
上架审核前,我会至少查四件事:
- 搜索 hilog、console、JSON.stringify,看有没有裸输出;
- 搜索 phone、token、password、authorization,看有没有直接拼日志;
- 确认发布态 debug 日志被开关挡住;
- 对照隐私声明,看日志里有没有声明之外的采集字段。
这一步很实际。很多问题不是代码不会写,而是发布前没人把日志当成隐私出口来查。
第一个用例是登录失败。输入手机号和 token,让日志输出失败原因。合格结果应该能看到错误码和接口名称,但看不到完整手机号和完整 token。
第二个用例是发布包检查。把环境切到 release,再跑 debug 日志。合格结果应该是 debug 日志完全不输出,warn 和 error 仍然可以输出必要的错误码。
| 方案 | 好处 | 问题 |
|---|---|---|
| 手动不打敏感字段 | 改动少 | 靠人记,容易漏 |
| 调用点各自脱敏 | 比裸日志好 | 规则分散,后期难维护 |
| 统一日志入口脱敏加环境开关 | 最稳,适合团队复用 | 前期要补一个小封装 |
我会选第三种。日志是基础设施,越早统一,后面越少返工。
我的做法是把日志检查放进提交前和发布前两道门:提交前挡明显的敏感字段,发布前再查 release 包日志策略。这样既不影响开发定位问题,也能减少上架前临时大面积改代码的风险。
HiLog 本身是工具,真正决定安全的是我们往里面写什么。把脱敏、分级、发布态开关做成固定入口,日志就能继续帮我们定位问题,而不是变成新的审核风险。
登录失败最容易偷懒,开发时想知道手机号、验证码、token 到底有没有传过去,于是把整段对象直接打出来:
interface LoginRequest {
phone: string
verifyCode: string
token: string
}
function badLoginLog(req: LoginRequest) {
hilog.info(0x0000, 'Login', 'request=' + JSON.stringify(req))
}
这个写法排查问题很快,但风险也很直接:日志里会出现完整手机号、验证码和 token。只要日志被导出、截屏、上传到排障系统,敏感信息就跟着出去了。
我更倾向于把日志入口收住,而不是要求每个调用处都记得脱敏:
const LOG_DOMAIN = 0x0001
type LogLevel = 'debug' | 'info' | 'warn' | 'error'
function maskPhone(phone: string): string {
if (!phone || phone.length < 7) return '***'
return phone.slice(0, 3) + '****' + phone.slice(-4)
}
function maskToken(token: string): string {
if (!token) return 'empty'
return token.slice(0, 4) + '***' + token.slice(-4)
}
function safeLoginLog(level: LogLevel, req: LoginRequest, isRelease: boolean) {
if (isRelease && level === 'debug') return
const safePayload = {
phone: maskPhone(req.phone),
verifyCode: '***',
token: maskToken(req.token)
}
hilog.info(LOG_DOMAIN, 'Login', JSON.stringify(safePayload))
}
这里的关键不是多写几行工具函数,而是把规则固定下来:手机号只能保留首尾,验证码永远不打印,token 只能留很短的定位片段,发布态直接关掉 debug 日志。
第二类问题更隐蔽。很多接口失败会把请求头、服务端返回、错误码一起打印,代码看起来像这样:
function badApiErrorLog(url: string, headers: Record<string, string>, body: string) {
hilog.error(0x0000, 'Api', 'url=' + url + ', headers=' + JSON.stringify(headers) + ', body=' + body)
}
这个问题不只出现在登录页。支付、个人资料、设备绑定、消息推送这些链路,只要 headers 里带 Authorization,日志就可能把凭证带出去。
更稳的做法是把“允许打印的字段”和“必须拦截的字段”分开:
const BLOCK_KEYS = ['authorization', 'token', 'cookie', 'set-cookie', 'phone', 'verifycode']
function sanitizeRecord(input: Record<string, string>): Record<string, string> {
const output: Record<string, string> = {}
Object.keys(input).forEach((key) => {
const lowerKey = key.toLowerCase()
output[key] = BLOCK_KEYS.includes(lowerKey) ? '***' : input[key]
})
return output
}
function safeApiErrorLog(url: string, headers: Record<string, string>, statusCode: number) {
hilog.error(LOG_DOMAIN, 'Api', JSON.stringify({
url,
statusCode,
headers: sanitizeRecord(headers)
}))
}
这段代码故意不打印完整 body。很多时候定位接口问题只需要 url、状态码、traceId、少量白名单字段;完整 body 反而是最容易出事的部分。
| 方案 | 优点 | 问题 | 适合场景 |
|---|---|---|---|
| 每次打印前手动脱敏 | 改动小,上手快 | 很容易漏,代码审查压力大 | 临时排查,不适合长期维护 |
| 封装 safeLog 方法 | 规则统一,方便 CI 检查 | 前期要梳理字段规则 | 推荐,适合团队项目 |
| 发布包完全关闭日志 | 风险最低 | 线上问题定位会变弱 | 金融、账号、安全要求很高的模块 |
我会选第二种:保留必要日志,但所有入口都走 safeLog。这样开发态还能定位问题,发布态也不会把敏感字段带出去。
只靠人眼看日志代码不稳定。至少要在提交前挡住几个高风险写法:
const fs = require('fs')
const source = fs.readFileSync('entry/src/main/ets/pages/LoginPage.ets', 'utf8')
const blockedPatterns = [
/hilog.(debug|info|warn|error)([^
]*(phone|token|authorization|cookie)/i,
/JSON.stringify([^
]*(request|headers|body)/i,
/verifyCode/i
]
const hit = blockedPatterns.find((pattern) => pattern.test(source))
if (hit) {
console.error('日志检查失败:存在可能泄漏敏感字段的日志写法')
process.exit(1)
}
console.log('日志检查通过')
这个检查不需要一开始就做得很复杂。先挡住手机号、token、authorization、cookie、验证码,再逐步把项目里的敏感字段补进去。
- 发布包里是否还有 debug 级别日志;
- 登录、支付、用户资料、设备绑定页面是否打印手机号、token、验证码;
- 网络错误日志是否打印完整 headers 和 body;
- 线上排障是否只保留 traceId、状态码、接口名和脱敏后的关键字段;
- 代码提交前是否有脚本拦截高风险日志;
- 团队是否约定所有日志统一走 safeLog,不允许业务代码直接拼接敏感字段。
这个问题越早处理越省事。等到上架前再全局搜索日志,通常会发现一堆临时排查代码混在业务里,很难判断哪条能删、哪条还要保留。把脱敏、分级、发布态关闭做成固定入口,后面每个页面都能复用。
更多推荐



所有评论(0)