java - How to write custom Spring Security PreAuthorize annotation - Stack Overflow

admin2025-04-30  1

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.

Share Improve this question asked Jan 4 at 21:28 KevinKevin 1,7554 gold badges27 silver badges55 bronze badges 8
  • I may be misunderstanding what you are trying to do, but couldn't you just Add it so on the AuthorityRequired interface is annotated with @PreAuthorize("@tenantSecurityService.hasAuthority('" + Authorities.PRODUCTS_WRITE + "')") and then have it be just @AuthorityRequired on the classes? Even less duplicated code and places for errors that way as well. – heyo Commented Jan 4 at 22:03
  • Hi @heyo, that doesn't allow me to use @AuthorityRequired with multiple different authorities. I would need @AuthorityRequiredProductsWrite, @AuthorityRequiredProductsRead, etc for hundreds of authorities. – Kevin Commented Jan 5 at 15:41
  • Hello @Kevin. I agree, but I do want to caution that you are adding extra code to the security level to get rid "String concatenation", but could unintentionally adding a security issue by accident. Always be extra careful when adding extra code to security stuff and make sure you trust it. Also document it so that it is known why, and that it doesn't get out of sync with how spring security does stuff. Example is where strings get stored in memory when they are literal versus dynamic and things like that. – heyo Commented Jan 6 at 15:53
  • Hi @heyo, I believe that the solution I accepted here will lead to more robust code overall. I'm able to have an enum of Authorities, and use them like: @AuthorityRequired(AuthorityType.PRODUCTS_READ). Everything is type-checked. Can you explain the potential issue where 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
  • 1 I don't find any of those concerns relevant. Where the Strings are stored would only matter if I was comparing them with ==. The SpEL expressions are all controlled directly by me because I'm writing the contents of the @AuthorityRequired annotation. – Kevin Commented Jan 7 at 1:22
 |  Show 3 more comments

1 Answer 1

Reset to default 2
  1. custom annotation
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.
}
  1. custom root object
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;}

}
  1. subclassing DefaultMethodSecurityExpressionHandler
import 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;
        }
    }
  1. authorize a method programmatically
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 
}
转载请注明原文地址:http://anycun.com/QandA/1746026868a91532.html