HarmonyOS 7 API 26 HiLog 日志隐私检查图

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。这样开发态还能定位问题,发布态也不会把敏感字段带出去。

放进 CI 里的最低检查

只靠人眼看日志代码不稳定。至少要在提交前挡住几个高风险写法:

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,不允许业务代码直接拼接敏感字段。

这个问题越早处理越省事。等到上架前再全局搜索日志,通常会发现一堆临时排查代码混在业务里,很难判断哪条能删、哪条还要保留。把脱敏、分级、发布态关闭做成固定入口,后面每个页面都能复用。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐