Principle Analysis of Permission Annotation in shiro

Summary

Not long ago, I just learned how to use the permission annotation (), and began to think about it. The initial guess is to annotate @Aspect, which is implemented in a way similar to that shown below (recording audit logs facetly). Later found that this is not the case, so a special analysis of the source code.

@Component
@Aspect
public class AuditLogAspectConfig {
    @Pointcut("@annotation(com.ygsoft.ecp.mapp.basic.audit.annotation.AuditLog) || @annotation(com.ygsoft.ecp.mapp.basic.audit.annotation.AuditLogs)")
    public void pointcut() {        
    }

    @After(value="pointcut()")
    public void after(JoinPoint joinPoint) {
        //Logic of execution
    }
    ...
}

Source code analysis of permission annotations

The DefaultAdvisor AutoProxyCreator class implements the BeanProcessor interface. When the ApplicationContext reads all Bean configuration information, it scans the context to find all Advistors (an Advisor is a pointcut and a notification), and applies these Advisors to all Beans that meet the pointcut.

@Configuration
public class ShiroAnnotationProcessorConfiguration extends AbstractShiroAnnotationProcessorConfiguration{
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    protected DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        return super.defaultAdvisorAutoProxyCreator();
    }

    @Bean
    protected AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        return super.authorizationAttributeSourceAdvisor(securityManager);
    }

}

Authorization Attribute Source Advisor inherits the Static Method Matcher Pointcut Advisor. As shown in the following code, only five annotations are matched, that is to say, only the classes or methods annotated by the five annotations are enhanced. StaticMethod MatcherPointcutAdvisor is an abstract base class for static method tangents, which by default matches all classes. StaticMethodMatcherPointcut includes two main subclasses: NameMatchMethodPointcut and Abstract RegexpMethodPointcut. The former provides a simple string matching method, while the latter uses regular expression matching method. Dynamic Method Tangent: DynamicMethodMatcerPointcut is an abstract base class of dynamic method tangent. By default, it matches all classes and is outdated. It is recommended that DefaultPointcutAdvisor and DynamicMethodMatcherPointcut be used instead. In addition, you need to pay attention to the imported AopAlliance Annotations Authorization Method Interceptor in the constructor.

public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {

    private static final Logger log = LoggerFactory.getLogger(AuthorizationAttributeSourceAdvisor.class);

    private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES =
            new Class[] {
                    RequiresPermissions.class, RequiresRoles.class,
                    RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
            };

    protected SecurityManager securityManager = null;

    public AuthorizationAttributeSourceAdvisor() {
        setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
    }

    public SecurityManager getSecurityManager() {
        return securityManager;
    }

    public void setSecurityManager(org.apache.shiro.mgt.SecurityManager securityManager) {
        this.securityManager = securityManager;
    }

    public boolean matches(Method method, Class targetClass) {
        Method m = method;

        if ( isAuthzAnnotationPresent(m) ) {
            return true;
        }
        
        if ( targetClass != null) {
            try {
                m = targetClass.getMethod(m.getName(), m.getParameterTypes());
                if ( isAuthzAnnotationPresent(m) ) {
                    return true;
                }
            } catch (NoSuchMethodException ignored) {
                
            }
        }

        return false;
    }

    private boolean isAuthzAnnotationPresent(Method method) {
        for( Class<? extends Annotation> annClass : AUTHZ_ANNOTATION_CLASSES ) {
            Annotation a = AnnotationUtils.findAnnotation(method, annClass);
            if ( a != null ) {
                return true;
            }
        }
        return false;
    }

}

When AopAlliance Annotations Authorization Method Interceptor is initialized, interceptors add five method interceptors (all inherited from Authorization Annotation Method Interceptor), which intercept five methods of privilege verification and execute invoke method.

public class AopAllianceAnnotationsAuthorizingMethodInterceptor
        extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor {

    public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
        List<AuthorizingAnnotationMethodInterceptor> interceptors =
                new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);
        AnnotationResolver resolver = new SpringAnnotationResolver();
        
        interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
        interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
        interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
        interceptors.add(new UserAnnotationMethodInterceptor(resolver));
        interceptors.add(new GuestAnnotationMethodInterceptor(resolver));
        setMethodInterceptors(interceptors);
    }
    
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation);
        return super.invoke(mi);
    }
    ...
}

The invoke method of the AopAlliance Annotation Authorization Method Interceptor calls the invoke method of the superclass Authorization Method Interceptor. In this method, the assert Authorized method is executed first to verify the permission, but the verification fails. The Authorization Exception exception is thrown, and the method is interrupted if the verification passes, the met is executed. HodInvocation. proceed (), which is intercepted and requires permission checking.

public abstract class AuthorizingMethodInterceptor extends MethodInterceptorSupport {

    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        assertAuthorized(methodInvocation);
        return methodInvocation.proceed();
    }

    protected abstract void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException;
}

The assert Authorized method ultimately executes Authorized Annotation Method Interceptor. Assert Authorized, while Authorizing Annotation Method Interceptor has five specific implementation classes (Role Annotation Method Interceptor, Permission Annotation Interceptor, Authenticated Annotation Method Interceptor, User Annotation Interceptor) Method Interceptor, Guest Annotation Method Interceptor.

public abstract class AnnotationsAuthorizingMethodInterceptor extends   AuthorizingMethodInterceptor {
  
    protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException {
        //default implementation just ensures no deny votes are cast:
        Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors();
        if (aamis != null && !aamis.isEmpty()) {
            for (AuthorizingAnnotationMethodInterceptor aami : aamis) {
                if (aami.supports(methodInvocation)) {
                    aami.assertAuthorized(methodInvocation);
                }
            }
        }
    }
    ...
}

AuthorizingAnnotationMethod Interceptor's assertAuthorized first obtains the AuthorizingAnnotationHandler from the subclass, and then calls the assertAuthorized method of the implementation class.

public abstract class AuthorizingAnnotationMethodInterceptor extends AnnotationMethodInterceptor
{

    public AuthorizingAnnotationMethodInterceptor( AuthorizingAnnotationHandler handler ) {
        super(handler);
    }

    public AuthorizingAnnotationMethodInterceptor( AuthorizingAnnotationHandler handler,
                                                   AnnotationResolver resolver) {
        super(handler, resolver);
    }

    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        assertAuthorized(methodInvocation);
        return methodInvocation.proceed();
    }

    public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
        try {
            ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi));
        }
        catch(AuthorizationException ae) {
            if (ae.getCause() == null) ae.initCause(new AuthorizationException("Not authorized to invoke method: " + mi.getMethod()));
            throw ae;
        }         
    }
}

Now it's most useful to analyze one of the implementation classes, PermissionAnnotation Method Interceptor, but the actual code of this class is very small. Obviously, the getHandler of the above analysis returns PermissionAnnotation Method Interceptor as PermissionAnnotation Handler.

public class PermissionAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {

    public PermissionAnnotationMethodInterceptor() {
        super( new PermissionAnnotationHandler() );
    }

 
    public PermissionAnnotationMethodInterceptor(AnnotationResolver resolver) {
        super( new PermissionAnnotationHandler(), resolver);
    }
}

In the PermissionAnnotationHandler class, it is finally found that the actual validation logic is still called Subject.checkPermission().

public class PermissionAnnotationHandler extends AuthorizingAnnotationHandler {

    public PermissionAnnotationHandler() {
        super(RequiresPermissions.class);
    }

    protected String[] getAnnotationValue(Annotation a) {
        RequiresPermissions rpAnnotation = (RequiresPermissions) a;
        return rpAnnotation.value();
    }

    public void assertAuthorized(Annotation a) throws AuthorizationException {
        if (!(a instanceof RequiresPermissions)) return;

        RequiresPermissions rpAnnotation = (RequiresPermissions) a;
        String[] perms = getAnnotationValue(a);
        Subject subject = getSubject();

        if (perms.length == 1) {
            subject.checkPermission(perms[0]);
            return;
        }
        if (Logical.AND.equals(rpAnnotation.logical())) {
            getSubject().checkPermissions(perms);
            return;
        }
        if (Logical.OR.equals(rpAnnotation.logical())) {
            boolean hasAtLeastOnePermission = false;
            for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true;
            if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]);
            
        }
    }
}

Implementing similar programmable AOP

Define an annotation

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default "";
}

Inherit the StaticMethodMatcherPointcutAdvisor class and implement the related methods.

@SuppressWarnings("serial")
@Component
public class HelloAdvisor extends StaticMethodMatcherPointcutAdvisor{
    
    public HelloAdvisor() {
        setAdvice(new LogMethodInterceptor());
    }

    public boolean matches(Method method, Class targetClass) {
        Method m = method;
        if ( isAuthzAnnotationPresent(m) ) {
            return true;
        }

        if ( targetClass != null) {
            try {
                m = targetClass.getMethod(m.getName(), m.getParameterTypes());
                return isAuthzAnnotationPresent(m);
            } catch (NoSuchMethodException ignored) {
               
            }
        }
        return false;
    }

    private boolean isAuthzAnnotationPresent(Method method) {
        Annotation a = AnnotationUtils.findAnnotation(method, Log.class);
        return a!= null;
    }
}

Implementing the Method Interceptor interface and defining the logic of aspect processing

public class LogMethodInterceptor implements MethodInterceptor{

    public Object invoke(MethodInvocation invocation) throws Throwable {
        Log log = invocation.getMethod().getAnnotation(Log.class);
        System.out.println("log: "+log.value());
        return invocation.proceed();    
    }
}

Define a test class and add Log annotations

@Component
public class TestHello {

    @Log("test log")
    public String say() {
        return "ss";
    }
}

Write the startup class and configure DefaultAdvisor AutoProxyCreator

@Configuration
public class TestBoot {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext("com.fzsyw.test");  
        TestHello th = ctx.getBean(TestHello.class);
        System.out.println(th.say());
    }
    
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator da = new DefaultAdvisorAutoProxyCreator();
        da.setProxyTargetClass(true);
        return da;
    }
}

The final print results are as follows, which proves that the programmable AOP works.

log: test log
ss

Summary and Reflection

The annotated permission of shiro is really convenient to use. The implementation principle of shiro is also analyzed through source code. The core is to configure Default Advisor AutoProxy Creator and inherit Static Method Matcher Pointcut Advisor. Five of them use a unified set of code and a large number of template patterns to facilitate expansion. Finally, I made a simple example to deepen my understanding of programmable AOP.

Keywords: Java Shiro Apache Attribute

Added by deltawing on Tue, 20 Aug 2019 10:48:30 +0300