Shiro許可權註解原理

  • 2019 年 10 月 3 日
  • 筆記

概述

前不久剛學會使用許可權註解(),開始思索了一番。最開始猜測實現方式是註解@Aspect,具體實現方式類似如下所示(切面記錄審計日誌)。後來發現並非如此,所以特地分析一下源碼。

@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) {          //執行的邏輯      }      ...  }

許可權註解的源碼分析

DefaultAdvisorAutoProxyCreator這個類實現了BeanProcessor介面,當ApplicationContext讀取所有的Bean配置資訊後,這個類將掃描上下文,尋找所有的Advistor(一個Advisor是一個切入點和一個通知的組成),將這些Advisor應用到所有符合切入點的Bean中。

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

AuthorizationAttributeSourceAdvisor繼承了StaticMethodMatcherPointcutAdvisor,如下程式碼所示,只匹配五個註解,也就是說只對這五個註解標註的類或者方法增強。StaticMethodMatcherPointcutAdvisor是靜態方法切點的抽象基類,默認情況下它匹配所有的類。StaticMethodMatcherPointcut包括兩個主要的子類分別是NameMatchMethodPointcutAbstractRegexpMethodPointcut,前者提供簡單字元串匹配方法前面,而後者使用正則表達式匹配方法前面。動態方法切點:DynamicMethodMatcerPointcut是動態方法切點的抽象基類,默認情況下它匹配所有的類,而且也已經過時,建議使用DefaultPointcutAdvisorDynamicMethodMatcherPointcut動態方法代替。另外還需關注構造器中的傳入的AopAllianceAnnotationsAuthorizingMethodInterceptor

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

AopAllianceAnnotationsAuthorizingMethodInterceptor在初始化時,interceptors添加了5個方法攔截器(都繼承自AuthorizingAnnotationMethodInterceptor),這5個攔截器分別對5種許可權驗證的方法進行攔截,執行invoke方法。

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

AopAllianceAnnotationsAuthorizingMethodInterceptor的invoke方法,又會調用超類AuthorizingMethodInterceptor的invoke方法,在該方法中先執行assertAuthorized方法,進行許可權校驗,校驗不通過,拋出AuthorizationException異常,中斷方法;校驗通過,則執行methodInvocation.proceed(),該方法也就是被攔截並且需要許可權校驗的方法。

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

assertAuthorized方法最終執行的還是AuthorizingAnnotationMethodInterceptor.assertAuthorized,而AuthorizingAnnotationMethodInterceptor有5中的具體的實現類(RoleAnnotationMethodInterceptor, PermissionAnnotationMethodInterceptor, AuthenticatedAnnotationMethodInterceptor, UserAnnotationMethodInterceptor, GuestAnnotationMethodInterceptor)。

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

AuthorizingAnnotationMethodInterceptor的assertAuthorized,首先從子類獲取AuthorizingAnnotationHandler,再調用該實現類的assertAuthorized方法。

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

現在分析其中一種實現類PermissionAnnotationMethodInterceptor,也是用的最多的,但是這個類的實際程式碼很少,很明顯上述分析的getHandler在PermissionAnnotationMethodInterceptor中返回值為PermissionAnnotationHandler

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

PermissionAnnotationHandler類中,終於發現實際的檢驗邏輯,還是調用的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]);            }      }  }  

實現類似編程式AOP

定義一個註解

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

繼承StaticMethodMatcherPointcutAdvisor類,並實現相關的方法。

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

實現MethodInterceptor介面,定義切面處理的邏輯

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

定義一個測試類,並添加Log註解

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

編寫啟動類,並且配置DefaultAdvisorAutoProxyCreator

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

最終列印的結果如下,證明編程式的AOP生效。

log: test log  ss

總結與思考

Shiro的註解式許可權,使用確實方便,通過源碼也分析了它的實現原理,比較核心的是配置DefaultAdvisorAutoProxyCreator和繼承StaticMethodMatcherPointcutAdvisor。其中的5中許可權註解,使用了統一一套程式碼架構,用到了的模板模式,方便擴展。最後自己也簡單做了一個小例子,加深對編程式AOP的理解。