shiro直接对类进行注解,类似于@Controller的形式

  • Post author:
  • Post category:其他


因为我司项目重构,我负责编写了一些模块,而每个模块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在启动的时候初始化的

public 
    public 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去判断是否有权限了,而我们想要组合该方法所属类上的注解,那我们肯定得改这个方法
,问题是看这个方法的签名,直接接收了一个注解,不给我们获取类注解的机会,那肯定又得重新写一个自己的类。于是有了如下:
MyPermissionAnnotationHandler

    public 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这个方法。
所以我们自己写一个并且继承他:
MyPermissionAnnotationMethodInterceptor

public 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),我们肯定要写成自己的:
MyAop

public 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.实现自己的权限处理器,因为我们需要类上的注解,在这里我们需要接收到执行的方法,用该方法获取类上的注解,和该方法的注解,最后拼接出最终的权限修饰符去验证,到此就OK

shiro的流程还算简单,改起来也算容易,我这里是有一个小问题的,那就是拼接类注解是在方法注解的value只有一个的时候,如果有多个就不去拼类上的注解了,我想方法上的权限注解value也应该只有一个吧,毕竟直接针对某个权限,而不是角色了。

转载请注明原创地址





版权声明:本文为tragedyxd原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。