I would like to secure my services layer using Spring Security. As explained in the documentation, I need to use a MethodSecurityInterceptor
that will check if
I achieved that by implementing my own AccessDecisionManager
that delegates access decisions to my special interface AccessDecisionStrategy
:
public interface AccessDecisionStrategy {
void decide(Authentication authentication, MethodInvocation methodInvocation, ConfigAttribute configAttribute);
}
Each access decision strategy represents different way of making access decision.
You can easily implement your own strategy (even in other language - for instance Scala):
public class SomeStrategy implements AccessDecisionStrategy { ...
As you can see, my AccessDecisionManager
has a map of strategies. Strategy used by manager is based on annotation argument.
public class MethodSecurityAccessDecisionManager implements AccessDecisionManager {
private Map<String, AccessDecisionStrategy> strategyMap;
public MethodSecurityAccessDecisionManager(Map<String, AccessDecisionStrategy> strategyMap) {
this.strategyMap = strategyMap;
}
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
ConfigAttribute configAttribute = getSingleConfigAttribute(configAttributes);
AccessDecisionStrategy accessDecisionStrategy = strategyMap.get(configAttribute.getAttribute());
if (accessDecisionStrategy == null) {
throw new IllegalStateException("AccessDecisionStrategy with name "
+ configAttribute.getAttribute() + " was not found!");
}
try {
accessDecisionStrategy.decide(authentication, (MethodInvocation) object, configAttribute);
} catch (ClassCastException e) {
throw new IllegalStateException();
}
}
private ConfigAttribute getSingleConfigAttribute(Collection<ConfigAttribute> configAttributes) {
if (configAttributes == null || configAttributes.size() != 1) {
throw new IllegalStateException("Invalid config attribute configuration");
}
return configAttributes.iterator().next();
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return clazz.equals(MethodInvocation.class);
}
}
Now when I want to protect my method I put @Secured
annotation with argument that is name of the strategy:
@Secured("GetByOwner")
FlightSpotting getFlightSpotting(Long id);
You can implement and configure as many strategies as you want:
<bean id="methodSecurityAccessDecisionManager"
class="some.package.MethodSecurityAccessDecisionManager">
<constructor-arg>
<map>
<entry key="GetByOwner">
<bean class="some.package.GetByOwnerStrategy"/>
</entry>
<entry key="SomeOther">
<bean class="some.package.SomeOtherStrategy"/>
</entry>
</map>
</constructor-arg>
</bean>
To inject that access decision manager you type:
<sec:global-method-security secured-annotations="enabled"
access-decision-manager-ref="methodSecurityAccessDecisionManager">
</sec:global-method-security>
I also implemented helper class to handle MethodInvocation
arguments:
import org.aopalliance.intercept.MethodInvocation;
public class MethodInvocationExtractor<ArgumentType> {
private MethodInvocation methodInvocation;
public MethodInvocationExtractor(MethodInvocation methodInvocation) {
this.methodInvocation = methodInvocation;
}
public ArgumentType getArg(int num) {
try {
Object[] arguments = methodInvocation.getArguments();
return (ArgumentType) arguments[num];
} catch (ClassCastException | ArrayIndexOutOfBoundsException e) {
throw new IllegalStateException();
}
}
}
Now you can easily extract interesting arguments in the code of your strategy to make decision:
Let's say I want to get argument number 0
that is of type Long
:
MethodInvocationExtractor<Long> extractor = new MethodInvocationExtractor<>(methodInvocation);
Long id = extractor.getArg(0);
You can implement your own method security annotations based on Spring @PreAuthorize("")
construction.
To fetch extra information about the method(beyond method argument values) to SpEL evaluation context you can implement your own MethodSecurityExpressionHandler
@Service
public class MySecurityExpressionHandler extends
DefaultMethodSecurityExpressionHandler {
@Override
public StandardEvaluationContext createEvaluationContextInternal(
Authentication auth, MethodInvocation mi) {
StandardEvaluationContext evaluationContext = super
.createEvaluationContextInternal(auth, mi);
SomeMethodInfoData methodInfoData = mi.getMethod(). ...;
evaluationContext.setVariable("someData", <value computed based on method info data>);
}
return evaluationContext;
}
and register it in your global-method-security
declaration
<security:global-method-security
pre-post-annotations="enabled">
<security:expression-handler
ref="mySecurityExpressionHandler" />
</security:global-method-security>
Now you can create custom security annotations(and extra process annotation data in MySecurityExpressionHandler if required)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#<someData>")
public @interface CustomSecurityAnnotation { ... }
for example you can create a custom annotation to check user roles without messing with strings:
@MyUserRoleCheck(MyAppRole.Admin)
public void someMethod()