Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save novoj/2081353 to your computer and use it in GitHub Desktop.
Save novoj/2081353 to your computer and use it in GitHub Desktop.
Combining custom annotations for securing methods with Spring Security (http://blog.novoj.net)
package cz.novoj.spring.security.aop;
import cz.novoj.spring.security.annotation.RulesRelation;
import cz.novoj.spring.security.annotation.RulesRelation.BooleanOperation;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.method.AbstractMethodSecurityMetadataSource;
import org.springframework.security.access.prepost.*;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import java.lang.Class;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Replacement for standard {@link PrePostAnnotationSecurityMetadataSource} that supports multiple different
* custom security annotations that are combined into a single SPEL expression in a following way:
*
* - all security rules placed upon method are by default combined with OR relation
* - security rules placed upon method and the owning class are always combined with OR relation - meaning you
* can place global rule for all methods of the class (such as Admin always can call anything with any parameters) that
* override specific security rules placed on respective methods
* - relation among rules at the same place (ie. annotations for single method, annotations for single class) can be
* changed by {@link @RulesRelation} annotation
*
* @author Jan Novotný (novotnaci@gmail.com)
*/
public class ExperimentalPrePostAnnotationSecurityMetadataSource extends AbstractMethodSecurityMetadataSource {
private final PrePostInvocationAttributeFactory attributeFactory;
public ExperimentalPrePostAnnotationSecurityMetadataSource(PrePostInvocationAttributeFactory attributeFactory) {
this.attributeFactory = attributeFactory;
}
public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) {
if (method.getDeclaringClass() == Object.class) {
return Collections.emptyList();
}
logger.trace("Looking for Pre/Post annotations for method '" +
method.getName() + "' on target class '" + targetClass + "'");
AnnotationGroupHolderTuple<PreFilter> preFilter = findAnnotation(method, targetClass, PreFilter.class);
AnnotationGroupHolderTuple<PreAuthorize> preAuthorize = findAnnotation(method, targetClass, PreAuthorize.class);
AnnotationGroupHolderTuple<PostFilter> postFilter = findAnnotation(method, targetClass, PostFilter.class);
AnnotationGroupHolderTuple<PostAuthorize> postAuthorize = findAnnotation(method, targetClass, PostAuthorize.class);
if (!preFilter.hasValue() && !preAuthorize.hasValue() && !postFilter.hasValue() && !postAuthorize.hasValue() ) {
// There is no meta-data so return
logger.trace("No expression annotations found");
return Collections.emptyList();
}
PreInvocationAttribute pre = getPreInvocationAttribute(preFilter, preAuthorize);
PostInvocationAttribute post = getPostInvocationAttribute(postFilter, postAuthorize);
ArrayList<ConfigAttribute> attrs = new ArrayList<ConfigAttribute>(2);
if (pre != null) {
attrs.add(pre);
logger.debug("Method " + targetClass + "#" + method.getName() + " will be checked before invocation: " + pre.getAttribute());
}
if (post != null) {
attrs.add(post);
logger.debug("Method " + targetClass + "#" + method.getName() + " will be checked after invocation: " + pre.getAttribute());
}
attrs.trimToSize();
return attrs;
}
/**
* Composes PreInvocationAttribute from @PreFilter and @PreAuthorize annotations found on method and class level.
* @param preFilter
* @param preAuthorize
* @return
*/
private PreInvocationAttribute getPreInvocationAttribute(AnnotationGroupHolderTuple<PreFilter> preFilter, AnnotationGroupHolderTuple<PreAuthorize> preAuthorize) {
String filterObject = null;
StringBuilder preFilterAttribute = null;
if (preFilter.hasValue()) {
preFilterAttribute = new StringBuilder();
if (preFilter.getClassAnnotations().hasValue()) {
preFilterAttribute.append("(");
filterObject = composePreFilterAttribute(filterObject, preFilterAttribute, preFilter.getClassAnnotations());
preFilterAttribute.append(")");
}
if (preFilterAttribute.length() > 0 && preFilter.getMethodAnnotations().hasValue()) {
preFilterAttribute.append(" or ");
}
if (preFilter.getMethodAnnotations().hasValue()) {
preFilterAttribute.append("(");
filterObject = composePreFilterAttribute(filterObject, preFilterAttribute, preFilter.getMethodAnnotations());
preFilterAttribute.append(")");
}
}
StringBuilder preAuthorizeAttribute = null;
if (preAuthorize.hasValue()) {
preAuthorizeAttribute = new StringBuilder();
if (preAuthorize.getClassAnnotations().hasValue()) {
preAuthorizeAttribute.append("(");
composePreAuthorizeAttribute(preAuthorizeAttribute, preAuthorize.getClassAnnotations());
preAuthorizeAttribute.append(")");
}
if (preAuthorizeAttribute.length() > 0 && preAuthorize.getMethodAnnotations().hasValue()) {
preAuthorizeAttribute.append(" or ");
}
if (preAuthorize.getMethodAnnotations().hasValue()) {
preAuthorizeAttribute.append("(");
composePreAuthorizeAttribute(preAuthorizeAttribute, preAuthorize.getMethodAnnotations());
preAuthorizeAttribute.append(")");
}
}
return attributeFactory.createPreInvocationAttribute(
preFilterAttribute != null ? preFilterAttribute.toString() : null,
filterObject,
preAuthorizeAttribute != null ? preAuthorizeAttribute.toString() : null
);
}
/**
* Composes SPEL expression for @PreFilter annotation. Takes into account @RulesRelation annotation if defined for
* relation composition.
*
* @param filterObject
* @param preFilterAttribute
* @param preFilterAnnotations
* @return
*/
private String composePreFilterAttribute(String filterObject, StringBuilder preFilterAttribute, AnnotationGroupHolder<PreFilter> preFilterAnnotations) {
final List<PreFilter> annotations = preFilterAnnotations.getAnnotations();
for (int i = 0; i < annotations.size(); i++) {
PreFilter filter = annotations.get(i);
if (filter.filterTarget() != null) {
Assert.isTrue(filterObject == null || filterObject.equals(filter.filterTarget()), "Different filter target objects are not supported!");
filterObject = filter.filterTarget();
}
preFilterAttribute.append("(").append(filter.value()).append(")");
if (i < annotations.size() - 1) {
if (preFilterAnnotations.getRelation() == BooleanOperation.OR) {
preFilterAttribute.append(" or ");
} else {
preFilterAttribute.append(" and ");
}
}
}
return filterObject;
}
/**
* Composes SPEL expression for @PreAuthorize annotation. Takes into account @RulesRelation annotation if defined for
* relation composition.
*
* @param preAuthorizeAttribute
* @param preAuthorizeAnnotations
*/
private void composePreAuthorizeAttribute(StringBuilder preAuthorizeAttribute, AnnotationGroupHolder<PreAuthorize> preAuthorizeAnnotations) {
final List<PreAuthorize> annotations = preAuthorizeAnnotations.getAnnotations();
for (int i = 0; i < annotations.size(); i++) {
PreAuthorize filter = annotations.get(i);
preAuthorizeAttribute.append("(").append(filter.value()).append(")");
if (i < annotations.size() - 1) {
if (preAuthorizeAnnotations.getRelation() == BooleanOperation.OR) {
preAuthorizeAttribute.append(" or ");
} else {
preAuthorizeAttribute.append(" and ");
}
}
}
}
/**
* Composes PostInvocationAttribute from @PostFilter and @PostAuthorize annotations found on method and class level.
* @param postFilter
* @param postAuthorize
* @return
*/
private PostInvocationAttribute getPostInvocationAttribute(AnnotationGroupHolderTuple<PostFilter> postFilter, AnnotationGroupHolderTuple<PostAuthorize> postAuthorize) {
StringBuilder postFilterAttribute = null;
if (postFilter.hasValue()) {
postFilterAttribute = new StringBuilder();
if (postFilter.getClassAnnotations().hasValue()) {
postFilterAttribute.append("(");
composePostFilterAttribute(postFilterAttribute, postFilter.getClassAnnotations());
postFilterAttribute.append(")");
}
if (postFilterAttribute.length() > 0 && postFilter.getMethodAnnotations().hasValue()) {
postFilterAttribute.append(" or ");
}
if (postFilter.getMethodAnnotations().hasValue()) {
postFilterAttribute.append("(");
composePostFilterAttribute(postFilterAttribute, postFilter.getMethodAnnotations());
postFilterAttribute.append(")");
}
}
StringBuilder postAuthorizeAttribute = null;
if (postAuthorize.hasValue()) {
postAuthorizeAttribute = new StringBuilder();
if (postAuthorize.getClassAnnotations().hasValue()) {
postAuthorizeAttribute.append("(");
composePostAuthorizeAttribute(postAuthorizeAttribute, postAuthorize.getClassAnnotations());
postAuthorizeAttribute.append(")");
}
if (postAuthorizeAttribute.length() > 0 && postAuthorize.getMethodAnnotations().hasValue()) {
postAuthorizeAttribute.append(" or ");
}
if (postAuthorize.getMethodAnnotations().hasValue()) {
postAuthorizeAttribute.append("(");
composePostAuthorizeAttribute(postAuthorizeAttribute, postAuthorize.getMethodAnnotations());
postAuthorizeAttribute.append(")");
}
}
return attributeFactory.createPostInvocationAttribute(
postFilterAttribute != null ? postFilterAttribute.toString() : null,
postAuthorizeAttribute != null ? postAuthorizeAttribute.toString() : null
);
}
/**
* Composes SPEL expression for @PostFilter annotation. Takes into account @RulesRelation annotation if defined for
* relation composition.
*
* @param postFilterAttribute
* @param postFilterAnnotations
*/
private void composePostFilterAttribute(StringBuilder postFilterAttribute, AnnotationGroupHolder<PostFilter> postFilterAnnotations) {
final List<PostFilter> annotations = postFilterAnnotations.getAnnotations();
for (int i = 0; i < annotations.size(); i++) {
PostFilter filter = annotations.get(i);
postFilterAttribute.append("(").append(filter.value()).append(")");
if (i < annotations.size() - 1) {
if (postFilterAnnotations.getRelation() == BooleanOperation.OR) {
postFilterAttribute.append(" or ");
} else {
postFilterAttribute.append(" and ");
}
}
}
}
/**
* Composes SPEL expression for @PostAuthorize annotation. Takes into account @RulesRelation annotation if defined for
* relation composition.
*
* @param postAuthorizeAttribute
* @param postAuthorizeAnnotations
*/
private void composePostAuthorizeAttribute(StringBuilder postAuthorizeAttribute, AnnotationGroupHolder<PostAuthorize> postAuthorizeAnnotations) {
final List<PostAuthorize> annotations = postAuthorizeAnnotations.getAnnotations();
for (int i = 0; i < annotations.size(); i++) {
PostAuthorize filter = annotations.get(i);
postAuthorizeAttribute.append("(").append(filter.value()).append(")");
if (i < annotations.size() - 1) {
if (postAuthorizeAnnotations.getRelation() == BooleanOperation.OR) {
postAuthorizeAttribute.append(" or ");
} else {
postAuthorizeAttribute.append(" and ");
}
}
}
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* This method finds a particular annotation on method or class level. It uses following schema:
* 1) tries to find annotations on specific method {@link ClassUtils#getMostSpecificMethod(Method, Class<?>)}
* 2) tries to find annotations on passed method (if differs from specific one)
* 3) tries to find annotations on class level
*
* This method differs from the former Spring Security behaviour so that if finds ALL annotations of the passed
* kind. Ie. when you have multiple custom annotations placed on the method / class and all these annotations are
* annotated with fe. @PreAuthorize annotation, all these annotations are found and combined together into
* the AnnotationGroupHolderTuple.
*
* Lookup doesn't finish when annotations are found on the method level, but class is examined always too. Return
* object always hold both results - method and class annotations together.
*/
private <A extends Annotation> AnnotationGroupHolderTuple<A> findAnnotation(Method method, Class<?> targetClass, Class<A> annotationClass) {
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
List<A> methodAnnotation = ExtendedAnnotationUtils.findAnnotation(specificMethod, annotationClass);
RulesRelation methodAnnotationRelation = AnnotationUtils.findAnnotation(specificMethod, RulesRelation.class);
if (methodAnnotation != null) {
logger.debug(methodAnnotation + " found on specific method: " + specificMethod);
} else {
// Check the original (e.g. interface) method
if (specificMethod != method) {
methodAnnotation = ExtendedAnnotationUtils.findAnnotation(method, annotationClass);
methodAnnotationRelation = AnnotationUtils.findAnnotation(method, RulesRelation.class);
if (methodAnnotation != null) {
logger.debug(methodAnnotation + " found on: " + method);
}
}
}
// Check the class-level (note declaringClass, not targetClass, which may not actually implement the method)
List<A> classAnnotation = ExtendedAnnotationUtils.findAnnotation(specificMethod.getDeclaringClass(), annotationClass);
RulesRelation classAnnotationRelation = AnnotationUtils.findAnnotation(specificMethod.getDeclaringClass(), RulesRelation.class);
if (classAnnotation != null) {
logger.debug(classAnnotation + " found on: " + specificMethod.getDeclaringClass().getName());
}
return new AnnotationGroupHolderTuple<A>(
new AnnotationGroupHolder<A>(classAnnotation, classAnnotationRelation != null ? classAnnotationRelation.value() : BooleanOperation.OR),
new AnnotationGroupHolder<A>(methodAnnotation, methodAnnotationRelation != null ? methodAnnotationRelation.value() : BooleanOperation.OR)
);
}
/**
* Simple DTO for transferring found annotations on class and method level along with related @RulesRelation annotation value.
* @param <A>
*/
private static class AnnotationGroupHolderTuple<A> {
private final AnnotationGroupHolder<A> classAnnotations;
private final AnnotationGroupHolder<A> methodAnnotations;
private AnnotationGroupHolderTuple(AnnotationGroupHolder<A> classAnnotations, AnnotationGroupHolder<A> methodAnnotations) {
this.classAnnotations = classAnnotations;
this.methodAnnotations = methodAnnotations;
}
public boolean hasValue() {
return classAnnotations.hasValue() || methodAnnotations.hasValue();
}
public AnnotationGroupHolder<A> getClassAnnotations() {
return classAnnotations;
}
public AnnotationGroupHolder<A> getMethodAnnotations() {
return methodAnnotations;
}
}
/**
* Simple DTO for transferring found annotations and @RulesRelation annotation value.
* @param <A>
*/
private static class AnnotationGroupHolder<A> {
private final List<A> annotations;
private final BooleanOperation relation;
private AnnotationGroupHolder(List<A> annotations, BooleanOperation relation) {
this.annotations = annotations;
this.relation = relation;
}
public boolean hasValue() {
return annotations != null && !annotations.isEmpty();
}
public List<A> getAnnotations() {
return annotations;
}
public BooleanOperation getRelation() {
return relation;
}
}
}
package cz.novoj.spring.security.aop;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.Assert;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
/**
* Alteration of the base Spring methods in {@link AnnotationUtils} with small change allowing to find multiple annotations
* instead of first one.
*
* @author Jan Novotný (novotnaci@gmail.com)
*/
public abstract class ExtendedAnnotationUtils {
private static final Map<Class, Boolean> annotatedInterfaceCache = new WeakHashMap<Class, Boolean>();
/**
* Returns all annotations that are exactly instance of passed type or multiple custom annotations that are
* itself annotated with passed annotationType.
*
* Slightly modified copy of:
*
* @see AnnotationUtils#getAnnotation(java.lang.reflect.Method, Class)
*/
public static <A extends Annotation> List<A> getAnnotation(Method method, Class<A> annotationType) {
Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method);
List<A> result = new ArrayList<A>();
A ann = resolvedMethod.getAnnotation(annotationType);
if (ann != null) {
result.add(ann);
}
for (Annotation metaAnn : resolvedMethod.getAnnotations()) {
ann = metaAnn.annotationType().getAnnotation(annotationType);
if (ann != null) {
result.add(ann);
}
}
return result.isEmpty() ? null : result;
}
/**
* Returns all annotations that are exactly instance of passed type or multiple custom annotations that are
* itself annotated with passed annotationType.
*
* Slightly modified copy of:
*
* @see AnnotationUtils#findAnnotation(java.lang.reflect.Method, Class)
*/
public static <A extends Annotation> List<A> findAnnotation(Method method, Class<A> annotationType) {
List<A> annotation = getAnnotation(method, annotationType);
Class<?> cl = method.getDeclaringClass();
if (annotation == null) {
annotation = searchOnInterfaces(method, annotationType, cl.getInterfaces());
}
while (annotation == null) {
cl = cl.getSuperclass();
if (cl == null || cl == Object.class) {
break;
}
try {
Method equivalentMethod = cl.getDeclaredMethod(method.getName(), method.getParameterTypes());
annotation = getAnnotation(equivalentMethod, annotationType);
if (annotation == null) {
annotation = searchOnInterfaces(method, annotationType, cl.getInterfaces());
}
}
catch (NoSuchMethodException ex) {
// We're done...
}
}
return annotation;
}
/**
* Returns all annotations that are exactly instance of passed type or multiple custom annotations that are
* itself annotated with passed annotationType.
*
* Slightly modified copy of:
*
* @see AnnotationUtils#getAnnotation(java.lang.reflect.Method, Class)
*/
public static <A extends Annotation> List<A> getAnnotation(Class<?> clazz, Class<A> annotationType) {
List<A> result = new ArrayList<A>();
A ann = clazz.getAnnotation(annotationType);
if (ann != null) {
result.add(ann);
}
for (Annotation metaAnn : clazz.getAnnotations()) {
ann = metaAnn.annotationType().getAnnotation(annotationType);
if (ann != null) {
result.add(ann);
}
}
return result.isEmpty() ? null : result;
}
/**
* Returns all annotations that are exactly instance of passed type or multiple custom annotations that are
* itself annotated with passed annotationType.
*
* Slightly modified copy of:
*
* @see AnnotationUtils#findAnnotation(Class, Class)
*/
public static <A extends Annotation> List<A> findAnnotation(Class<?> clazz, Class<A> annotationType) {
Assert.notNull(clazz, "Class must not be null");
List<A> annotation = getAnnotation(clazz, annotationType);
if (annotation != null) {
return annotation;
}
for (Class<?> ifc : clazz.getInterfaces()) {
annotation = findAnnotation(ifc, annotationType);
if (annotation != null) {
return annotation;
}
}
Class<?> superClass = clazz.getSuperclass();
if (superClass == null || superClass == Object.class) {
return null;
}
return findAnnotation(superClass, annotationType);
}
private static <A extends Annotation> List<A> searchOnInterfaces(Method method, Class<A> annotationType, Class[] ifcs) {
List<A> annotation = null;
for (Class<?> iface : ifcs) {
if (isInterfaceWithAnnotatedMethods(iface)) {
try {
Method equivalentMethod = iface.getMethod(method.getName(), method.getParameterTypes());
annotation = getAnnotation(equivalentMethod, annotationType);
}
catch (NoSuchMethodException ex) {
// Skip this interface - it doesn't have the method...
}
if (annotation != null) {
break;
}
}
}
return annotation;
}
private static boolean isInterfaceWithAnnotatedMethods(Class<?> iface) {
synchronized (annotatedInterfaceCache) {
Boolean flag = annotatedInterfaceCache.get(iface);
if (flag != null) {
return flag;
}
boolean found = false;
for (Method ifcMethod : iface.getMethods()) {
if (ifcMethod.getAnnotations().length > 0) {
found = true;
break;
}
}
annotatedInterfaceCache.put(iface, found);
return found;
}
}
}
package cz.novoj.spring.security.annotation;
import java.lang.annotation.*;
/**
* Directly specifies form of relation among different custom security annotations at the same place (ie. single method,
* single class).
*
* @author Jan Novotný (novotnaci@gmail.com)
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RulesRelation {
enum BooleanOperation { OR, AND }
BooleanOperation value() default BooleanOperation.OR;
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<aop:config proxy-target-class="true">
<aop:advisor advice-ref="experimentalMethodSecurityInterceptor"
pointcut="execution(@(@org.springframework.security.access.prepost.PreAuthorize *) * *.* (..))"/>
<aop:advisor advice-ref="experimentalMethodSecurityInterceptor"
pointcut="execution(@(@org.springframework.security.access.prepost.PreFilter *) * *.* (..))"/>
<aop:advisor advice-ref="experimentalMethodSecurityInterceptor"
pointcut="execution(@(@org.springframework.security.access.prepost.PostAuthorize *) * *.* (..))"/>
<aop:advisor advice-ref="experimentalMethodSecurityInterceptor"
pointcut="execution(@(@org.springframework.security.access.prepost.PostFilter *) * *.* (..))"/>
</aop:config>
<!-- Configure custom security interceptor -->
<bean id="experimentalMethodSecurityInterceptor"
class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<property name="securityMetadataSource">
<bean class="cz.novoj.spring.security.aop.ExperimentalPrePostAnnotationSecurityMetadataSource">
<constructor-arg>
<bean class="org.springframework.security.access.expression.method.ExpressionBasedAnnotationAttributeFactory">
<constructor-arg ref="expressionHandler"/>
</bean>
</constructor-arg>
</bean>
</property>
<property name="authenticationManager" ref="authenticationManager"/>
<property name="validateConfigAttributes" value="false"/>
<property name="accessDecisionManager">
<bean class="org.springframework.security.access.vote.AffirmativeBased">
<constructor-arg>
<list>
<bean class="org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter">
<constructor-arg>
<bean class="org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice">
<property name="expressionHandler" ref="expressionHandler"/>
</bean>
</constructor-arg>
</bean>
</list>
</constructor-arg>
</bean>
</property>
</bean>
<bean id="expressionHandler"
class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler"/>
</beans>
@gvsrini
Copy link

gvsrini commented Dec 4, 2014

I am very much interested in this project. Can you please share your code as a github project please.

@dodgex
Copy link

dodgex commented May 4, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment