Kevin
Kevin

Reputation: 1697

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.

Upvotes: 2

Views: 170

Answers (1)

木原金
木原金

Reputation: 307

  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 
}

Upvotes: 2

Related Questions