I have Spring Security set up in a spring-boot app. I can add @PreAuthorize annotations to check authorization on methods by calling my own TenantSecurityService:
  @PostMapping
  @PreAuthorize("@tenantSecurityService.hasAuthority('" + Authorities.PRODUCTS_WRITE + "')")
  public Product createProduct(@RequestBody @Valid ProductCreateRequest createRequest) {
    return productService.createProduct(createRequest);
  }
I really don't like the manual String concatenation that I would need to put on hundreds of methods. I'd like to be able to do something like this:
  @PostMapping
  @AuthorityRequired(Authorities.PRODUCTS_WRITE) // <-- I want this
  public Product createProduct(@RequestBody @Valid ProductCreateRequest createRequest) {
    return productService.createProduct(createRequest);
  }
...
@Retention(RUNTIME)
@Target(METHOD)
@PreAuthorize("@tenantSecurityService.hasAuthority(#value)") // <-- pass in value
public @interface AuthorityRequired {
  String value();
}
But I can't figure out how to pass the value field from AuthorityRequired into the SPEL expression. I have read the Spring Security docs, but they seemed to point in the direction that every method needed its own @PreAuthorize("@tenantSecurityService.hasAuthority... annotation that hard-codes the authority name directly inside the SPEL expression.
I'm looking for any pointers on how to handle authority based authorization with Spring Security in a more convenient way.
I have Spring Security set up in a spring-boot app. I can add @PreAuthorize annotations to check authorization on methods by calling my own TenantSecurityService:
  @PostMapping
  @PreAuthorize("@tenantSecurityService.hasAuthority('" + Authorities.PRODUCTS_WRITE + "')")
  public Product createProduct(@RequestBody @Valid ProductCreateRequest createRequest) {
    return productService.createProduct(createRequest);
  }
I really don't like the manual String concatenation that I would need to put on hundreds of methods. I'd like to be able to do something like this:
  @PostMapping
  @AuthorityRequired(Authorities.PRODUCTS_WRITE) // <-- I want this
  public Product createProduct(@RequestBody @Valid ProductCreateRequest createRequest) {
    return productService.createProduct(createRequest);
  }
...
@Retention(RUNTIME)
@Target(METHOD)
@PreAuthorize("@tenantSecurityService.hasAuthority(#value)") // <-- pass in value
public @interface AuthorityRequired {
  String value();
}
But I can't figure out how to pass the value field from AuthorityRequired into the SPEL expression. I have read the Spring Security docs, but they seemed to point in the direction that every method needed its own @PreAuthorize("@tenantSecurityService.hasAuthority... annotation that hard-codes the authority name directly inside the SPEL expression.
I'm looking for any pointers on how to handle authority based authorization with Spring Security in a more convenient way.
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@PreAuthorize("@tenantSecurityService.hasAuthority(#root)")
public @interface AuthorityRequired {
    String value();
    // You can add as many attributes as you want.
}
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations;
import org.springframework.security.core.Authentication;
import java.util.function.Supplier;
public class RootObject extends SecurityExpressionRoot implements MethodSecurityExpressionOperations {
    private Object filterObject;
    private Object returnObject;
    private Object target;
    private MethodInvocation methodInvocation;//The default root does not provide this object.
    
    public RootObject(Authentication authentication) {//SpringSecurity5
        super(authentication);
    }
    public RootObject(Supplier<Authentication> authentication) {//SpringSecurity6
        super(authentication);
    }
    @Override
    public Object getThis() {return this.target;}
    @Override
    public Object getFilterObject() {return this.filterObject;}
    @Override
    public Object getReturnObject() {return this.returnObject;}
    public MethodInvocation getMethodInvocation() {return this.methodInvocation;}
    public void setThis(Object target) {this.target = target;}
    @Override
    public void setFilterObject(Object filterObject) {this.filterObject = filterObject;}
    @Override
    public void setReturnObject(Object returnObject) {this.returnObject = returnObject;}
    public void setMethodInvocation(MethodInvocation methodInvocation) {this.methodInvocation = methodInvocation;}
}
DefaultMethodSecurityExpressionHandlerimport org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Role;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.expression.EvaluationContext;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.function.Supplier;
@Component
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler {
        @Override
        public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
            // Replace with custom root object
            MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(createSecurityExpressionRoot(authentication, mi)
                    , AopUtils.getMostSpecificMethod(mi.getMethod(), AopProxyUtils.ultimateTargetClass(Objects.requireNonNull(mi.getThis()))), mi.getArguments(), getParameterNameDiscoverer());
            context.setBeanResolver(getBeanResolver());
            return context;
        }
        @Override
        protected MethodSecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, MethodInvocation invocation) {
            return createSecurityExpressionRoot(() -> authentication, invocation);
        }
        private MethodSecurityExpressionOperations createSecurityExpressionRoot(Supplier<Authentication> authentication,
                                                                                MethodInvocation invocation) {
            RootObject root = new RootObject(authentication);
            root.setMethodInvocation(invocation);
            root.setThis(invocation.getThis());
            root.setPermissionEvaluator(getPermissionEvaluator());
            root.setTrustResolver(getTrustResolver());
            root.setRoleHierarchy(getRoleHierarchy());
            root.setDefaultRolePrefix(getDefaultRolePrefix());
            return root;
        }
    }
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.stereotype.Service;
@Service("tenantSecurityService")
public class TenantSecurityService {
    public boolean hasAuthority(RootObject root) {
        MethodInvocation methodInvocation = root.getMethodInvocation();
        if (methodInvocation == null) {
            return true;
        }
        AuthorityRequired annotation;
        if (methodInvocation.getMethod().isAnnotationPresent(AuthorityRequired.class)) {
            annotation = methodInvocation.getMethod().getAnnotation(AuthorityRequired.class);
        } else if (methodInvocation.getMethod().getDeclaringClass().isAnnotationPresent(AuthorityRequired.class)) {
            annotation = methodInvocation.getMethod().getDeclaringClass().getAnnotation(AuthorityRequired.class);
        } else {
            return true;
        }
        // Your authorization logic 
}

@AuthorityRequiredwith multiple different authorities. I would need@AuthorityRequiredProductsWrite,@AuthorityRequiredProductsRead, etc for hundreds of authorities. – Kevin Commented Jan 5 at 15:41@AuthorityRequired(AuthorityType.PRODUCTS_READ). Everything is type-checked. Can you explain the potential issuewhere strings get stored in memory when they are literal versus dynamic? I'm not aware of this difference. – Kevin Commented Jan 6 at 17:19==. The SpEL expressions are all controlled directly by me because I'm writing the contents of the@AuthorityRequiredannotation. – Kevin Commented Jan 7 at 1:22