因为我司项目重构,我负责编写了一些模块,而每个模块Controller都有list,add等相同的方法,他们并不需要处理业务逻辑,只是将请求转到Service层处理完后,根据返回结果再转到相应的视图。所以我编写了Controller层的抽象类,并且在抽象类中实现常用的方法的方法且标记此类的常用注解
@RequestMapping
(
“/list”
)。
然后其他的所有想要具有 普通CRUD功能的Controller都继承这个抽象类,那就完全不用再写一遍了,而且根据类上的
@RequestMapping
注解即可自动分路径,节约大量工作时间,和维护时间,每个类只需要设置自己的视图地址即可,将转发功能再向上提了一层。
而在这里出了个问题,我司使用Shiro作为权限管理,而@RequiresPermissions注解目前还不能直接写在类上,只能针对某个方法进行注解,这就导致我的抽象类完不能工作了,于是有了本文。
首先我们先找到shiro进行权限处理的入口,可是我并不知道怎么找。。。于是我想到让他报错,然后看请求栈肯定能找到。
严重: Servlet.service() for servlet [spring] in context with path [] threw exception [Request processing failed; nested exception is org.apache.shiro.authz.UnauthenticatedException: This subject is anonymous - it does not have any identifying principals and authorization operations require an identity to check against. A Subject instance will acquire these identifying principals automatically after a successful login is performed be executing org.apache.shiro.subject.Subject.login(AuthenticationToken) or when 'Remember Me' functionality is enabled by the SecurityManager. This exception can also occur when a previously logged-in Subject has logged out which makes it anonymous again. Because an identity is currently not known due to any of these conditions, authorization is denied.] with root cause
org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method: public com.component.response.RespJSON com.web.mvc.business.ProductController.listJson(java.util.Map,java.lang.Integer,java.lang.Integer)
at org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor.assertAuthorized(<span style="color:#FF0000;">AuthorizingAnnotationMethodInterceptor.java:90</span>)
at org.apache.shiro.authz.aop.AnnotationsAuthorizingMethodInterceptor.assertAuthorized(AnnotationsAuthorizingMethodInterceptor.java:100)
at org.apache.shiro.authz.aop.AuthorizingMethodInterceptor.invoke(AuthorizingMethodInterceptor.java:38)
at org.apache.shiro.spring.security.interceptor.AopAllianceAnnotationsAuthorizingMethodInterceptor.invoke(AopAllianceAnnotationsAuthorizingMethodInterceptor.java:115)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:646)
at com.web.mvc.business.ProductController$$EnhancerByCGLIB$$76eb0343.listJson(<generated>)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:214)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:132)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:748)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:689)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:945)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:876)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:931)
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:822)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:807)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:728)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
我们直接进红色那个类里打个断点,可以得到如下请求栈:
看到好多AOP,想着也是,要判断权限肯定得AOP才方便。
我们一个一个的下去,看看他都经过了什么处理。
就在第二行:
AnnotationsAuthorizingMethodInterceptor,这个类里: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); } } } }
在这里他获得了权限拦截器,并且逐个去验证,再去找他的实现类:AopAllianceAnnotationsAuthorizingMethodInterceptor
public AopAllianceAnnotationsAuthorizingMethodInterceptor() { List<AuthorizingAnnotationMethodInterceptor> interceptors = new ArrayList<AuthorizingAnnotationMethodInterceptor>(5); //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the //raw JDK resolution process. AnnotationResolver resolver = new SpringAnnotationResolver(); //we can re-use the same resolver instance - it does not retain state: 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); }
确实,在这里将各个权限的拦截器放进了拦截器栈,此时找到这里我就找不到了。。但是我知道这个
AopAllianceAnnotationsAuthorizingMethodInterceptor
肯定是要被创建出来的,所以把断点打在构造器中,得到如下请求栈:
那明显第二个AuthorizationAttributeSourceAdvisor就是了嘛,Spring在启动的时候初始化的
publicpublic AuthorizationAttributeSourceAdvisor() { setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor()); }
Spring知道这东西,那肯定是我们配置的,搜索一下项目,发现有这个:
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
哎,找了半天,却在眼前而不自知,然而此时根据上面的分析我们也知道了
1.shiro根据权限拦截器栈中的权限拦截器去验证权限(AopAllianceAnnotationsAuthorizingMethodInterceptor)
2.看名字就知道PermissionAnnotationMethodInterceptor这个是和我们所需的注解有关,他继承了AuthorizingAnnotationMethodInterceptor/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.shiro.authz.aop; import org.apache.shiro.aop.AnnotationMethodInterceptor; import org.apache.shiro.aop.AnnotationResolver; import org.apache.shiro.aop.MethodInvocation; import org.apache.shiro.authz.AuthorizationException; /** * An <tt>AnnotationMethodInterceptor</tt> that asserts the calling code is authorized to execute the method * before allowing the invocation to continue by inspecting code annotations to perform an access control check. * * @since 0.1 */ public abstract class AuthorizingAnnotationMethodInterceptor extends AnnotationMethodInterceptor { /** * Constructor that ensures the internal <code>handler</code> is set which will be used to perform the * authorization assertion checks when a supported annotation is encountered. * @param handler the internal <code>handler</code> used to perform authorization assertion checks when a * supported annotation is encountered. */ public AuthorizingAnnotationMethodInterceptor( AuthorizingAnnotationHandler handler ) { super(handler); } /** * * @param handler * @param resolver * @since 1.1 */ public AuthorizingAnnotationMethodInterceptor( AuthorizingAnnotationHandler handler, AnnotationResolver resolver) { super(handler, resolver); } /** * Ensures the <code>methodInvocation</code> is allowed to execute first before proceeding by calling the * {@link #assertAuthorized(org.apache.shiro.aop.MethodInvocation) assertAuthorized} method first. * * @param methodInvocation the method invocation to check for authorization prior to allowing it to proceed/execute. * @return the return value from the method invocation (the value of {@link org.apache.shiro.aop.MethodInvocation#proceed() MethodInvocation.proceed()}). * @throws org.apache.shiro.authz.AuthorizationException if the <code>MethodInvocation</code> is not allowed to proceed. * @throws Throwable if any other error occurs. */ public Object invoke(MethodInvocation methodInvocation) throws Throwable { assertAuthorized(methodInvocation); return methodInvocation.proceed(); } /** * Ensures the calling Subject is authorized to execute the specified <code>MethodInvocation</code>. * <p/> * As this is an AnnotationMethodInterceptor, this implementation merely delegates to the internal * {@link AuthorizingAnnotationHandler AuthorizingAnnotationHandler} by first acquiring the annotation by * calling {@link #getAnnotation(MethodInvocation) getAnnotation(methodInvocation)} and then calls * {@link AuthorizingAnnotationHandler#assertAuthorized(java.lang.annotation.Annotation) handler.assertAuthorized(annotation)}. * * @param mi the <code>MethodInvocation</code> to check to see if it is allowed to proceed/execute. * @throws AuthorizationException if the method invocation is not allowed to continue/execute. */ <span style="color:#FF0000;"> public void assertAuthorized(MethodInvocation mi) throws AuthorizationException { try { ((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi)); } catch(AuthorizationException ae) { // Annotation handler doesn't know why it was called, so add the information here if possible. // Don't wrap the exception here since we don't want to mask the specific exception, such as // UnauthenticatedException etc. if (ae.getCause() == null) ae.initCause(new AuthorizationException("Not authorized to invoke method: " + mi.getMethod())); throw ae; } }</span> }
而红色部分就是我们第一次断点的地方,再进入assertAuthorized看看怎么认证的,他有5个实现,我们当然是进入PermissionAnnotationHandler,
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())) { // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first boolean hasAtLeastOnePermission = false; for (String permission : perms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true; // Cause the exception if none of the role match, note that the exception message will be a bit misleading if (!hasAtLeastOnePermission) getSubject().checkPermission(perms[0]); } }
OK,重点就是这里,他获得了方法上的注解,然后用注解value去判断是否有权限了,而我们想要组合该方法所属类上的注解,那我们肯定得改这个方法
,问题是看这个方法的签名,直接接收了一个注解,不给我们获取类注解的机会,那肯定又得重新写一个自己的类。于是有了如下:
MyPermissionAnnotationHandlerpublic void assertAuthorized(MethodInvocation mi) throws AuthorizationException { RequiresPermissions methodAnnotation = mi.getMethod().getAnnotation(RequiresPermissions.class); String[] methodPerms = methodAnnotation.value(); Subject subject = getSubject(); <span style="color:#FF0000;"> <strong>if (methodPerms.length == 1)</strong> { RequiresPermissions classAnnotation = mi.getThis().getClass().getAnnotation(RequiresPermissions.class); if(null != classAnnotation) { String[] classPerms = classAnnotation.value(); subject.checkPermission(classPerms[0] + methodPerms[0]); } else { subject.checkPermission(methodPerms[0]); } return; }</span> if (Logical.AND.equals(methodAnnotation.logical())) { getSubject().checkPermissions(methodPerms); return; } if (Logical.OR.equals(methodAnnotation.logical())) { // Avoid processing exceptions unnecessarily - "delay" throwing the exception by calling hasRole first boolean hasAtLeastOnePermission = false; for (String permission : methodPerms) if (getSubject().isPermitted(permission)) hasAtLeastOnePermission = true; // Cause the exception if none of the role match, note that the exception message will be a bit misleading if (!hasAtLeastOnePermission) getSubject().checkPermission(methodPerms[0]); } }
红色部分即实现所属类注解value和方法注解value的拼接并验证,方法写完了,我们还得有地方调用,其实就是AuthorizingAnnotationMethodInterceptor的实现类去调用了assertAuthorized这个方法。
所以我们自己写一个并且继承他:
MyPermissionAnnotationMethodInterceptorpublic class MyPermissionAnnotationMethodInterceptor extends AuthorizingAnnotationMethodInterceptor { public MyPermissionAnnotationMethodInterceptor() { super(new PermissionAnnotationHandler()); } <span style="color:#FF0000;"> public MyPermissionAnnotationMethodInterceptor(AnnotationResolver resolver) { super(new MyPermissionAnnotationHandler(), resolver); }</span> @Override public void assertAuthorized(MethodInvocation mi) throws AuthorizationException { try { <span style="color:#FF0000;"> AuthorizingAnnotationHandler handler = (AuthorizingAnnotationHandler) getHandler(); if(handler instanceof MyPermissionAnnotationHandler) { ((MyPermissionAnnotationHandler) handler).assertAuthorized(mi); } else { handler.assertAuthorized(getAnnotation(mi)); }</span> } catch(AuthorizationException ae) { // Annotation handler doesn't know why it was called, so add the information here if possible. // Don't wrap the exception here since we don't want to mask the specific exception, such as // UnauthenticatedException etc. if (ae.getCause() == null) ae.initCause(new AuthorizationException("Not authorized to invoke method: " + mi.getMethod())); throw ae; } } }
红色部分的构造器,设置我们自己的Handler,下面的则是因为我们自己实现的Handler有一个在父类没有定义的函数,所以需要判断并强转才能执行我们需要的方法。
最后,在设置拦截器栈的地方(AopAllianceAnnotationsAuthorizingMethodInterceptor),我们肯定要写成自己的:
MyAoppublic class MyAop extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor { public MyAop() { List<AuthorizingAnnotationMethodInterceptor> interceptors = new ArrayList<AuthorizingAnnotationMethodInterceptor>(5); //use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the //raw JDK resolution process. AnnotationResolver resolver = new SpringAnnotationResolver(); //we can re-use the same resolver instance - it does not retain state: interceptors.add(new RoleAnnotationMethodInterceptor(resolver)); <span style="color:#FF0000;">interceptors.add(new MyPermissionAnnotationMethodInterceptor(resolver));</span> interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver)); interceptors.add(new UserAnnotationMethodInterceptor(resolver)); interceptors.add(new GuestAnnotationMethodInterceptor(resolver)); setMethodInterceptors(interceptors); }
最后的最后,spring的配置文件中的定义要换成自己的入口
public class MyAuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor { private static final Logger log = LoggerFactory.getLogger(MyAuthorizationAttributeSourceAdvisor.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; /** * Create a new AuthorizationAttributeSourceAdvisor. */ <span style="color:#FF0000;"> public MyAuthorizationAttributeSourceAdvisor() { setAdvice(new MyAop()); }</span>
<bean class="com.component.shiro.MyAuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
这样就大功搞成,直接把注解写在类上,多方便~~
回顾一下:
1.修改shiro入口,将原本spring提供的org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor换成自己的,修改其构造函数中new的对象,new成自己的aop实现
2.将spring提供的AopAllianceAnnotationsAuthorizingMethodInterceptor重写一个自己的,基本就是复制他的源码,然后修改权限验证拦截器栈的设置,想增加也可在这里增加,我现在只需要修改权限验证的拦截器。
3.实现自己的权限验证拦截器,因为我们需要类上的注解,所以要执行不同的方法。
4.实现自己的权限处理器,因为我们需要类上的注解,在这里我们需要接收到执行的方法,用该方法获取类上的注解,和该方法的注解,最后拼接出最终的权限修饰符去验证,到此就OKshiro的流程还算简单,改起来也算容易,我这里是有一个小问题的,那就是拼接类注解是在方法注解的value只有一个的时候,如果有多个就不去拼类上的注解了,我想方法上的权限注解value也应该只有一个吧,毕竟直接针对某个权限,而不是角色了。
转载请注明原创地址