Don’t Overcomplicate Permissions: The “Lazy” Field‑Level Permission Solution for Spring Boot

This article demonstrates a lazy, field‑level permission solution for Spring Boot 3 + Java 17 that uses annotations, AOP, and a custom Jackson serializer to hide unauthorized fields without modifying controllers, DTOs, or business logic.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Don’t Overcomplicate Permissions: The “Lazy” Field‑Level Permission Solution for Spring Boot

Problem Statement

When the same REST endpoint returns different fields for different users, developers often resort to three ad‑hoc solutions:

Creating multiple DTO classes (e.g., AdminUserDTO, UserDTO, SimpleUserDTO)

Adding role‑based if‑else checks inside controllers

Writing explicit column lists in SQL queries

All three approaches work but become difficult to maintain as the number of roles grows.

Field‑Level Permission Control (FLPC)

FLPC defines the visibility or editability of a single object field for a specific user or role. It adds a third granularity level to the traditional permission model:

Interface‑level – can the endpoint be accessed?

Object‑level – can the object be retrieved?

Field‑level – which fields of the object can be seen or edited.

Core Design Idea

Declare field permissions directly on the data model using annotations.

Perform permission checks in a unified layer that reads the current user context, roles, and optional SpEL expressions.

Business code does not contain any permission logic.

Technology Stack (Spring Boot 3 + Java 17)

Spring Boot 3.1.0

Java 17

Spring Security 6.1

Jackson 2.15+

Spring AOP

Project Structure

field-permission-demo
├── src/main/java
│   └── com/icoderoad
│       ├── annotation
│       ├── aspect
│       ├── config
│       ├── context
│       ├── controller
│       ├── model
│       └── serializer

Key Implementations

Field Permission Annotation

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldPermission {
    String[] roles() default {};
    String expression() default "";
    PermissionType type() default PermissionType.BOTH;
    String alias() default "";
}

Permission Type Enum

public enum PermissionType {
    READ,
    WRITE,
    BOTH
}

User Context (ThreadLocal)

@Component
public class SecurityContextHolder {
    private static final ThreadLocal<UserContext> CONTEXT = new ThreadLocal<>();

    @Data
    @AllArgsConstructor
    public static class UserContext {
        private Long userId;
        private String username;
        private Set<String> roles;
        private Map<String, Object> attributes;
        public boolean hasRole(String role) { return roles.contains(role); }
    }

    public static void set(UserContext context) { CONTEXT.set(context); }
    public static UserContext get() { return CONTEXT.get(); }
    public static void clear() { CONTEXT.remove(); }
}

Jackson Field Permission Serializer (Core)

public class FieldPermissionSerializer extends JsonSerializer<Object> {
    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        BeanPropertyWriter writer = (BeanPropertyWriter) gen.getOutputContext().getCurrentValue();
        if (hasPermission(writer)) {
            gen.writeObject(value);
        }
    }
}
If the user lacks permission, the field is omitted from the JSON output.

Jackson Module Registration

public class FieldPermissionModule extends SimpleModule {
    @Override
    public void setupModule(SetupContext context) {
        context.addBeanSerializerModifier(new FieldPermissionSerializerModifier());
    }
}

AOP Aspect: Intercept Controller Return Values

@Aspect
@Component
public class FieldPermissionAspect {
    private final ObjectMapper objectMapper;
    public FieldPermissionAspect(ObjectMapper mapper) {
        this.objectMapper = mapper;
        this.objectMapper.registerModule(new FieldPermissionModule());
    }
    @Around("@within(org.springframework.web.bind.annotation.RestController)")
    public Object around(ProceedingJoinPoint jp) throws Throwable {
        Object result = jp.proceed();
        return filter(result);
    }
}

Data Model Example

@Data
@ClassPermission(roles = {"ADMIN", "USER"})
public class UserDTO {
    @FieldPermission(roles = {"ADMIN"})
    private BigDecimal salary;

    @FieldPermission(roles = {"ADMIN", "USER"})
    private String nickname;
}

Controller (No Permission Logic)

@RestController
@RequestMapping("/api/users")
public class UserController {
    @GetMapping
    public List<UserDTO> list() {
        return userData;
    }
}

Advantages

Fine‑grained field control.

Zero intrusion into business code.

Supports role‑based and expression‑based extensions.

Handled during Jackson serialization, keeping runtime overhead low.

Avoids DTO explosion.

Applicable Scenarios

Multi‑tenant SaaS systems.

Shared admin and regular‑user APIs.

Financial, governmental, or medical systems that require strict data isolation.

API gateways that need to trim fields per caller.

Conclusion

Field‑level permission control moves permission concerns out of controllers and services, allowing developers to write business logic without being aware of permission checks.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

AOPSpring Bootsecurityjacksonjava-17Field-Level Permission
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.