SpringBoot+Shiro學(xué)習(xí)之?dāng)?shù)據(jù)庫動態(tài)權(quán)限管理和Redis緩存

發(fā)現(xiàn)問題,需找解決思路。

之前我們整合Shiro,完成了登錄認(rèn)證和權(quán)限管理的實(shí)現(xiàn),登錄認(rèn)證沒什么說的,需要實(shí)現(xiàn)AuthorizingRealm中的doGetAuthenticationInfo方法進(jìn)行認(rèn)證,但是我們在實(shí)現(xiàn)doGetAuthorizationInfo權(quán)限控制這個(gè)方法的時(shí)候發(fā)現(xiàn)以下兩個(gè)問題:

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

  • 第二個(gè)問題:每次在訪問設(shè)置了權(quán)限的頁面時(shí),都會去執(zhí)行doGetAuthorizationInfo方法來判斷當(dāng)前用戶是否具備訪問權(quán)限,由于在實(shí)際情況中,權(quán)限是不會經(jīng)常改變的。解決辦法就是進(jìn)行緩存處理。

個(gè)人博客:http://z77z.oschina.io/

此項(xiàng)目下載地址:https://git.oschina.net/z77z/springboot_mybatisplus

第一個(gè)問題解決步驟

建立數(shù)據(jù)庫

我們從ShiroConfig中的filterChainDefinitionMap.put("/add", "perms[權(quán)限添加]"); 配置可以看出,我們需要存儲鏈接,和鏈接需要具備的權(quán)限這兩個(gè)關(guān)鍵字段。還有這個(gè)權(quán)限的讀取是有順序的,所以還要進(jìn)行排序控制,所以我新建表為:

-- ----------------------------
-- 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 '需要具備的權(quán)限',
  `sort` int(50) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

當(dāng)然可以按實(shí)際情況進(jìn)行表的設(shè)計(jì),這里只做簡單學(xué)習(xí)。

改造ShiroConfig.java

改造前:

@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

    // 必須設(shè)置 SecurityManager
    shiroFilterFactoryBean.setSecurityManager(securityManager);

    // 如果不設(shè)置默認(rèn)會自動尋找Web工程根目錄下的"/login.jsp"頁面
    shiroFilterFactoryBean.setLoginUrl("/login");
    // 登錄成功后要跳轉(zhuǎn)的鏈接
    shiroFilterFactoryBean.setSuccessUrl("/index");
    // 未授權(quán)界面;
    shiroFilterFactoryBean.setUnauthorizedUrl("/403");

    // 攔截器.
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
    // 配置不會被攔截的鏈接 順序判斷
    filterChainDefinitionMap.put("/static/**", "anon");
    filterChainDefinitionMap.put("/ajaxLogin", "anon");

    // 配置退出過濾器,其中的具體的退出代碼Shiro已經(jīng)替我們實(shí)現(xiàn)了
    filterChainDefinitionMap.put("/logout", "logout");

    filterChainDefinitionMap.put("/add", "perms[權(quán)限添加]");

    // <!-- 過濾鏈定義,從上向下順序執(zhí)行,一般將 /**放在最為下邊 -->:這是一個(gè)坑呢,一不小心代碼就不好使了;
    // <!-- authc:所有url都必須認(rèn)證通過才可以訪問; anon:所有url都都可以匿名訪問-->
    filterChainDefinitionMap.put("/**", "authc");

    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    System.out.println("Shiro攔截器工廠類注入成功");
    return shiroFilterFactoryBean;
}

改造后:

@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

    // 必須設(shè)置 SecurityManager
    shiroFilterFactoryBean.setSecurityManager(securityManager);

    // 如果不設(shè)置默認(rèn)會自動尋找Web工程根目錄下的"/login.jsp"頁面
    shiroFilterFactoryBean.setLoginUrl("/login");
    // 登錄成功后要跳轉(zhuǎn)的鏈接
    shiroFilterFactoryBean.setSuccessUrl("/index");
    // 未授權(quán)界面;
    shiroFilterFactoryBean.setUnauthorizedUrl("/403");

    // 權(quán)限控制map.
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
    //從數(shù)據(jù)庫獲取
    List<SysPermissionInit> list = sysPermissionInitService.selectAll();

    for (SysPermissionInit sysPermissionInit : list) {
        filterChainDefinitionMap.put(sysPermissionInit.getUrl(),
                sysPermissionInit.getPermissionInit());
    }

    shiroFilterFactoryBean
            .setFilterChainDefinitionMap(filterChainDefinitionMap);
    System.out.println("Shiro攔截器工廠類注入成功");
    return shiroFilterFactoryBean;
}

這里的selectAll()就是從數(shù)據(jù)庫查詢之前創(chuàng)建的權(quán)限管理列表,這里就不貼具體的查詢代碼了。

添加權(quán)限

在數(shù)據(jù)庫中添加權(quán)限如下圖:

這里寫圖片描述

現(xiàn)在啟動程序,在控制臺可以發(fā)現(xiàn)啟動的時(shí)候程序在數(shù)據(jù)庫查詢了權(quán)限的列表信息。做到這步之后還沒有達(dá)到動態(tài)的目的,比如現(xiàn)在到數(shù)據(jù)庫手動修改/add鏈接的權(quán)限,這時(shí)不重啟程序,權(quán)限是不會修改的。

動態(tài)更改權(quán)限實(shí)現(xiàn)

ShiroService.java:

/**
 * 
 * @author 作者: z77z
 * @date 創(chuàng)建時(shí)間:2017年2月15日 下午4:16:07
 */
@Service
public class ShiroService {
    
    @Autowired
    ShiroFilterFactoryBean shiroFilterFactoryBean;
    
    @Autowired
    SysPermissionInitService sysPermissionInitService;
    
    /**
     * 初始化權(quán)限
     */
    public Map<String, String> loadFilterChainDefinitions() {
        // 權(quán)限控制map.從數(shù)據(jù)庫獲取
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        List<SysPermissionInit> list = sysPermissionInitService.selectAll();

        for (SysPermissionInit sysPermissionInit : list) {
            filterChainDefinitionMap.put(sysPermissionInit.getUrl(),
                    sysPermissionInit.getPermissionInit());
        }
        return filterChainDefinitionMap;
    }

    /**
     * 重新加載權(quán)限
     */
    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();

            // 清空老的權(quán)限控制
            manager.getFilterChains().clear();

            shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();
            shiroFilterFactoryBean
                    .setFilterChainDefinitionMap(loadFilterChainDefinitions());
            // 重新構(gòu)建生成
            Map<String, String> chains = shiroFilterFactoryBean
                    .getFilterChainDefinitionMap();
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue().trim()
                        .replace(" ", "");
                manager.createChain(url, chainDefinition);
            }

            System.out.println("更新權(quán)限成功?。?);
        }
    }
}

這樣,可以在修改權(quán)限之后,執(zhí)行updatePermission()這個(gè)方法,權(quán)限就會先被clear,然后重新查詢權(quán)限列表后再set。動態(tài)修改就實(shí)現(xiàn)了!

注意:在本學(xué)習(xí)項(xiàng)目里面,我在設(shè)置登錄用戶的權(quán)限的時(shí)候是寫死了的,所以每個(gè)登錄用戶權(quán)限都是一樣的,實(shí)際開發(fā)中在MyShiroRealm文件中設(shè)置登錄用戶的權(quán)限是從數(shù)據(jù)庫獲取的。還有在實(shí)際開發(fā)中sys_permission_init權(quán)限管理這種表是會在前端提供增刪改查功能的,我學(xué)習(xí)的時(shí)候是直接在數(shù)據(jù)庫手動修改。說到底,本人很懶!

第二個(gè)問題的解決步驟

我們知道Shiro 提供了一系列讓我們自己實(shí)現(xiàn)的接口,包括org.apache.shiro.cache.CacheManager 、org.apache.shiro.cache.Cache 等接口。那么我們要對這些做實(shí)現(xiàn),就實(shí)現(xiàn)了 Shiro 對 Session 和用戶認(rèn)證信息、用戶緩存信息等的緩存,存儲。我們可以用緩存,如 Redis 、 memcache 、 EHCache 等,甚至我們可以用數(shù)據(jù)庫,如 Oracle 、 Mysql 等,都可以,只有效率的快慢問題,功能都可以達(dá)到。

那么我的教程是采用了 Redis ,而且是用了Jedis 。Jedis 可以實(shí)現(xiàn)pool 和hash 的集群Redis 。

本來我想是在網(wǎng)上學(xué)習(xí)學(xué)習(xí),自己實(shí)現(xiàn)redis的集成。最后發(fā)現(xiàn)已經(jīng)有大神已經(jīng)做了這個(gè)插件,對shiro提供的CacheManager,Cache ,這些接口使用redis都有了很好的實(shí)現(xiàn)。我就不需要再費(fèi)心學(xué)習(xí)了,我們就直接拿來用。

pom.xml依賴添加

<!-- shiro+redis緩存插件 -->
<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>2.4.2.1-RELEASE</version>
</dependency>

改造ShiroConfig.java文件

/**
 * @author 作者 z77z
 * @date 創(chuàng)建時(shí)間: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();

        // 必須設(shè)置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 如果不設(shè)置默認(rèn)會自動尋找Web工程根目錄下的"/login.jsp"頁面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登錄成功后要跳轉(zhuǎn)的鏈接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 未授權(quán)界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        // 權(quán)限控制map.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 從數(shù)據(jù)庫獲取
        List<SysPermissionInit> 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();
        // 設(shè)置realm.
        securityManager.setRealm(myShiroRealm());
        // 自定義緩存實(shí)現(xiàn) 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定義session管理 使用redis
        securityManager.setSessionManager(SessionManager());
        return securityManager;
    }

    /**
     * 身份認(rèn)證realm; (這個(gè)需要自己寫,賬號密碼校驗(yàn);權(quán)限等)
     * 
     * @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);// 配置過期時(shí)間
        // redisManager.setTimeout(timeout);
        // redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * cacheManager 緩存 redis實(shí)現(xiàn)
     * 
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao層的實(shí)現(xiàn) 通過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;
    }
}

這里,因?yàn)槭褂玫氖莚edis來做容器緩存,所以要創(chuàng)建redisManager來配置shiro,SessionManager(),cacheManager()這兩個(gè)類都是插件給我們寫好了的,里面就是對shiro提供的接口的redis實(shí)現(xiàn)方式。

使用插件就是這么簡單,直接啟動程序,多訪問幾次具有權(quán)限的頁面,查看控制臺發(fā)現(xiàn),權(quán)限認(rèn)證方法:MyShiroRealm.doGetAuthorizationInfo()會只執(zhí)行了一次。說明我們的緩存生效了。

總結(jié)

到此,我們集成shiro和redis,學(xué)習(xí)了一下功能的實(shí)現(xiàn):

  1. 用戶必須要登陸之后才能訪問定義鏈接,否則跳轉(zhuǎn)到登錄頁面,被禁用戶不能登錄。并且對一些敏感操作鏈接設(shè)置權(quán)限,只有滿足權(quán)限的才可以訪問。
  2. 每個(gè)鏈接的權(quán)限信息保存在數(shù)據(jù)庫,可以動態(tài)進(jìn)行設(shè)置,并且熱加載權(quán)限。
  3. 使用redis對shiro的用戶信息進(jìn)行緩存,不用每次都去執(zhí)行MyShiroRealm.doGetAuthorizationInfo()權(quán)限認(rèn)證方法。
  4. 之前有很多同學(xué)下載我的項(xiàng)目時(shí),運(yùn)行會報(bào)錯(cuò),那是因?yàn)樽罱荚诓粩嘈薷奶峤唬锌赡軙霈F(xiàn)版本問題,現(xiàn)在我在我的碼云上面創(chuàng)建了stable_version分支,都是可以跑起來的。sqltable放在resource目錄下面。
  5. 下一博,我應(yīng)該會寫對在線用戶的管理,踢出登錄的功能學(xué)習(xí)記錄。

香蕉硬幣點(diǎn)贊走一波啦。。。。。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容