SpringBoot+Shiro/SpringMVC+shiro整合

    最近一直想總結下最近學習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>

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="kickoutSessionControlFilter"  class="com.jiahe.common.shiro.KickoutSessionControlFilter">
        <property name="sessionManager" ref="sessionManager"/>
        <property name="cacheManager" ref="shiroEhcacheManager"/>
        <!-- 是否踢出後來登錄的,默認是false;即後者登錄的用戶踢出前者登錄的用戶 -->
        <property name="kickoutAfter" value="false"/>
        <!-- 同一個用戶最大的會話數,默認1;比如2的意思是同一個用戶允許最多同時兩個人登錄 -->
        <property name="maxSession" value="1"/>
        <!--被踢出後跳轉的地址-->
        <property name="kickoutUrl" value="/kickout.htm"/>
    </bean>

    <!-- 會話管理器 -->
    <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的案例。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章