系统功能
- 根据用户权限获取菜单集合,返回前端显示。当前只有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()验证用户提交的表单信息是否与数据库匹配。
- 调用链从高级到低级:
- PathMatchingFilter.preHandle()
- AccessControlFilter.onPreHandle()
- FormAuthenticationFilter.onAccessDenied()
- DelegatingSubject.login()
- ModularRealmAuthenticator.doAuthenticate()
- 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