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
相关产品推荐
相关产品推荐

