SpringMVC+shiro+ehcache整合

    最近一直想总结下最近学习shiro权限管理的收获,惰性使然,没有去整理。春节放假前一天没什么工作,闲来总结下。

    shiro是一个用来管理权限的框架。经过两个项目的整合,个人认为,整合shiro的重点在于配置文件的配置还有Realm自定义拦截器的编写。当然,想要良好的把shiro用于开发环境,前提需要有一套合适的表结构,最基础的权限模块应包含:用户表、角色表、权限表。再复杂一些的,可以再加入部门表、菜单表、数据权限表等。至于表结构的设计,在此不做详述,总之要根据自己的业务需求去设计。下面开始按流程,一步步整合shiro。

    第一部分:SpringMVC整合shiro的步骤:

    1. 导入jar包(Maven)

<!-- Spring 整合Shiro需要的依赖 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.2.5</version>
</dependency>
<!--导入eheache的缓存包 -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache-core</artifactId>
    <version>2.6.11</version>
</dependency>

2. 添加配置文件

    SpringMvc整合shiro过程中,配置文件是很烦人的一件事,尤其是在配置出错的时候,控制台会报一大长串的错误。这时候大家需要心平气和的来对待,因为生气也没有用,活不还是得自己干嘛....

    1. 首先,在resource文件夹下,创建spring-shiro.xml文件。创建完毕后,先来配置一下(具体配置内容在下面),让项目去加载这个配置文件,这里有两种配置方式。

 方式①

    找到项目的web.xml文件,在<context-param><context-param/>中,加入<param-value>值为 classpath:spring-shiro.xml.如图:

项目结构有可能不一样,web.xml:

此处加入shiro配置文件:

方式②

    在applicationContext.xml,引入shiro配置文件。

   

2. 加载shiro过滤器

    在web.xml中配置shiro过滤器加载

<!-- 配置Shiro过滤器,先让Shiro过滤系统接收到的请求 -->
<!-- 这里filter-name必须对应spring-shiro.xml中定义的<bean id="shiroFilter"/> -->
<!-- 使用[/*]匹配所有请求,保证所有的可控请求都经过Shiro的过滤 -->
<!-- 通常会将此filter-mapping放置到最前面(即其他filter-mapping前面),以保证它是过滤器链中第一个起作用的 -->
<filter>
  <filter-name>shiroFilter</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  <init-param>
    <param-name>targetFilterLifecycle</param-name>
    <param-value>true</param-value>  <!--该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理 -->
  </init-param>
</filter>
<filter-mapping>
  <filter-name>shiroFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

此时,spring-shiro.xml中我们还没有配置<bean id="shiroFilter"/>,下一步将配置spring-shiro.xml

3. 配置 spring-shiro.xml(这里整合了ehcache作为权限缓存)

在这里我直接贴上配置好的文件,至于bean的功能会有注释

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
                        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
    default-lazy-init="true">

    <description>Shiro Configuration</description>
    <import resource="spring-ehcache.xml"></import>

    <!-- 项目自定义的Realm,需自己编写该类 -->
    <bean id="jhShiroRealm" class="com.jiahe.common.shiro.ShiroRealm"/>

    <!-- 会话管理器 -->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!-- session的失效时长(1分钟/600000),单位毫秒 -->
        <property name="globalSessionTimeout" value="1800000"/>
        <!-- 删除失效的session -->
        <property name="deleteInvalidSessions" value="true"/>
        <property name="sessionDAO" ref="sessionDAO"/>
        <property name="sessionIdCookieEnabled" value="true"/>
        <property name="sessionIdCookie" ref="sessionIdCookie"/>
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <!--
           Shiro提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话;
           出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;
           但是如在web环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,
           Shiro提供了会话验证调度器SessionValidationScheduler来做这件事情。
        -->
        <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
        <property name="cacheManager" ref="shiroEhcacheManager"/>
    </bean>

    <!--安全管理器-->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="jhShiroRealm" />
        <property name="sessionManager" ref="sessionManager"/>
        <property name="cacheManager" ref="shiroEhcacheManager"/>
    </bean>

    <!-- Shiro Filter -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager" />
        <property name="loginUrl" value="/login.htm" /> <!--没有登录的用户跳转的界面-->
        <property name="successUrl" value="" />  <!--登录成功后跳转页面,若不配置,则可在逻辑中实现-->
        <property name="unauthorizedUrl" value="/403.htm" />  <!--没有权限跳转的页面,自己编写该界面-->
        <property name="filterChainDefinitions">  <!--全局过滤器 -->
            <value>
                <!-- anon代表不需要授权即可访问,对静态资源设置匿名访问 -->
                /css/** = anon
                /js/** = anon
                /static/** = anon
                /json/** = anon
                /images/** = anon
                /tools/** = anon
                /login.htm = anon
                /toLogin.htm = anon
                /swagger-ui.html = anon
                /webjars/** = anon
                /v2/** = anon
                /swagger-resources/** = anon
                /configuration/** = anon
                /kickout.htm = anon
                /verify.htm = anon
                <!-- 所有的请求(除去配置的静态资源请求或请求地址为anon的请求)都要通过登录验证,如果未登录则跳到unauthorizedUrl指定的url -->
                /** = authc
            </value>
        </property>
    </bean>

    <!-- 会话DAO,sessionManager里面的session需要保存在会话Dao里,没有会话Dao,session是瞬时的,没法从 sessionManager里面拿到session -->
    <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
        <!-- 用于生成会话ID,默认就是JavaUuidSessionIdGenerator,使用java.util.UUID生成 -->
        <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
        <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
    </bean>

    <!-- 会话ID生成器 -->
    <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"></bean>

    <!-- 会话Cookie模板,sessionManager创建会话Cookie的模板 -->
    <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
        <constructor-arg value="sid"/>
        <property name="httpOnly" value="true"/>
        <property name="maxAge" value="-1"/>
    </bean>

    <!-- 会话验证调度器 -->
    <bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler">
        <!-- 设置调度时间间隔,单位毫秒,默认就是1小时 -->
        <property name="interval" value="1800000"/>
        <!-- 设置会话验证调度器进行会话验证时的会话管理器 -->
        <property name="sessionManager" ref="sessionManager"/>
    </bean>

    <!-- 保证实现了Shiro内部生命周期函数的bean执行 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- 启用shrio授权注解拦截方式 -->
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager" />
    </bean>

    <!--登出系统 交给shiro处理-->
    <!--<bean id="logout" class="org.apache.shiro.web.filter.authc.LogoutFilter">
        <property name="redirectUrl" value="/login.htm" />
    </bean>-->

</beans>  

4. 添加spring-ehcache.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:cache="http://www.springframework.org/schema/cache"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-4.3.xsd
       ">


   <cache:annotation-driven cache-manager ="springCacheManager"  />


    <bean id="ehcacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation" value="classpath:ehcache_jh.xml"/>
    </bean>


    <bean id="springCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cacheManager-ref= "ehcacheManager"/>

    <!-- 用户授权信息Cache -->
    <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManager" ref="ehcacheManager"></property>
    </bean>

</beans>

配置完之后,需要在applicationContext.xml中加载缓存配置文件

5. 配置ehcache_jh.xml缓存

<?xml version="1.0" encoding="UTF-8"?>
<ehcache>

   <!--指定一个目录:当 EHCache 把数据写到硬盘上时, 将把数据写到这个目录下. <diskStore path="d:\\tempDirectory"/> -->
    <diskStore path="java.io.tmpdir/ehcache"/>

   <!--
     eternal:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。
     maxElementsInMemory:缓存中允许创建的最大对象数
     overflowToDisk:设置基于内存的缓存中的对象数目达到上限后,是否把溢出的对象写到基于硬盘的缓存中
     timeToIdleSeconds:设置对象空闲最长时间,以秒为单位, 超过这个时间,对象过期。当对象过期时,EHCache会把它从缓存中清除。如果此值为0,表示对象可以无限期地处于空闲状态。
     timeToLiveSeconds:设置对象生存最长时间,超过这个时间,对象过期。如果此值为0,表示对象可以无限期地存在于缓存中.该属性值必须大于或等于 timeToIdleSeconds 属性值
     maxElementsOnDisk:在磁盘上缓存的element的最大数目,默认值为0,表示不限制。
     diskPersistent: 设定在虚拟机重启时是否进行磁盘存储,默认为false
     diskExpiryThreadIntervalSeconds:对象检测线程运行时间间隔。标识对象状态的线程多长时间运行一次。
     maxEntriesLocalHeap=: 堆内存中最大缓存对象数,0没有限制(必须设置)
     memoryStoreEvictionPolicy:缓存满了之后的淘汰算法。
     1 FIFO,先进先出
     2 LFU,最少被使用,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
     3 LRU,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
     -->

   <defaultCache eternal="false"
              maxElementsInMemory="10000"
              overflowToDisk="false"
              timeToIdleSeconds="0"
              timeToLiveSeconds="0"
              memoryStoreEvictionPolicy="LFU" />
   <!--
          设定具体的命名缓存的数据过期策略。每个命名缓存代表一个缓存区域
          缓存区域(region):一个具有名称的缓存块,可以给每一个缓存块设置不同的缓存策略。
          如果没有设置任何的缓存区域,则所有被缓存的对象,都将使用默认的缓存策略。即:<defaultCache.../>
          Hibernate 在不同的缓存区域保存不同的类/集合。
           对于类而言,区域的名称是类名。如:com.atguigu.domain.Customer
           对于集合而言,区域的名称是类名加属性名。如com.atguigu.domain.Customer.orders
      -->
    <!-- shiro 会话缓存-->
   <cache name="shiro-activeSessionCache"
         eternal="false"
         maxElementsInMemory="10000"
         timeToIdleSeconds="1800000"
         timeToLiveSeconds="1800000"
         overflowToDisk="false"
         statistics="true"/>

   <!--<cache name="sysDataCache"
      maxElementsInMemory="10000"
      maxElementsOnDisk="1000" 
      eternal="false" 
      overflowToDisk="false"
      diskSpoolBufferSizeMB="20" 
      timeToIdleSeconds="3600" 
      timeToLiveSeconds="3600" 
      memoryStoreEvictionPolicy="LFU" />-->
 
   <!--<cache name="sysDictCache"
      maxElementsInMemory="10000"
      maxElementsOnDisk="1000" 
      eternal="true" 
      overflowToDisk="false"
      diskSpoolBufferSizeMB="20" 
      timeToIdleSeconds="3600" 
      timeToLiveSeconds="3600" 
      memoryStoreEvictionPolicy="LFU" />-->
   
   <!-- 默认缓存 -->
   <!--<cache name="DefaultRegion"
      maxElementsInMemory="100000"   
      eternal="true"
      maxElementsOnDisk="100000"
      overflowToDisk="true"
      memoryStoreEvictionPolicy="LRU" /> -->

</ehcache>

6. 在spring-mvc.xml 配置文件中开启shiro注解(不配置的话,无法使用shiro权限注解)

之前在spring-shiro.xml中配置后,不起作用。具体原因没找到

<!-- AOP 式方法级权限检查 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)-->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

<!-- 切面自动代理:相当于以前的AOP标签配置 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
      depends-on="lifecycleBeanPostProcessor">
    <!-- 设置aop的代理使用CGLIB代理 -->
    <property name="proxyTargetClass" value="true"/>
</bean>

至此,基本的配置就完毕了,剩下的就是对自定义Realm拦截器的编写。

3.自定义Realm

自定义拦截器分为两个部分,一个是登录认证doGetAuthenticationInfo方法,一个是授权方法doGetAuthorizationInfo。

首先,新建ShiroReaml类,并继承AuthorizingRealm抽象类,并重写doGetAuthenticationInfo和doGetAuthorizationInfo方法。

注意:有很多朋友配置好shiro之后,登录的时候是会走登录验证的方法,但是不走权限认证的方法。在此说明一下,当用户登录的时候,系统会根据你的配置文件,在用户登录时自动调用shiro登录认证逻辑,但是不会调用授权方法。若想调用授权方法,需要开发人员在接口上使用shiro注解,当程序调用了该接口之后,系统才会走授权认证的逻辑。

这里直接贴上我的逻辑,有需要实体类的朋友可以@我

package com.jiahe.common.shiro;

import com.jiahe.system.entity.User;
import com.jiahe.system.model.MenuModal;
import com.jiahe.system.service.*;
import com.jiahe.system.service.impl.UserRoleMemberServiceImpl;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.*;

/**
 * @auther chenqian
 * @date 2018/11/28 13:17
 * shiro 拦截器 实现登录验证和角色赋权
 */
public class ShiroRealm extends AuthorizingRealm {

    private Logger log = LogManager.getLogger();

    @Autowired
    private UserServiceI userServiceI;
    @Autowired
    private UserRoleMemberServiceImpl memberServiceI;
    @Autowired
    private FunctionNodeServiceI nodeServiceI;
    @Autowired
    private PermisstionRangeServiceI rangeServiceI;
    @Autowired
    private FeaturePointServiceI pointServiceI;

    /**
     * 1.登录验证
     * @param authcToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
            throws AuthenticationException {
        log.info("1.1 ===> 用户登录验证");
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        log.info("1.2 ===> 获取当前用户登录的 Token :" + token);
        String accountName = token.getUsername();
        // 让shiro框架去验证账号密码
        if(!StringUtils.isEmpty(accountName)){
            User user = userServiceI.findUserByUserName(accountName);
            if(user != null) {
                log.info("1.3 ===> 登录验证成功");
                return new SimpleAuthenticationInfo(user.getCode(), user.getPassword(), getName());
            }
        }
        log.info("1.4 ===> 验证失败");
        return null;
    }

    /**
     *  2.授权
     *  从输入参数principalCollection得到身份信息,
     *  根据身份信息到数据库查找权限信息,
     *  将权限信息添加给授权信息对象
     * @param principals 身份集合
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("2.1 ====> 进入角色授权");
        // 待授权用户
        String currentUser = (String)super.getAvailablePrincipal(principals);
        // 从用户表中查找该用户信息
        User user = userServiceI.findUserByUserName(currentUser);
        // 权限信息对象,用来存放用户的所有 角色(role)权限(permission)
        SimpleAuthorizationInfo simpleAuthorInfo = new SimpleAuthorizationInfo();

        // 查询用户角色信息
        List<Map> roleInfo = memberServiceI.findRoleIdsByUserId(user.getObjectId());
        // 角色名称
        List<String> roleNames = new ArrayList<String>();
        // 角色id
        List<String> rolesIds = new ArrayList<String>();

        if (roleInfo.size() > 0){
            for (Map info : roleInfo){
                roleNames.add((String) info.get("roleName"));
                rolesIds.add((String) info.get("roleId"));
            }
            log.info("2.2 ====> 用户角色授权:" + roleNames);
            simpleAuthorInfo.addRoles(roleNames);

            log.info("2.3 ====> 用户权限授权:start" );
            simpleAuthorInfo.addStringPermissions(this.getUserPermissions(rolesIds,user.getObjectId()));
        }else {

            log.info("2.4 ====> 授权失败,用户不具备角色权限!" );
        }

        return simpleAuthorInfo;
    }

    /**
     * 授权过程
     * @param rolesIds 角色ids
     * @return
     */
    private Set<String> getUserPermissions(List<String> rolesIds,String userkey){
        // 权限
        Set<String> permissions = new HashSet<>();
        // 菜单信息
        List<MenuModal> nodesInfo = nodeServiceI.findRolesNodeByRoleIds(rolesIds,userkey);
        // 菜单id
        List<String> nodeIds = new ArrayList<String>();

        // 角色菜单
        for (MenuModal node : nodesInfo){
            nodeIds.add(node.getObjectId());
            permissions.add("function:node:" + node.getCode());
        }
        log.info("2.6 ====> 角色菜单权限授权完成");

        if (nodeIds.size() > 0){
            // 菜单的操作权限
            List<Map> points = pointServiceI.getPointsByNodeIds(nodeIds);
            for (Map point : points){
                permissions.add((String) point.get("nodeCode") + ":menuPoint:" + (String) point.get("pointCode"));
            }
            // 用户的操作权限
            List<Map> userPoints = pointServiceI.getUserPointsByNodeIdsAndRoleIds(nodeIds, rolesIds);
            for (Map point : userPoints){
                permissions.add((String) point.get("nodeCode") + ":userPoint:" + (String) point.get("pointCode"));
            }
            log.info("2.7 ====> 用户权限授权结束:end" );
        }

        log.info("2.8 ====> 授权信息集合: " + permissions);
        return permissions;
    }

    //清除缓存
    public void clearCached() {
        PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
        super.clearCache(principals);
    }

}

4.登录逻辑

/**
 * 用户登录
 * @param request
 * @param userName
 * @param usrPwd
 * @param capt
 */
@RequestMapping(value = "toLogin", method = RequestMethod.POST)
public void validateLogin(HttpServletRequest request, @RequestParam String userName,
                          @RequestParam String usrPwd, @RequestParam String capt){
    Json json = new Json(true, "登录成功!");
    String sessionCapt = (String) request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
    // 当前Subject
    Subject currentUser = SecurityUtils.getSubject();
    String md5Pwd = new Md5Hash(usrPwd).toString();
    UsernamePasswordToken token = new UsernamePasswordToken(userName, md5Pwd);
    // 传递token给shiro 的realm
    try{
        if (!sessionCapt.equals(capt)){
            json.setSuccess(false);
            json.setMsg("验证码错误!");
            writeJson(json);
            return;
        }
        currentUser.login(token);
    }catch (UnknownAccountException uae) {
        json.setSuccess(false);
        json.setMsg("用户名不存在!");
    }catch (IncorrectCredentialsException ice) {
        json.setSuccess(false);
        json.setMsg("密码不正确!");
    }catch (Exception ex) {
        json.setSuccess(false);
        json.setMsg("系统错误!");
    }
    writeJson(json);
}

5. shiro注解用法

    在controller中的方法上使用注解,就像之前说的那样,当程序调用该注解标注的接口时,shiro才会去走授权认证的逻辑。如果此时缓存中有该用户的权限,则直接从ehcache中取用户的权限进行判断。

/**
 * 列表页面
 * @return
 */
@RequiresPermissions("function:node:YHGL") // shiro权限注解
@RequestMapping(value = "list" ,method = RequestMethod.GET)
public ModelAndView orglist() {
    mv.setViewName(url+"orglist");
    return mv;
}

    shiro本身有一套整合了jsp的前端注解,有需要做细粒度控制的朋友可以了解下(对按钮权限的控制),同时支持自定义前端注解,非常长方便。马上要下班了,就写到这里。抽时间补齐SpringBoot整合shiro的案例。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值