Backend Development 7 min read

Dynamic Annotations and Return Object Protection in Spring Security 6.3

This article explains the new authorization features of Spring Boot 3.3 and Spring Security 6.3, including dynamic annotation parameters, return‑object protection for data security, and custom 403 error handling with code examples and practical guidance.

Java Architecture Diary
Java Architecture Diary
Java Architecture Diary
Dynamic Annotations and Return Object Protection in Spring Security 6.3

Spring Boot 3.3 and Spring Security 6.3 introduce several notable changes in authorization. This article explores these new features and demonstrates their usage with concrete code examples.

1. Dynamic Annotation Parameters

Spring Security method security supports meta‑annotations, allowing you to create readable custom annotations. For example, the standard

@PreAuthorize("hasRole('USER')")

can be wrapped in a meta‑annotation:

<code>@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('USER')")
public @interface IsUser {
    String[] value();
}
</code>

You can then use

@IsUser

on service methods:

<code>@Service
public class DemoService {
    @IsUser
    public Data demoService() {
        return "data";
    }
}
</code>

Instead of creating separate annotations for each role, Spring Security 6.3 lets you define a meta‑annotation that accepts role parameters. Define a bean for template defaults:

<code>@Bean
PrePostTemplateDefaults prePostTemplateDefaults() {
    return new PrePostTemplateDefaults();
}
</code>

Then create a meta‑annotation that can handle any role:

<code>@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole({value})")
public @interface CustomHasAnyRole {
    String[] value();
}
</code>

Use it in a service class:

<code>@Service
public class DemoService {
    private final List<Message> messages;
    public DemoService() {
        messages = new ArrayList<>();
        messages.add(new Message(1, "Message 1"));
    }
    @CustomHasAnyRole({"'USER'", "'ADMIN'"})
    public Message readMessage(Integer id) {
        return messages.get(0);
    }
    @CustomHasAnyRole({"'ADMIN'"})
    public String writeMessage(Message message) {
        return "Message Written";
    }
    @CustomHasAnyRole({"'ADMIN'"})
    public String deleteMessage(Integer id) {
        return "Message Deleted";
    }
}
</code>

In the PIG micro‑service framework, a similar approach is used with

@HasPermission

:

<code>@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("@pms.hasPermission('{value}'.split(','))")
public @interface HasPermission {
    /**
     * Permission strings
     */
    String[] value();
}
</code>

Usage becomes clearer:

<code>@PreAuthorize("@pms.hasPermission('job_sys_job_add')")
↓
@HasPermission("job_sys_job_add")
</code>

2. Return‑Object Protection (Data Permissions)

Spring Security 6.3 adds the

@AuthorizeReturnObject

annotation, which secures the returned domain object. Only users with the appropriate authority can access the protected fields.

<code>public class Account {
    private String iban;
    private Double balance;
    public String getIban() { return iban; }
    @PreAuthorize("hasAuthority('read')")
    public Double getBalance() { return balance; }
}
</code>

The service returning the object is annotated as follows:

<code>@Service
public class AccountService {
    @AuthorizeReturnObject
    public Optional<Account> getAccountByIban(String iban) {
        return Optional.of(new Account("XX1234567809", 2345.6));
    }
}
</code>

The annotation ensures that only callers with the "read" authority can retrieve the

Account

instance.

3. 403 Error Handling

When

@AuthorizeReturnObject

denies access, Spring Security throws an

AccessDeniedException

. You can handle this with a custom

MethodAuthorizationDeniedHandler

implementation that returns a masked value instead of the exception.

<code>@Component
public class MaskMethodAuthorizationDeniedHandler implements MethodAuthorizationDeniedHandler {
    @Override
    public Object handleDeniedInvocation(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
        return "****";
    }
}
</code>

Apply the handler to a method:

<code>@PreAuthorize("hasAuthority('read')")
@HandleAuthorizationDenied(handlerClass = MaskMethodAuthorizationDeniedHandler.class)
public String getIban() {
    return iban;
}
</code>

Feature Preview

Based on Spring Boot 3.3's flexible annotation capabilities, the new version of the PIG micro‑service platform implements the #I8I2YL permission design as a Sa‑Token alternative, compatible with Spring Authorization Server and Sa‑Token OAuth 2.0 models.

All code and architecture remain unchanged while supporting both Spring Authorization Server and Sa‑Token.

Source code: https://github.com/pig-mesh/pig/tree/next-sa

Reference: #I8I2YL Permission Design – Sa‑Token alternative via plugin mechanism

AuthorizationSpring SecurityAccess Denied HandlingDynamic AnnotationsReturn Object ProtectionSpring Boot 3.3
Java Architecture Diary
Written by

Java Architecture Diary

Committed to sharing original, high‑quality technical articles; no fluff or promotional content.

0 followers
Reader feedback

How this landed with the community

login 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.