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包括兩個主要的子類分別是NameMatchMethodPointcut和AbstractRegexpMethodPointcut,前者提供簡單字元串匹配方法前面,而後者使用正則表達式匹配方法前面。動態方法切點:DynamicMethodMatcerPointcut是動態方法切點的抽象基類,默認情況下它匹配所有的類,而且也已經過時,建議使用DefaultPointcutAdvisor和DynamicMethodMatcherPointcut動態方法代替。另外還需關注構造器中的傳入的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的理解。
