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;}
}
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;
}
}
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
}