@JsonView 不能直接做字段脱敏,因其仅控制字段是否序列化,不修改字段值;脱敏需通过 @JsonSerialize 自定义序列化器实现,视图决定“要不要”,序列化器决定“怎么呈现”。

为什么 @JsonView 不能直接做字段脱敏
@JsonView 的本质是视图控制,不是安全过滤器。它只决定哪些字段参与序列化,不修改字段值;如果字段本身存的是明文(比如 idCard、phone),直接用 @JsonView 标记该字段,它照样原样输出——脱敏得靠你提前把值处理成星号或掩码。
常见错误现象:@JsonView(User.Public.class) private String phone; 配合 objectMapper.writerWithView(User.Public.class),结果返回的还是完整手机号,不是 138****1234。
- 视图注解只管“要不要序列化”,不管“序列化成什么”
- 字段脱敏必须发生在序列化前(设值时)或序列化中(自定义序列化器)
- 若在 Controller 层手动 set 脱敏值,容易漏、难复用、破坏实体纯净性
推荐做法:用 @JsonSerialize + 自定义 JsonSerializer 做字段级脱敏
把脱敏逻辑下沉到字段级别,和 @JsonView 协同:视图决定“这个字段要不要出现”,序列化器决定“如果出现,怎么呈现”。这样职责清晰,也支持同一字段在不同视图下不同脱敏规则(比如管理后台显示后 4 位,App 接口全隐藏)。
以手机号为例:
public class PhoneSerializer extends JsonSerializer<string> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null || value.length() <p>然后在实体类中标注:</p>
<pre class="brush:java;toolbar:false;">public class User {
public static class Public {}
public static class Admin {}
<pre class="brush:php;toolbar:false;">@JsonView({Public.class, Admin.class})
@JsonSerialize(using = PhoneSerializer.class)
private String phone;
@JsonView(Admin.class) // 管理后台才暴露
private String idCard;}
- 脱敏逻辑与视图解耦,
PhoneSerializer可复用于任何带手机号的类 - 若某视图不需要该字段,
@JsonView会跳过它,@JsonSerialize不生效,无副作用 - 注意:自定义序列化器对
null和异常长度要健壮,否则抛JsonMappingException
进阶:同一字段在不同视图下走不同脱敏策略
Jackson 不支持“按视图动态切换序列化器”,但可以通过上下文传递视图信息,再在序列化器里分支判断。关键点是重写 SerializerProvider 的查找逻辑,或利用 JsonGenerator 的 getOutputContext() 获取当前视图。
更轻量的做法是定义多个序列化器:
public class PhonePublicSerializer extends JsonSerializer<string> { /* 显示 138****1234 */ }
public class PhoneAdminSerializer extends JsonSerializer<string> { /* 显示 138****1234(或全量,依需)*/ }</string></string>
然后在字段上按视图分别标注:
public class User {
@JsonView(Public.class)
@JsonSerialize(using = PhonePublicSerializer.class)
private String phone;
<pre class="brush:php;toolbar:false;">@JsonView(Admin.class)
@JsonSerialize(using = PhoneAdminSerializer.class)
private String phone; // ❌ 编译报错:重复字段}
所以实际要用“包装字段”或“Builder 模式”绕过重复声明限制。更可行的方案是统一用一个序列化器,通过反射读取当前视图类型:
public class ContextualPhoneSerializer extends JsonSerializer<string> {
private Class> activeView;
<pre class="brush:php;toolbar:false;">@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (activeView == User.Public.class) {
gen.writeString(maskPartial(value));
} else if (activeView == User.Admin.class) {
gen.writeString(maskFull(value));
}
}
@Override
public JsonSerializer<string> createContextual(SerializerProvider prov, BeanProperty property) {
activeView = prov.getActiveView();
return this;
}</string>
}
-
createContextual是 Jackson 提供的钩子,能拿到当前正在使用的@JsonView类型 - 必须继承
JsonSerializer并实现该方法,否则activeView始终为null - 这种写法对性能影响极小,但调试困难——建议加日志或单元测试覆盖各视图分支
容易被忽略的兼容性细节
脱敏字段若参与反序列化(POST/PUT 请求体),@JsonSerialize 不起作用,需要配对使用 @JsonDeserialize 和自定义反序列化器,否则前端传入 "138****1234" 会导致绑定失败或数据污染。
- 除非明确禁止客户端提交敏感字段,否则脱敏应仅限响应(序列化)侧
- Spring Boot 中,若全局配置了
spring.jackson.serialization.write_dates_as_timestamps=false,不影响自定义序列化器 - 使用 Lombok 的
@Data时,确保@JsonSerialize注解写在字段上而非 getter 上,否则 Lombok 生成的 getter 会覆盖注解 - 当实体类有继承关系,且父类字段需脱敏,子类视图未显式声明
@JsonView,该字段默认不输出——必须在子类字段上重新标注视图或用@JsonView修饰父类字段并确保子类继承有效
最麻烦的其实是时间字段和枚举字段的组合脱敏:比如“用户注册时间”在公开接口只显示年月,在管理后台显示完整时间戳。这时候得写一个能识别视图的 DateTimeSerializer,而不是依赖 @JsonFormat——后者无法按视图切换 pattern。











