PAYATHON 2026

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 接口读取实体。

备注:内容仅供参考。