Springboot与shiro整合遇到的坑

最近迷上了springboot,开始把公司我搭的框架从dubbo移植为springboot,xml全部转换为configuration类,遇到了很多问题,解决差不多了主要把与shiro的问题放出来与大家分享,因为网上这方面的信息太少了。

springboot中的动态代理设置

在spring的xml配置中我们知道,要设置cglib动态代理只需一句

<aop:aspectj-autoproxy proxy-target-class="true"/>

至于为什么要用强制使用cglib,因为很多时候我们并不会特意为某个bean去写一个接口,再去实现,这种情况下如果用默认的jdk动态代理,则一旦有多个aop代理,比如既有shiro注解代理,又有事务切片或注解代理,这种时候去注入这个bean便会报错:

springboot中的报错

Description:
The bean 'XXX' could not be injected as a 'com.XXX.XXX' because it is a JDK dynamic proxy that implements:
com.sun.proxy.$XX

Action:

Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxies by setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.

原因就是这个bean被多次代理的时候,jdk代理是基于接口的,所以最后这个bean的类型变成了代理接口proxy的类型。

而bean被spring aop重复代理其实也是因为错误的配置,比如shiro中:

<!-- 重复代理错误配置-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<bean id="serviceAdvisorAutoProxyCreator"
      class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
</bean> 
<bean id="serviceAuthorizationAttributeSourceAdvisor"
      class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
     <property name="securityManager" ref="securityManager"/>
</bean>

(如上,我们已经开启了autoproxy,然后又为shiro的注解开启了一个切面代理)

正确配置应该删除“serviceAdvisorAutoProxyCreator”这个bean。

在springboot中同理,springboot的默认配置中已经默认开启了自动代理

{
      "name": "spring.aop.auto",
      "type": "java.lang.Boolean",
      "description": "Add @EnableAspectJAutoProxy.",
      "defaultValue": true
    }

所以在configuration中只需定义好切面bean或开启相应的注解支持bean即可,springboot的启动类上也不需要@EnableAspectJAutoProxy注解,而强制cglib动态代理只需在application配置文件中加入

spring.aop.proxy-target-class=true


springboot中的shiroFilter配置

实际集成shiro时,我们大部分情况下都需要自定义一个或者多个filter,然后在ShiroFilterFactoryBean中配置这些filter的匹配规则,最后再把这个ShiroFilterFactoryBean自动创建的shiroFilterBean加入到mvc的web filter链中。

在spring mvc项目中,我们通过web.xml来加入shiroFilter:

   <!-- shiro 安全过滤器 -->
    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <async-supported>true</async-supported>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
    </filter-mapping>

并在spring.xml中配置ShiroFilterFactoryBean来管理自定义filter:

   <bean id="MyAuthFilter" class="com.persistence.shiro.MyAuthFilter"/>

    <!-- Shiro的Web过滤器 -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="filters">
            <util:map>
                <entry key="myAuthFilter" value-ref="MyAuthFilter"/>
            </util:map>
        </property>
        <property name="filterChainDefinitions">
            <value>
                /anon/**=myAuthFilter
                /auth/**=myAuthFilter
            </value>
        </property>
    </bean>

但是在springboot中,我们按照上面的xml转换为configuration类时会出现问题,比如如下配置:

   @Bean
    @Order(1)
    public MyAuthFilter myAuthFilter() {
        return new MyAuthFilter();
    }

    @Bean
    @Order(2)
    public ShiroFilterFactoryBean shirFilter() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("myAuthFilter", myAuthFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        Map<String, String> mapping = new LinkedHashMap<>();
        mapping.put("/anon/**", "myAuthFilter");
        mapping.put("/auth/**", "myAuthFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(mapping);
        return shiroFilterFactoryBean;
    }

运行项目,发现登录验证能够正常进入自定义filter,但是运行到subject.hasRoles或者有@RequiresPermissions注解时,会抛出异常:

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.
	at org.apache.shiro.subject.support.DelegatingSubject.assertAuthzCheckPossible(DelegatingSubject.java:203) [shiro-core-1.4.0.jar:1.4.0]
	at org.apache.shiro.subject.support.DelegatingSubject.checkPermission(DelegatingSubject.java:208) [shiro-core-1.4.0.jar:1.4.0]
	at org.apache.shiro.authz.aop.PermissionAnnotationHandler.assertAuthorized(PermissionAnnotationHandler.java:74) ~[shiro-core-1.4.0.jar:1.4.0]
	at org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor.assertAuthorized(AuthorizingAnnotationMethodInterceptor.java:84) ~[shiro-core-1.4.0.jar:1.4.0]
	at org.apache.shiro.authz.aop.AnnotationsAuthorizingMethodInterceptor.assertAuthorized(AnnotationsAuthorizingMethodInterceptor.java:100) ~[shiro-core-1.4.0.jar:1.4.0]
	at org.apache.shiro.authz.aop.AuthorizingMethodInterceptor.invoke(AuthorizingMethodInterceptor.java:38) ~[shiro-core-1.4.0.jar:1.4.0]
	...
Caused by: org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method: public java.util.Map dna.base.service.UserInfoService.queryUserInfoAndRoles(java.lang.Integer)
	at org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor.assertAuthorized(AuthorizingAnnotationMethodInterceptor.java:90) ~[shiro-core-1.4.0.jar:1.4.0]
	... 84 common frames omitted

(当前的subject对象是未验证的,无法授权)

打断点在自定义filter中可以看到调用subject.login()可以进入到配置的realm中成功进行登录验证的,并且此时subject.isAuthenticated值为true,但在filter链执行完毕后,subject对象又变成未验证的。通过在org.apache.catalina.core.ApplicationFilterChain(spring的filter执行类)中的internalDoFilter方法打断点:

ApplicationFilterConfig filterConfig = this.filters[this.pos++];

我们可以看到,在执行完自定义filter后又执行了shiroFilter,此时会进入shiro的AbstractShiroFilter中将当前的subject删除又新创建了一个subject对象并不会进行验证操作:

protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException {
        Throwable t = null;

        try {
            final ServletRequest request = this.prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = this.prepareServletResponse(request, servletResponse, chain);
            Subject subject = this.createSubject(request, response);
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    AbstractShiroFilter.this.updateSessionLastAccessTime(request, response);
                    AbstractShiroFilter.this.executeChain(request, response, chain);
                    return null;
                }
            });
        } catch (ExecutionException var8) {
            t = var8.getCause();
        } catch (Throwable var9) {
            t = var9;
        }

因此之后获取到subject已经不是我们自己验证的subject。

原因:springboot会将所有实现Filter接口的bean自动加入到web filter链中执行,即自定义filter与shiroFilter平级,而不是原xml配置中的包含关系。启动日志中我们也可以看到:

[ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
[ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
[ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
[ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
[ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
[ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'statelessAuthFilter' to: [/*]
[ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'shirFilter' to: [/*]

解决方法

1.改变filter链的执行顺序,先执行shiroFilter再执行自定义filter:

注意到configuration类中的@order注解,将两个filter的优先级调换,或者不用@order注解,将自定义filter的bean放在shiroFilter bean下面,即保证最后执行的是自定义filter。

2.方法1只是简单保证了自定义filter的执行,但是我们原先在shiroFilter中配置的mapping会失效,即无法按url匹配不同的自定义filter,因此应该正确的建立filter的关系:

   @Bean
    public ShiroFilterFactoryBean shirFilter() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setLoginUrl("/login");
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("myAuthFilter", new MyAuthFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        Map<String, String> mapping = new LinkedHashMap<>();
        mapping.put("/anon/**", "myAuthFilter");
        mapping.put("/auth/**", "myAuthFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(mapping);
        return shiroFilterFactoryBean;
    }

(即不将自定义filter注册到spring bean,而是交由shiroFilter管理)


编辑于 2018-04-11