Haskell中基于Aeson实现上下文感知的JSON脱敏序列化
推荐方案:自定义带上下文的JSON序列化类型类
首先,你找对方向了——原生Aeson的toJSON是纯函数,没办法携带当前用户这类上下文信息,所以必须扩展它的能力。我最推荐你用自定义ToJSONReader类型类的方案,这是最优雅、可维护性最高的实现方式,下面给你细化具体的代码实现:
1. 定义核心类型类
先创建一个支持Reader上下文的序列化类型类,同时给所有已有ToJSON实例的类型提供默认实现,这样大部分类型不用额外修改就能复用原有序列化逻辑:
import Control.Monad.Reader import Data.Aeson (ToJSON(..), Value, object, (.=)) import Data.ByteString.Lazy (ByteString) -- 假设你的用户ID类型是这样的 newtype UserId = UserId Int deriving (Eq, Show, ToJSON) -- 定义带Reader上下文的JSON序列化类型类 class ToJSONReader a where toJSONReader :: a -> Reader UserId Value -- 给所有已有ToJSON实例的类型提供默认实现 instance ToJSON a => ToJSONReader a where toJSONReader = return . toJSON
2. 为需要脱敏的类型实现实例
针对你的嵌套类型MyNestedType,重写toJSONReader方法,根据当前用户判断是否脱敏:
data MyNestedType = MyNestedType { owner :: UserId , sensitiveData :: String , otherData :: Int , nestedChild :: AnotherType -- 假设有嵌套子类型 } deriving (Show) -- 默认的纯函数序列化(供所有者查看时使用) instance ToJSON MyNestedType where toJSON x = object [ "owner" .= owner x , "sensitiveData" .= sensitiveData x , "otherData" .= otherData x , "nestedChild" .= nestedChild x ] -- 带上下文的脱敏序列化 instance ToJSONReader MyNestedType where toJSONReader x = do currentUser <- ask if owner x == currentUser then return $ toJSON x -- 所有者查看完整数据 else return $ object [ "owner" .= owner x , "sensitiveData" .= String "REDACTED" -- 脱敏敏感字段 , "otherData" .= otherData x -- 非敏感字段保留 , "nestedChild" .= runReader toJSONReader (nestedChild x) -- 递归处理嵌套类型 ]
这里的优势是递归处理嵌套结构:如果你的类型里包含其他需要脱敏的子类型,只要给子类型也实现ToJSONReader,就会自动应用对应的脱敏逻辑,不用手动遍历整个结构。
3. 顶层编码函数
最后写一个方便使用的顶层函数,传入用户ID和数据,直接输出脱敏后的JSON:
encodeWithUser :: ToJSONReader a => UserId -> a -> ByteString encodeWithUser user = encode . runReader toJSONReader user
对比其他方案的优缺点
- 先转Value再脱敏:确实可行,但嵌套结构深的时候需要递归遍历
Value(比如Object、Array),代码繁琐且容易遗漏字段,维护成本高。 - 扩展类型加脱敏标记:会污染原有的数据类型,每次修改原类型都要同步修改带标记的版本,不够简洁。
- 创建RedactedMyType:本质是复制原类型结构,重复代码过多,原类型结构变化时需要同步修改脱敏版本,后期维护很麻烦。
额外优化:更通用的Monad上下文
如果以后需要更多上下文(比如请求ID、权限等级),可以把类型类改成支持任意MonadReader的版本,扩展性更强:
class MonadReader r m => ToJSONM r m a where toJSONM :: a -> m Value instance (ToJSON a, MonadReader r m) => ToJSONM r m a where toJSONM = return . toJSON
这样你可以把Reader UserId换成Reader AppContext(包含更多上下文信息),不用修改核心的序列化逻辑。
内容的提问来源于stack exchange,提问作者Xavier Shay
相关产品推荐
相关产品推荐

