You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Play JSON递归替换含password字段值为掩码的优化实现咨询

优质实现:Play JSON密码掩码处理

场景说明

我们需要对JSON中所有键名包含password(不区分大小写)的字符串值进行掩码替换(改为****),用于日志脱敏。示例如下:

输入JSON:

{
  "a": 1,
  "password": "$uper$ecure",
  "nested": {
    "airShieldPassword": "12345",
    "b": "abc"
  }
}

处理后输出:

{
  "a": 1,
  "password": "****",
  "nested": {
    "airShieldPassword": "****",
    "b": "abc"
  }
}

下面提供几种更优的实现方式:


方案1:利用Play JSON内置转换API(推荐)

Play JSON的JsValue.transform配合自定义Reads,是符合Play惯用风格的实现,扩展性和健壮性更强:

import play.api.libs.json._

val passwordMaskReads: Reads[JsValue] = new Reads[JsValue] {
  override def reads(js: JsValue): JsResult[JsValue] = js match {
    // 处理JSON对象:遍历字段并递归处理值
    case obj: JsObject =>
      val maskedFields = obj.fields.map { case (key, value) =>
        val processedValue = if (key.toLowerCase.contains("password")) {
          value match {
            case _: JsString => JsString("****")
            case other => other // 非字符串类型不处理
          }
        } else {
          passwordMaskReads.reads(value).getOrElse(value)
        }
        key -> processedValue
      }
      JsSuccess(JsObject(maskedFields))
    
    // 处理JSON数组:递归处理每个元素
    case arr: JsArray =>
      val maskedElements = arr.value.map(v => passwordMaskReads.reads(v).getOrElse(v))
      JsSuccess(JsArray(maskedElements))
    
    // 其他类型直接返回
    case other => JsSuccess(other)
  }
}

// 使用方式
def maskPasswords(js: JsValue): JsValue = js.transform(passwordMaskReads).getOrElse(js)

优势:

  • 支持JSON数组的脱敏处理(原始实现未覆盖该场景)
  • 基于Play JSON标准转换模式,后续可轻松添加其他脱敏规则
  • 内置JsResult机制处理转换异常,更健壮

方案2:尾递归实现

如果需要处理超大JSON结构,担心递归栈溢出,可以用尾递归+栈结构模拟遍历:

import play.api.libs.json._
import scala.annotation.tailrec

def maskPasswordsTailRec(js: JsValue): JsValue = {
  // 用栈存储待处理节点及其上下文信息
  case class NodeContext(value: JsValue, path: List[String], isArrayElement: Boolean)

  @tailrec
  def processStack(stack: List[NodeContext], result: JsValue): JsValue = stack match {
    case Nil => result
    case NodeContext(obj: JsObject, path, _) :: rest =>
      val maskedFields = obj.fields.map { case (key, value) =>
        val maskedValue = if (key.toLowerCase.contains("password") && value.isInstanceOf[JsString]) {
          JsString("****")
        } else {
          value
        }
        key -> maskedValue
      }
      val newObj = JsObject(maskedFields)
      // 更新结果中对应路径的值
      val newResult = updateJsValue(result, path, newObj)
      // 将子对象/数组加入待处理栈
      val childStack = maskedFields.collect {
        case (k, o: JsObject) => NodeContext(o, path :+ k, isArrayElement = false)
        case (k, a: JsArray) => NodeContext(a, path :+ k, isArrayElement = false)
      }
      processStack(childStack ++ rest, newResult)
    
    case NodeContext(arr: JsArray, path, _) :: rest =>
      val maskedElements = arr.value.zipWithIndex.map { case (value, idx) =>
        val maskedValue = path.lastOption.exists(_.toLowerCase.contains("password")) && value.isInstanceOf[JsString] match {
          case true => JsString("****")
          case false => value
        }
        maskedValue
      }
      val newArr = JsArray(maskedElements)
      val newResult = updateJsValue(result, path, newArr)
      // 将子对象/数组加入待处理栈
      val childStack = maskedElements.zipWithIndex.collect {
        case (o: JsObject, idx) => NodeContext(o, path :+ idx.toString, isArrayElement = true)
        case (a: JsArray, idx) => NodeContext(a, path :+ idx.toString, isArrayElement = true)
      }
      processStack(childStack ++ rest, newResult)
    
    case _ :: rest => processStack(rest, result) // 非对象/数组节点直接跳过
  }

  // 辅助函数:根据路径更新JSON值
  def updateJsValue(js: JsValue, path: List[String], newValue: JsValue): JsValue = path match {
    case Nil => newValue
    case key :: Nil => js match {
      case obj: JsObject => obj + (key -> newValue)
      case arr: JsArray => arr.updated(key.toInt, newValue)
      case other => other
    }
    case key :: remaining => js match {
      case obj: JsObject =>
        val child = obj.value.getOrElse(key, JsNull)
        obj + (key -> updateJsValue(child, remaining, newValue))
      case arr: JsArray =>
        val idx = key.toInt
        val child = arr.value(idx)
        arr.updated(idx, updateJsValue(child, remaining, newValue))
      case other => other
    }
  }

  processStack(List(NodeContext(js, Nil, isArrayElement = false)), js)
}

优势:完全尾递归,无栈溢出风险,适合超大型JSON结构。


方案3:简化递归实现(轻量场景首选)

如果只处理常规JSON结构,不需要复杂扩展,可以用更紧凑的递归写法,同时覆盖数组场景:

import play.api.libs.json._

def maskPasswordsSimple(js: JsValue): JsValue = js match {
  case obj: JsObject =>
    JsObject(obj.fields.map {
      case (k, v: JsString) if k.toLowerCase.contains("password") => k -> JsString("****")
      case (k, o: JsObject) => k -> maskPasswordsSimple(o)
      case (k, a: JsArray) => k -> JsArray(a.value.map(maskPasswordsSimple))
      case field => field
    })
  case arr: JsArray => JsArray(arr.value.map(maskPasswordsSimple))
  case other => other
}

优势:代码简洁易读,实现成本低,满足大多数日志脱敏需求。


内容的提问来源于stack exchange,提问作者M. Justin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.02 03:33:11