系统功能

  • 根据用户权限获取菜单集合,返回前端显示。当前只有admin/123456。
  • 本系统实体包括用户、公司、资源和角色,在界面上可以对它们进行增删查改等。
  • 比如,用户可以修改用户所属公司、拥有角色和资源、修改密码;公司可以移动子公司到其他公司旗下;资源可以修改资源所需权限、在父菜单下增删子菜单,角色可以修改用户可访问的资源。
  • 下图为资源管理
  • 在组织机构管理栏点击“添加子节点”

数据库设计

  • 数据库:shiro2。所在会话:mysql。
  • 用户表:organization_id:所属公司id,roles_ids:拥有角色id,用逗号连接成字符串。
  • 角色表:role:英文名称,用于后台交互;description:中文名称,用于前端显示;resource_ids:能访问的资源id,用逗号连接成字符串。
  • 资源表:type:资源类型,菜单和按钮,按钮实际为菜单的子菜单,为最低一级,当然菜单与菜单之间也有父子关系;parent_id:父菜单的id号;parent_ids:祖宗菜单链,从最顶级菜单的id开始排到父菜单,中间用/分隔,最后也用/结束;url:该资源对应的url;permission:访问该资源需要的权限;avaiable:是否可用。
  • 公司:parent_id:父公司id;parent_ids:祖宗公司id,从顶级公司排到父公司,中间用/分隔,最后也用/结束;avaliable:是否经营中。

配置文件

  • 注意:配置文件的class属性不能断开,否则视为类路径出错
  • resources.properties: 常量配置,包括DataSource的jdbc属性、数据连接池属性和shiro的密码加密算法,以备spring-config.xml引入。
#这里只列出加密算法属性
#shiro
password.algorithmName=md5
password.hashIterations=2
  • spring-config-cache.xml:定义cache底层为ehcache,以备spring-config.xml注入。

    <bean id="springCacheManager"     
      class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManager" ref="ehcacheManager"/>
    </bean>
    
    <!--ehcache-->
    <bean id="ehcacheManager"
          class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation" value="classpath:ehcache.xml"/>
    </bean>
    
  • ehcache.xml:与以前配置相同,不再赘述。

  • spring-mvc-shiro.xml: 定义aop切面,开启@RequriresPermissions注解进行权限控制。

    <aop:config proxy-target-class="true"></aop:config>
    <bean 
        class="org.apache.shiro.spring.security.interceptor
                .AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>
    
  • spring-config-shiro.xml: shiro配置,包括缓存、会话、realm、ShiroFilter、SecurityManager,以下只列出重要的。

<!-- Realm实现 -->
<bean id="userRealm" class="com.haien.chapter16.realm.UserRealm">
    <property name="credentialsMatcher" ref="credentialsMatcher"/>
    <property name="cachingEnabled" value="false"/>
    <!--<property name="authenticationCachingEnabled" value="true"/>-->
    <!--<property name="authenticationCacheName" value="authenticationCache"/>-->
    <!--<property name="authorizationCachingEnabled" value="true"/>-->
    <!--<property name="authorizationCacheName" value="authorizationCache"/>-->
</bean>

<bean id="sysUserFilter"
  class="com.github.zhangkaitao.shiro.chapter16.web.shiro.filter.SysUserFilter"/>

<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login"/>
    <property name="filters">
        <util:map>
            <entry key="authc" value-ref="formAuthenticationFilter"/>
            <entry key="sysUser" value-ref="sysUserFilter"/>
        </util:map>
    </property>
    <property name="filterChainDefinitions">
        <value>
            /login = authc
            /logout = logout
            /authenticated = authc
            /** = user,sysUser
        </value>
    </property>
</bean>
  • UserRealm禁用了shiro自己的缓存,而启用自己的缓存,否则需要在修改用户信息时频繁清理缓存。
  • SysUserFilter:根据当前用户身份获取User信息放入request,便于后续获取。

  • spring-config.xml: 扫描要注册的bean、注入数据源、配置事务管理器、引入其他配置文件,方便在web.xml只定位这一个配置文件即可发现其他配置文件。

    <context:property-placeholder location="classpath:resources.properties"/>
    
    <!-- 扫描注解Bean -->
    <context:component-scan base-package="com.haien.chapter16">
        <!--controller包的扫描交给MVC层的xml-->
        <context:exclude-filter type="annotation"
            expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    
    <!-- 开启AOP监听 只对当前配置文件有效  expose-proxy="true"-->
    <aop:aspectj-autoproxy/>
    
    <!-- 数据源 -->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
          init-method="init" destroy-method="close">
        <property name="url" value="${connection.url}"/>
        <property name="username" value="${connection.username}"/>
        <property name="password" value="${connection.password}"/>
    
        <!-- 配置初始化大小、最小、最大 -->
        <property name="initialSize" value="${druid.initialSize}"/>
        <property name="minIdle" value="${druid.minIdle}"/>
        <property name="maxActive" value="${druid.maxActive}"/>
    
        <!-- 配置获取连接等待超时的时间 -->
        <property name="maxWait" value="${druid.maxWait}"/>
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,
        单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis"
                  value="${druid.timeBetweenEvictionRunsMillis}" />
    
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" 
            value="${druid.minEvictableIdleTimeMillis}" />
    
        <property name="validationQuery" 
            value="${druid.validationQuery}" />
        <property name="testWhileIdle" value="${druid.testWhileIdle}" />
        <property name="testOnBorrow" value="${druid.testOnBorrow}" />
        <property name="testOnReturn" value="${druid.testOnReturn}" />
    
        <!-- 打开PSCache,并且指定每个连接上PSCache的大小
        如果用Oracle,则把poolPreparedStatements配置为true,
        mysql可以配置为false。-->
        <property name="poolPreparedStatements" 
            value="${druid.poolPreparedStatements}" />
        <property name="maxPoolPreparedStatementPerConnectionSize"
          value="${druid.maxPoolPreparedStatementPerConnectionSize}" />
    
        <!-- 配置监控统计拦截的filters -->
        <property name="filters" value="${druid.filters}" />
    </bean>
    
    <bean id="dataSourceProxy"
          class="org.springframework.jdbc.datasource
            .TransactionAwareDataSourceProxy">
        <property name="targetDataSource" ref="dataSource"/>
    </bean>
    
    <bean id="jdbcTemplate" 
          class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg ref="dataSourceProxy"/>
    </bean>
    
    <!--事务管理器配置-->
    <bean id="transactionManager"
          class="org.springframework.jdbc.datasource.
            DataSourceTransactionManager">
        <property name="dataSource" ref="dataSourceProxy"/>
    </bean>
    
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>
    
    <!--expose-proxy="true"-->
    <aop:config proxy-target-class="true">
        <!-- 只对业务逻辑层实施事务 -->
        <!--匹配规则详见笔记:Spring基础概念-->
        <aop:pointcut id="txPointcut"
            expression="execution(* com.haien
                .chapter16..service..*+.*(..))"/>
        <aop:advisor id="txAdvisor" advice-ref="txAdvice" 
            pointcut-ref="txPointcut"/>
    </aop:config>
    
    <bean class="com.haien.chapter16.spring.SpringUtils"/>
    
    <import resource="classpath:spring-config-cache.xml"/>
    <import resource="classpath:spring-config-shiro.xml"/>
    
  • controller层无权限会抛出UnauthorizationException,被全局异常处理器截获并返回unauthorized.jsp。

  • spring-mvc.xml:扫描controller类、引入spring-mvc-shiro.xml。

    <!--引入常量配置文件-->
    <context:property-placeholder 
        location="classpath:resources.properties"/>
    
    <!-- 开启controller注解支持 -->
    <context:component-scan
            base-package="com.haien.chapter16.web.controller"
            use-default-filters="false">
        <context:include-filter type="annotation"
            expression="org.springframework.stereotype.Controller"/>
        <!--扫描全局异常处理类,否则改类不起作用-->
        <context:include-filter type="annotation"
            expression="org.springframework.web.bind.annotation.
                ControllerAdvice"/>
    </context:component-scan>
    
    <!--注册@CurrentUser参数解析器,用在IndexController中,从request中
    获取shiro sysUser拦截器放入的当前登录User对象-->
    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean
              class="com.haien.chapter16.web.bind
                        .method.CurrentUserMethodArgumentResolver"/>
        </mvc:argument-resolvers>
    </mvc:annotation-driven>
    
    <!-- 当在web.xml中DispatcherServlet使用 <url-pattern>/</url-pattern> 
    映射时,能映射静态资源 -->
    <mvc:default-servlet-handler/>
    
    <!-- 静态资源映射 -->
    <mvc:resources mapping="/static/**" location="/WEB-INF/static/"/>
    
    <!-- 默认的视图解析器 在上边的解析错误时使用 (默认使用html)- -->
    <bean id="defaultViewResolver"
          class="org.springframework.web.servlet.view.
            InternalResourceViewResolver"
          p:order="1">
        <property name="viewClass" 
            value="org.springframework.web.servlet.view.JstlView"/>
        <property name="contentType" value="text/html"/>
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    
    <!-- 控制器异常处理 -->
    <bean id="exceptionHandlerExceptionResolver"
          class="org.springframework.web.servlet.mvc.method.annotation
                    .ExceptionHandlerExceptionResolver">
    </bean>
    
    <bean 
        class="com.haien.chapter16.web.exception.DefaultExceptionHandler"/>
    
    <import resource="spring-mvc-shiro.xml"/>
    
  • mvc:default-servlet-handler/:由于web.xml中DispatcherServlet使用/拦截all请求,会将静态资源的请求也拦截,导致找不到对应的controller处理,加上此标签会在SpringMvc上下文中定义一个org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler,检查进入DispatcherServlet的url,发现是对静态资源的请求则将该请求转由web服务器默认的Servlet处理,不是则由DispatcherServlet继续处理。
  • 一般web应用服务器默认的Servlet名为“default”,DefaultServletHttpRequestHandler可以找到它。但如果你所有的web服务器的默认Servlet名不是“default”,则需要通过default-servlet-name属性显式指定:

    <mvc:default-servlet-handler 
        default-servlet-name="所使用的Web服务器默认使用的Servlet名称" />
    
  • <mvc:resources />:除此之外,还可以利用此标签明确匹配url和静态资源。

    <mvc:resources location="/,classpath:/META-INF/publicResources/" 
        mapping="/resources/**"/>
    
  • 以上,将Web根路径(webapp)”/“及类路径下/META-INF/publicResources/ 的目录映射为/resources路径。假设Web根路径下拥有images、js这两个资源目录,在images下面有bg.gif图片,在js下面有test.js文件,则可以通过 /resources/images/bg.gif 和 /resources/js/test.js 访问这二个静态资源。

  • 参考文章

  • web.xml:配置Spring监听器、shiro安全过滤器、Servlet编码过滤器、Dispatcherservlet分发器,以下只列出重要的。

<!--加上这个可以在注册bean时切换bean作用域scope-->
<listener>
    <listener-class>
        org.springframework.web.context.request.RequestContextListener
    </listener-class>
</listener>

<!-- 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>
  • REQUEST:指定被分发的请求种类,默认request,如果此次请求非request则不会走该过滤器,也就是不会被分发给controller处理。它必须写在filter-mapping的最后。取值:
    • REQUEST:只要发起的请求是一次http请求,如某个url发起了一次get、post请求,或者发起相当于两次请求的重定向,那么就会走该过滤器。
    • FORWARD:请求是转发才走过滤器。
    • INCLUDE:只要是通过<jsp:include page=”xxx.jsp” />嵌入进来的页面,每嵌入一个页面,都会走一次该过滤器。
    • ERROR:当触发了一次error时,就会走一次该过滤器。什么是触发error?比如我在web.xml中配置了,当后台返回400/404/500时,容器就会将请求转发到一下错误页面,这就触发了一次error。,走进了过滤器。虽然这是转发的过程,但是配置成FORWARD并不会走过滤器。
  • 参考文章
<error-page>
    <error-code>400</error-code>
    <location>/filter/error.jsp</location>
</error-page>

<error-page>
    <error-code>404</error-code>
    <location>/filter/error.jsp</location>
</error-page>

<error-page>
    <error-code>500</error-code>
    <location>/filter/error.jsp</location>
</error-page>

用户验证链

  • 使用基于表单的拦截器实现用户验证,自定义了UserRealm,重写其中doGetAuthenticationInfo()验证用户提交的表单信息是否与数据库匹配。
  • 调用链从高级到低级:
  1. PathMatchingFilter.preHandle()
  2. AccessControlFilter.onPreHandle()
  3. FormAuthenticationFilter.onAccessDenied()
  4. DelegatingSubject.login()
  5. ModularRealmAuthenticator.doAuthenticate()
  6. UserRealm.doGetAuthenticationInfo()
  • 也就是说FormAuthenticationFilter是会调用login()的,而login()又会调用Realm,所以FormAuthenticationFilter的登录验证是通过Realm实现的。

自定义注解+注解解析器+使用实例

  • @CurrentUser:自定义注解
/**
 * @Author haien
 * @Description 绑定当前登录的用户
 * @Date 2019/3/14
 **/
//测试此注解作用
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {
    //Constants类CURRENT_USER常量="user",
    //因为当前登录用户在request中存为user属性,
    //所以将此注解用于方法参数,再定义一个解析器解析此注解:
    //获取此注解值并从request中查找属性赋给方法参数,
    //即实现了绑定当前用户到方法属性的功能。
    String value() default Constants.CURRENT_USER; 
}
  • CurrentUserMethodArgumentResolver:自定义注解解析器
public class CurrentUserMethodArgumentResolver 
        implements HandlerMethodArgumentResolver {

    public CurrentUserMethodArgumentResolver() {
    }

    /**
     * @Author haien
     * @Description 判断参数是否受支持,依据是它是否拥有CurrentUser注解
     * @Date 2019/3/14
     * @Param [parameter]
     * @return boolean
     **/
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        //判断参数是否被注解了
        if(parameter.hasParameterAnnotation(CurrentUser.class)){
            return true;
        }
        return false;
    }

    /**
     * @Author haien
     * @Description 对被注解参数的解析是:
                    获取当前登录对象并返回给此参数
     * @Date 2019/3/14
     * @Param [parameter, mavContainer, webRequest, binderFactory]
     * @return java.lang.Object
     **/
    @Override
    public Object resolveArgument(MethodParameter parameter,
            ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest,
            WebDataBinderFactory binderFactory) throws Exception {
        CurrentUser currentUserAnnotation=
            parameter.getParameterAnnotation(CurrentUser.class);
        //currentUserAnnotation.value()返回“user”,
        //从request获取“user”属性
        return webRequest.getAttribute(currentUserAnnotation.value(),
                NativeWebRequest.SCOPE_REQUEST);
    }
}
  • IndexController:使用注解
@RequestMapping("/")
public String index(@CurrentUser User loginUser, Model model) { 
//@CurrentUser获取当前登录对象并赋给loginUser

    //根据用户名查询权限字符串
    Set<String> permissions = 
        userService.findPermissions(loginUser.getUsername());
    //查询跟这些权限有关的菜单
    List<Resource> menus = resourceService.findMenus(permissions);
    model.addAttribute("menus", menus);
    return "index";
}

使用Spring的Cache代替Shiro的Cache

  • 因为shiro自己的cache每次都要手动清除缓存,才能防止修改后有获取到未更新的缓存,所以使用spring提供的cache,并连同shiro cache中一些较好的方法封装起来。
  • 以前的shiro cache:

    <!-- 缓存管理器 使用Ehcache实现 -->
    <bean id="cacheManager" 
          class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" 
            value="classpath:ehcache.xml"/>
    </bean>
    
  • 现在:

    //spring-config-cache.xml
    <!--底层使用ehcache-->
    <bean id="ehcacheManager"
          class="org.springframework.cache.ehcache.
            EhCacheManagerFactoryBean">
        <property name="configLocation" value="classpath:ehcache.xml"/>
    </bean>
    
    <!--将=引入以上CacheManager-->
    <bean id="springCacheManager" 
            class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManager" ref="ehcacheManager"/>
    </bean>
    
    //spring-config-shiro.xml
    <!-- 缓存管理器 -->
    <bean id="cacheManager" 
        class="com.haien.spring.SpringCacheManagerWrapper">
        <!--定义在spring-config-cache.xml中-->
        <property name="cacheManager" ref="springCacheManager"/>
    </bean>
    
    /**
     * @Author haien
     * @Description 包装Spring Cache,因为spring的cache没有shiro的cache那些功能,
     *              但又有其优点,所以给它封装一些shiro中比较好的方法进去。
     * @Date 2019/3/16
     **/
    public class SpringCacheManagerWrapper implements CacheManager {
        private org.springframework.cache.CacheManager cacheManager;
    
        //由xml文件注入一个Spring框架的cache对象
        public void setCacheManager(org.springframework.cache.CacheManager cacheManager){
            this.cacheManager=cacheManager;
        }
    
        /**
         * @Author haien
         * @Description 获取注入进来的cache对象并封装成SpringCacheWrapper对象,
         *              其中就提供了shiro中较好的方法。
         * @Date 2019/3/16
         * @Param [name]
         * @return org.apache.shiro.cache.Cache<K,V>
         **/
        @Override
        public <K, V> Cache<K, V> getCache(String name) throws CacheException {
            //获取上面setCacheManager()
            org.springframework.cache.Cache springCache=cacheManager.getCache(name);
            return new SpringCacheWrapper(springCache);
        }
    
        /**
         * @Author haien
         * @Description 内部类:继承shiro的Cache,重写了它的方法,
                        封装给当前类的springcache对象
         * @Date 2019/3/16
         **/
        static class SpringCacheWrapper implements Cache{ //继承shiro的cache
            private org.springframework.cache.Cache springCache;
    
            SpringCacheWrapper(org.springframework.cache.Cache springCache) {
                this.springCache = springCache;
            }
    
            @Override
            public Object get(Object key) throws CacheException {
                Object value=springCache.get(key);
                if(value instanceof SimpleValueWrapper) 
                    return ((SimpleValueWrapper)value).get();
                return value;
            }
    
            @Override
            public Object put(Object key, Object value) throws CacheException {
                springCache.put(key,value);
                return value;
            }
    
            @Override
            public Object remove(Object key) throws CacheException {
                springCache.evict(key);
                return null;
            }
    
            @Override
            public void clear() throws CacheException {
                springCache.clear();
            }
    
            @Override
            public int size() {
                if(springCache.getNativeCache() instanceof Ehcache){
                    Ehcache ehcache=(Ehcache)springCache.getNativeCache();
                    return ehcache.getSize();
                }
                throw new UnsupportedOperationException(
                        "invoke spring cache abstract size method not supported");
            }
    
            @Override
            public Set keys() {
                if(springCache.getNativeCache() instanceof Ehcache){
                    Ehcache ehcache=(Ehcache)springCache.getNativeCache();
                    return new HashSet(ehcache.getKeys());
                }
                throw new UnsupportedOperationException(
                        "invoke spring caceh abstract keys method not supported");
            }
    
            @Override
            public Collection values() {
                if(springCache.getNativeCache() instanceof Ehcache){
                    Ehcache ehcache=(Ehcache)springCache.getNativeCache();
                    List keys=ehcache.getKeys();
                    if(!CollectionUtils.isEmpty(keys)){
                        List values=new ArrayList(keys.size());
                        for(Object key:keys){
                            Object value=get(key);
                            if(value!=null)
                                values.add(value);
                        }
                        return Collections.unmodifiableList(values);
                    }else{
                        return Collections.emptyList();
                    }
                }
                throw new UnsupportedOperationException(
                        "invoke spring cache abstract values method not supported");
            }
        }
    }
    
  • 参考文章

  • 代码示例:ideaProjects/shiro-chapter16