Spring Data REST 如何不使用 projections 按用户角色序列化实体?
可以不使用 projections,直接在 Jackson 序列化时按当前用户角色判断权限,省略无权查看的 JSON 属性。
在实体字段上添加 @PreAuthorize 无法实现这种过滤。它只支持方法和类型,不支持字段,也不会在拒绝访问后自动省略 JSON 属性。
为什么 @PreAuthorize 和 projections 不能直接解决问题
@PreAuthorize(注意拼写,不是 @PreAutorize)通过方法安全机制判断是否允许执行一次方法调用。给普通 JPA 实体的 getter 添加这个注解,不会让实体自动成为受 Spring Security 保护的代理。即使 getter 上的授权拦截生效,默认的拒绝行为也是抛出异常,无法据此省略字段。参见 Spring Security 方法安全说明。
projections 可以改变返回的数据结构,但客户端可以选择是否使用投影。因此,仅在某个 projection 中排除敏感字段,无法保护默认的实体响应。Spring Data REST 的 projection 还可以显式暴露被 @JsonIgnore 隐藏的属性,已有投影也需要检查。参见 projections 与隐藏数据说明。
如果希望同一个接口只向管理员返回额外字段,可以用自定义注解标记属性,再由 Jackson 过滤器读取当前认证信息并判断权限。
实现字段标记与角色过滤器
下面的示例适用于使用 Jackson 2、提供 configureJacksonObjectMapper(ObjectMapper) 配置入口的 Spring Data REST 项目,并按常规 Servlet 请求流程处理。代码中的 JPA 注解来自 jakarta.persistence。
使用 Jackson 3 的项目需要根据版本调整 Jackson API 和注册方式,不能直接照搬这些导入。
1. 定义管理员字段注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface AdminOnly {
}
这个注解用于标记属性,权限由过滤器判断。
2. 在实体上启用过滤器
import com.fasterxml.jackson.annotation.JsonFilter;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
@JsonFilter("roleBasedFields")
public class Customer {
@Id
private Long id;
private String name;
@AdminOnly
private String adminNotes;
protected Customer() {
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getAdminNotes() {
return adminNotes;
}
}
按常规 Jackson 属性发现规则,字段上的注解会合并到对应属性,也可以把 @AdminOnly 放在 getter 上。如果项目定制过属性可见性或注解解析,需要验证过滤器能否读取该注解。
3. 在序列化时判断权限
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
public class RoleBasedPropertyFilter extends SimpleBeanPropertyFilter {
@Override
public void serializeAsField(
Object pojo,
JsonGenerator generator,
SerializerProvider provider,
PropertyWriter writer) throws Exception {
boolean adminOnly =
writer.getAnnotation(AdminOnly.class) != null;
if (!adminOnly || isAdmin()) {
writer.serializeAsField(pojo, generator, provider);
} else if (!generator.canOmitFields()) {
writer.serializeAsOmittedField(pojo, generator, provider);
}
}
private boolean isAdmin() {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
return authentication != null
&& authentication.isAuthenticated()
&& authentication.getAuthorities().stream()
.anyMatch(authority ->
"ROLE_ADMIN".equals(authority.getAuthority()));
}
}
这里精确匹配权限字符串 ROLE_ADMIN。如果系统存储的是 ADMIN 或其他权限名称,需要修改判断条件。这段代码不会自动计算角色层级。
没有认证信息时,过滤器会省略管理员字段。权限判断发生在写出属性之前,因此不会先调用对应 getter 再隐藏字段。Jackson 的扩展入口见 SimpleBeanPropertyFilter API。
注册到 Spring Data REST 使用的 mapper
过滤器必须注册到 Spring Data REST 实际使用的 Jackson mapper 上。单独创建一个新的 ObjectMapper,不能保证仓库接口会使用它。
项目提供以下 Jackson 2 配置方法时,可以这样注册:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer;
@Configuration
public class RestSerializationConfig
implements RepositoryRestConfigurer {
@Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
SimpleFilterProvider filters = new SimpleFilterProvider()
.addFilter(
"roleBasedFields",
new RoleBasedPropertyFilter());
objectMapper.setFilterProvider(filters);
}
}
示例假设 mapper 尚未配置其他 FilterProvider。如果已有过滤器,应将新规则合并到现有配置中,避免直接覆盖。
Spring Data REST 的 mapper 定制方法在不同版本中签名有所变化,例如使用 Jackson 3 的版本通过 MapperBuilder 配置。具体应以项目依赖对应的 API 为准。参见 Spring Data REST 对象映射说明。
配置后,普通用户请求默认实体接口时,无需传入 projection 参数,响应中就会省略 adminNotes;管理员的响应会保留该属性。以下示例只列出业务字段,省略了 HAL 链接等内容:
普通用户:
{
"name": "Alice"
}
管理员:
{
"name": "Alice",
"adminNotes": "需要人工复核"
}
其他 Jackson 规则仍然适用于管理员。例如,@JsonIgnore 排除的属性不会因为用户拥有管理员角色而重新出现。
上线前需要确认的边界
- 过滤器只控制输出。普通用户能否通过
POST、PUT或PATCH修改adminNotes,需要在服务层授权或请求 DTO 中另行控制。 - 验证集合、单个资源、搜索接口、关联资源和已有 projections。投影对象、DTO、自定义序列化器及其他 mapper 不一定执行实体上的过滤规则。
- 权限判断必须使用当前请求的认证信息。不要在过滤器构造时保存角色,也不要按请求修改共享 mapper。异步处理或切换线程后,要确保序列化时仍能取得正确的安全上下文。
- 同一 URL 会根据用户权限返回不同内容,缓存策略必须防止普通用户取得管理员响应。
至少要验证:普通用户不带 projection 请求时,响应中没有该字段;管理员请求时,响应中有该字段。再按“管理员、普通用户、管理员”的顺序连续请求,检查权限状态或缓存是否串用。这些验证需要在实际项目中执行,上述代码尚未提供相应的测试结果。
如果敏感字段较多,或者接口有多种返回路径,分别使用普通用户 DTO 和管理员 DTO 通常更容易审查。采用这种方案时,还要关闭或限制原实体仓库的直接导出入口,避免客户端绕过 DTO 接口读取实体。
备注:内容仅供参考。
