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.
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
│ └── serializerKey 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
