Shiro 動態權限管理和Redis緩存

問題描述

之前我們整合Shiro,完成了登錄認證和權限管理的實現,登錄認證沒什麼說的,需要實現AuthorizingRealm中的
doGetAuthenticationInfo方法進行認證,但是我們在實現doGetAuthorizationInfo權限控制這個方法的時候發現以下兩個問題:

  • 第一個問題:我們在ShiroConfig中配置鏈接權限的時候,每次只要有一個新的鏈接,或則權限需要改動,都要在ShiroConfig.java中進行權限的修改。而且改動後還需要重新啓動程序新的權限纔會生效,很麻煩。解決辦法就是將這些鏈接的權限存入數據庫,在前端可以提供增刪改查的功能,在配置文件中編寫權限的時候從數據庫讀取,當權限發生變更的時候利用ShiroFilterFactoryBean的清空功能,先clear,再set。這樣就可以做到到動態的管理權限了。

  • 第二個問題:每次在訪問設置了權限的頁面時,都會去執行doGetAuthorizationInfo方法來判斷當前用戶是否具備訪問權限,由於在實際情況中,權限是不會經常改變的。解決辦法就是進行緩存處理。

第一個問題解決步驟

(1) 建立數據庫表
我們從ShiroConfig中的filterChainDefinitionMap.put("/add", "perms[權限添加]"); 配置可以看出,我們需要存儲鏈接,和鏈接需要具備的權限這兩個關鍵字段。還有這個權限的讀取是有順序的,所以還要進行排序控制,所以我新建表爲:

-- ----------------------------
-- Table structure for sys_permission_init
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission_init`;
CREATE TABLE `sys_permission_init` (
  `id` varchar(255) NOT NULL,
  `url` varchar(255) DEFAULT NULL COMMENT '鏈接地址',
  `permission_init` varchar(255) DEFAULT NULL COMMENT '需要具備的權限',
  `sort` int(50) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

當然可以按實際情況進行表的設計,這裏只做簡單學習。
(2) 改造ShiroConfig.java

@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
  ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  // 必須設置 SecurityManager
  shiroFilterFactoryBean.setSecurityManager(securityManager);
  // 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
  shiroFilterFactoryBean.setLoginUrl("/login");
  // 登錄成功後要跳轉的鏈接
  shiroFilterFactoryBean.setSuccessUrl("/index");
  // 未授權界面;
  shiroFilterFactoryBean.setUnauthorizedUrl("/403");
  // 攔截器.
  Map filterChainDefinitionMap = new LinkedHashMap();
  // 配置不會被攔截的鏈接 順序判斷
  filterChainDefinitionMap.put("/static/**", "anon");
  filterChainDefinitionMap.put("/ajaxLogin", "anon");
  // 配置退出過濾器,其中的具體的退出代碼Shiro已經替我們實現了
  filterChainDefinitionMap.put("/logout", "logout");
  filterChainDefinitionMap.put("/add", "perms[權限添加]");
  // :這是一個坑呢,一不小心代碼就不好使了;
  // 
  filterChainDefinitionMap.put("/**", "authc");
  shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
  System.out.println("Shiro攔截器工廠類注入成功");
  return shiroFilterFactoryBean;
}

改造後:

@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
  ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  // 必須設置 SecurityManager
  shiroFilterFactoryBean.setSecurityManager(securityManager);
  // 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
  shiroFilterFactoryBean.setLoginUrl("/login");
  // 登錄成功後要跳轉的鏈接
  shiroFilterFactoryBean.setSuccessUrl("/index");
  // 未授權界面;
  shiroFilterFactoryBean.setUnauthorizedUrl("/403");
  // 權限控制map.
  Map filterChainDefinitionMap = new LinkedHashMap();
  //從數據庫獲取
  List list = sysPermissionInitService.selectAll();
  for (SysPermissionInit sysPermissionInit : list) {
    filterChainDefinitionMap.put(sysPermissionInit.getUrl(),
        sysPermissionInit.getPermissionInit());
  }
  shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
  System.out.println("Shiro攔截器工廠類注入成功");
  return shiroFilterFactoryBean;
}

這裏的selectAll()就是從數據庫查詢之前創建的權限管理列表,這裏就不貼具體的查詢代碼了。
(3) 添加權限
在數據庫中添加權限如下圖:
在這裏插入圖片描述
現在啓動程序,在控制檯可以發現啓動的時候程序在數據庫查詢了權限的列表信息。做到這步之後還沒有達到動態的目的,比如現在到數據庫手動修改/add鏈接的權限,這時不重啓程序,權限是不會修改的。

(4) 動態更改權限實現
ShiroService.java

/**
 * @author 作者: z77z
 * @date 創建時間:2017年2月15日 下午4:16:07
 */
@Service
public class ShiroService {
  
  @Autowired
  ShiroFilterFactoryBean shiroFilterFactoryBean;
  
  @Autowired
  SysPermissionInitService sysPermissionInitService;
  
  /**
   * 初始化權限
   */
  public Map loadFilterChainDefinitions() {
    // 權限控制map.從數據庫獲取
    Map filterChainDefinitionMap = new LinkedHashMap();
    List list = sysPermissionInitService.selectAll();
    for (SysPermissionInit sysPermissionInit : list) {
      filterChainDefinitionMap.put(sysPermissionInit.getUrl(),
          sysPermissionInit.getPermissionInit());
    }
    return filterChainDefinitionMap;
  }
  /**
   * 重新加載權限
   */
  public void updatePermission() {
    synchronized (shiroFilterFactoryBean) {
      AbstractShiroFilter shiroFilter = null;
      try {
        shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean
            .getObject();
      } catch (Exception e) {
        throw new RuntimeException(
            "get ShiroFilter from shiroFilterFactoryBean error!");
      }
      PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter
          .getFilterChainResolver();
      DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver
          .getFilterChainManager();
      // 清空老的權限控制
      manager.getFilterChains().clear();
      shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();
      shiroFilterFactoryBean
          .setFilterChainDefinitionMap(loadFilterChainDefinitions());
      // 重新構建生成
      Map chains = shiroFilterFactoryBean
          .getFilterChainDefinitionMap();
      for (Map.Entry entry : chains.entrySet()) {
        String url = entry.getKey();
        String chainDefinition = entry.getValue().trim()
            .replace(" ", "");
        manager.createChain(url, chainDefinition);
      }
      System.out.println("更新權限成功!!");
    }
  }
}

這樣,可以在修改權限之後,執行updatePermission()這個方法,權限就會先被clear,然後重新查詢權限列表後再set。動態修改就實現了!

注意:在本學習項目裏面,我在設置登錄用戶的權限的時候是寫死了的,所以每個登錄用戶權限都是一樣的,實際開發中在MyShiroRealm文件中設置登錄用戶的權限是從數據庫獲取的。還有在實際開發中sys_permission_init權限管理這種表是會在前端提供增刪改查功能的,我學習的時候是直接在數據庫手動修改。說到底,本人很懶!

第二個問題的解決步驟

我們知道Shiro 提供了一系列讓我們自己實現的接口,包括org.apache.shiro.cache.CacheManagerorg.apache.shiro.cache.Cache 等接口。那麼我們要對這些做實現,就實現了ShiroSession 和用戶認證信息、用戶緩存信息等的緩存,存儲。我們可以用緩存,如 RedismemcacheEHCache 等,甚至我們可以用數據庫,如 OracleMysql 等,都可以,只有效率的快慢問題,功能都可以達到。

那麼我的教程是採用了 Redis ,而且是用了JedisJedis 可以實現poolhash 的集羣Redis

本來我想是在網上學習學習,自己實現redis的集成。最後發現已經有大神已經做了這個插件,對shiro提供的CacheManagerCache ,這些接口使用redis都有了很好的實現。我就不需要再費心學習了,我們就直接拿來用。

pom.xml依賴添加

<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>2.4.2.1-RELEASE</version>
</dependency>

改造ShiroConfig.java文件

/**
 * @author 作者 z77z
 * @date 創建時間:2017年2月10日 下午1:16:38
 * 
 */
@Configuration
public class ShiroConfig {
  @Autowired
  SysPermissionInitService sysPermissionInitService;
  @Value("${spring.redis.host}")
  private String host;
  @Value("${spring.redis.port}")
  private int port;
  
  @Bean
  public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    // 必須設置 SecurityManager
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    // 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
    shiroFilterFactoryBean.setLoginUrl("/login");
    // 登錄成功後要跳轉的鏈接
    shiroFilterFactoryBean.setSuccessUrl("/index");
    // 未授權界面;
    shiroFilterFactoryBean.setUnauthorizedUrl("/403");
    // 權限控制map.
    Map filterChainDefinitionMap = new LinkedHashMap();
    // 從數據庫獲取
    List list = sysPermissionInitService.selectAll();
    for (SysPermissionInit sysPermissionInit : list) {
      filterChainDefinitionMap.put(sysPermissionInit.getUrl(),
          sysPermissionInit.getPermissionInit());
    }
    shiroFilterFactoryBean
        .setFilterChainDefinitionMap(filterChainDefinitionMap);
    System.out.println("Shiro攔截器工廠類注入成功");
    return shiroFilterFactoryBean;
  }
  @Bean
  public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 設置realm.
    securityManager.setRealm(myShiroRealm());
    // 自定義緩存實現 使用redis
    securityManager.setCacheManager(cacheManager());
    // 自定義session管理 使用redis
    securityManager.setSessionManager(SessionManager());
    return securityManager;
  }
  /**
   * 身份認證realm; (這個需要自己寫,賬號密碼校驗;權限等)
   * 
   * @return
   */
  @Bean
  public MyShiroRealm myShiroRealm() {
    MyShiroRealm myShiroRealm = new MyShiroRealm();
    return myShiroRealm;
  }
  /**
   * 配置shiro redisManager
   * 
   * @return
   */
  public RedisManager redisManager() {
    RedisManager redisManager = new RedisManager();
    redisManager.setHost(host);
    redisManager.setPort(port);
    redisManager.setExpire(1800);// 配置過期時間
    // redisManager.setTimeout(timeout);
    // redisManager.setPassword(password);
    return redisManager;
  }
  /**
   * cacheManager 緩存 redis實現
   * 
   * @return
   */
  public RedisCacheManager cacheManager() {
    RedisCacheManager redisCacheManager = new RedisCacheManager();
    redisCacheManager.setRedisManager(redisManager());
    return redisCacheManager;
  }
  /**
   * RedisSessionDAO shiro sessionDao層的實現 通過redis
   */
  public RedisSessionDAO redisSessionDAO() {
    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
    redisSessionDAO.setRedisManager(redisManager());
    return redisSessionDAO;
  }
  /**
   * shiro session的管理
   */
  public DefaultWebSessionManager SessionManager() {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setSessionDAO(redisSessionDAO());
    return sessionManager;
  }
}

這裏,因爲使用的是redis來做容器緩存,所以要創建redisManager來配置shiroSessionManager()cacheManager()這兩個類都是插件給我們寫好了的,裏面就是對shiro提供的接口的redis實現方式。

使用插件就是這麼簡單,直接啓動程序,多訪問幾次具有權限的頁面,查看控制檯發現,權限認證方法:MyShiroRealm.doGetAuthorizationInfo()會只執行了一次。說明我們的緩存生效了。

總結

到此,我們集成shiroredis,學習了一下功能的實現:

  1. 用戶必須要登陸之後才能訪問定義鏈接,否則跳轉到登錄頁面,被禁用戶不能登錄。並且對一些敏感操作鏈接設置權限,只有滿足權限的纔可以訪問。
  2. 每個鏈接的權限信息保存在數據庫,可以動態進行設置,並且熱加載權限。
  3. 使用redisshiro的用戶信息進行緩存,不用每次都去執行MyShiroRealm.doGetAuthorizationInfo()權限認證方法。
  4. 之前有很多同學下載我的項目時,運行會報錯,那是因爲最近都在不斷修改提交,有可能會出現版本問題,現在 我在我的碼雲上面創建了stable_version分支,都是可以跑起來的。sqltable放在resource目錄下面。
  5. 下一博,我應該會寫對在線用戶的管理,踢出登錄的功能學習記錄。

項目地址: https://gitee.com/z77z/springboot_mybatisplus

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