javaspringspring-bootspring-securityspring-el

How to write custom Spring Security PreAuthorize annotation


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.


Solution

    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 
    }