springboot+shiro整合

wangbiao007發表於2020-04-05

 

本篇部落格主要是學習shiro許可權管理系統的一篇入門部落格,已程式碼為主要內容。
shiro介紹
Shiro是Apache下的一個開源專案,我們稱之為Apache Shiro。它是一個很易用與Java專案的的安全框架,提供了認證、授權、加密、會話管理,與Spring Security 一樣都是做一個許可權的安全框架,但是與Spring Security 相比,在於 Shiro 使用了比較簡單易懂易於使用的授權方式。shiro屬於輕量級框架,相對於security簡單的多,也沒有security那麼複雜。所以我這裡也是簡單介紹一下shiro的使用。
Authentication(認證), Authorization(授權), Session Management(會話管理), Cryptography(加密)被 Shiro 框架的開發團隊稱之為應用安全的四大基石。 
 Authentication(認證):使用者身份識別,通常被稱為使用者“登入” 
 Authorization(授權):訪問控制。比如某個使用者是否具有某個操作的使用許可權。 
 Session Management(會話管理):特定於使用者的會話管理,甚至在非web 或 EJB 應用程式。 
 Cryptography(加密):在對資料來源使用加密演算法加密的同時,保證易於使用。 
shiro的三大組建
Subject:可以看成是使用者,雖然這樣理解不一定正確,但是這樣卻好理解,可以由subject來對使用者進行一個封裝,通過subject來判斷這個使用者的許可權等資訊。
SecurityManager:安全管理器,Shiro的核心,用於管理所有的Subject ,它主要用於協調Shiro內部各種安全元件,例如Realm,Session,Cache等,不過我們一般不用太關心SecurityManager,對於應用程式開發者來說,主要還是使用Subject的API來處理各種安全驗證邏輯。
Realm:這是用於連線Shiro和客戶系統的使用者資料的橋樑。一旦Shiro真正需要訪問各種安全相關的資料(比如使用使用者賬戶來做使用者身份驗證以及許可權驗證)時,他總是通過呼叫系統配置的各種Realm來讀取資料。 
正因為如此,Realm往往被看做是安全領域的DAO,他封裝了資料來源連線相關的細節,將資料以Shiro需要的格式提供給Shiro。
è¿éåå¾çæè¿°
Subject (org.apache.shiro.subject.Subject),如上所述. 
SecurityManager (org.apache.shiro.mgt.SecurityManager),如上所述. 
Authenticator(使用者認證管理器), (org.apache.shiro.authc.Authenticator) 這個元件主要用於處理使用者登入邏輯,他通過呼叫Realm的介面來判斷當前登入的使用者的身份。 
使用者認證策略,(org.apache.shiro.authc.pam.AuthenticationStrategy) 如果系統配置了多個Realm,則需要使用AuthenticationStrategy 來協調這些Realm以便決定一個使用者登入的認證是成功還是失敗。(比如,如果一個Realm驗證成功了,但是其他的都失敗了,怎麼算?還是說都成功才算成功). 
Authorizer(許可權管理器)(org.apache.shiro.authz.Authorizer)這個元件主要是用來做使用者的訪問控制。通俗來說就是決定使用者能做什麼、不能做什麼。和Authenticator類似,Authorizer也知道怎麼協調多個Realm資料來源的資料,他有自己的一套策略。 
SessionManager(會話管理器) (org.apache.shiro.session.mgt.SessionManager) SessionManager知道如何建立會話、管理使用者回話的宣告週期以便在所有執行環境下都可以給使用者提供一個健壯的回話管理體驗。Shiro在任何執行環境下都可以在本地管理使用者會話(即便沒有Web或者EJB容器也可以)——這在安全管理的框架中算是獨門絕技了。當然,如果當前環境中有會話管理機制(比如Servlet容器),則Shiro預設會使用該環境的會話管理機制。而如果像控制檯程式這種獨立的應用程式,本身沒有會話管理機制,此時Shiro就會使用內部的會話管理器來給應用的開發提供一直的程式設計體驗。SessionDAO允許使用者使用任何型別的資料來源來儲存Session資料。 
SessionDAO (org.apache.shiro.session.mgt.eis.SessionDAO) 用於代替SessionManager執行Session相關的增刪改查。這個介面允許我們將任意種類的資料儲存方式引入到Session管理的基礎框架中。 
CacheManager (org.apache.shiro.cache.CacheManager) CacheManager用於建立和維護一些在其他的Shiro元件中用到的Cache例項,維護這些Cache例項的生命週期。快取用於儲存那些從後端獲取到的使用者驗證與許可權控制方面的資料以提高效能,快取是一等公民,在獲取資料時,總是先從快取中查詢,如果沒有再呼叫後端介面從其他資料來源獲取。Shiro允許使用者使用其他更加現代的、企業級的資料來源來替代內部的預設實現,以提供更高的效能和更好的使用者體驗。 
Cryptography 加密技術,(org.apache.shiro.crypto.*) 對於一個企業級的安全框架來說,加密算是其固有的一種特性。Shiro的crypto包中包含了一系列的易於理解和使用的加密、雜湊(aka摘要)輔助類。這個包內的所有類都是經過精心設計,相比於java本身提供的那一套反人類的加密元件,Shiro提供的這套加密元件簡直不要好用太多。 
 Realm (org.apache.shiro.realm.Realm) 就如上文所提到的,Realm是連線Shiro和你的安全資料的橋樑。任何時候當Shiro需要執行登入或者訪問控制的時候,都需要呼叫已經配置的Realm的介面去獲取資料。一個應用程式可以配置一個或者多個Realm(最少配置一個)。
下面是一個使用者通過shiro登入的流程圖

1.使用使用者的登入資訊建立令牌
 

        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password,rememberMe);

2.執行登陸動作

//獲取user封裝物件,可以將它看成是一個操作使用者的入口        
Subject subject = SecurityUtils.getSubject();
subject.login(usernamePasswordToken);//執行登入操作
Session session = subject.getSession();獲取登入後的session
SysUser user=(SysUser) subject.getPrincipal();
//更新使用者登入時間,也可以在ShiroRealm裡面做
session.setAttribute("user", user);//將使用者資訊存入到session中
session.setTimeout(360000);

3.判斷使用者

Shiro本身無法知道所持有令牌的使用者是否合法,因為除了專案的設計人員恐怕誰都無法得知。因此Realm是整個框架中為數不多的必須由設計者自行實現的模組,當然Shiro提供了多種實現的途徑,本文只介紹最常見也最重要的一種實現方式——資料庫查詢。
會呼叫自己寫的Realm裡面的doGetAuthenticationInfo和doGetAuthorizationInfo進行使用者驗證和許可權授權。

下面是這個專案的程式碼,本專案是springboot+shiro+mybatis+redis。其中springboot的檢視解析器用的是freemark,實現了使用者的登入驗證,相關許可權授權,記住我,將使用者許可權存入redis,避免每次驗證許可權的時候都去資料庫中查詢。本來也想著實現將session存入到redis中的,但是最後這個功能沒有實現。
下面首先是資料庫的設計:總共五張表:使用者表,角色表,許可權表,使用者角色表,角色許可權表

CREATE TABLE `sys_user` (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT '' COMMENT '使用者名稱',
  `password` varchar(256) DEFAULT NULL COMMENT '登入密碼',
  `name` varchar(256) DEFAULT NULL COMMENT '使用者真實姓名',
  `id_card_num` varchar(256) DEFAULT NULL COMMENT '使用者身份證號',
  `state` char(3) DEFAULT '0' COMMENT '使用者狀態:0:正常狀態,1:使用者被鎖定',
  `create_time` datetime DEFAULT NULL COMMENT '建立時間',
  PRIMARY KEY (`uid`),
  UNIQUE KEY `username` (`username`) USING BTREE,
  UNIQUE KEY `id_card_num` (`id_card_num`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `state` char(1) DEFAULT '0' COMMENT '是否可用0可用  1不可用',
  `role` varchar(20) DEFAULT NULL COMMENT '角色標識程式中判斷使用,如"admin"',
  `description` varchar(100) DEFAULT NULL COMMENT '角色描述,UI介面顯示使用',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `role` (`role`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `parent_id` int(11) DEFAULT NULL COMMENT '父編號,本許可權可能是該父編號許可權的子許可權',
  `parent_ids` varchar(20) DEFAULT NULL COMMENT '父編號列表',
  `permission` varchar(100) DEFAULT NULL COMMENT '許可權字串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view',
  `resource_type` varchar(20) DEFAULT NULL COMMENT '資源型別,[menu|button]',
  `url` varchar(200) DEFAULT NULL COMMENT '資源路徑 如:/userinfo/list',
  `name` varchar(50) DEFAULT NULL COMMENT '許可權名稱',
  `available` char(1) DEFAULT '0' COMMENT '是否可用0可用  1不可用',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_user_role` (
  `uid` int(11) DEFAULT NULL COMMENT '使用者id',
  `role_id` int(11) DEFAULT NULL COMMENT '角色id',
  KEY `uid` (`uid`) USING BTREE,
  KEY `role_id` (`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `sys_role_permission` (
  `role_id` int(11) DEFAULT NULL COMMENT '角色id',
  `permission_id` int(11) DEFAULT NULL COMMENT '許可權id',
  KEY `role_id` (`role_id`) USING BTREE,
  KEY `permission_id` (`permission_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `sys_user` (`uid`, `username`, `password`, `name`, `id_card_num`, `state`, `create_time`)
VALUES
	(1, 'zhangsan', '123456', '張三', '422201199107280987', '1', '2019-08-07 11:21:00'),
	(2, 'lisi', '123456', '李四', '322213199509082345', '-1', '2019-08-07 11:21:00'),
	(3, 'wangwu', '123456', '王五', '12345678908', '-1', '2019-08-14 09:52:04'),
	(4, 'chenliu', '123456', '陳六', '243545342', '-1', '2019-08-14 10:11:12'),
	(5, 'qianqi', '666666', '錢七', '38478483993', '1', '2019-08-14 10:12:24');
INSERT INTO `sys_role` (`id`, `state`, `role`, `description`, `create_time`)
VALUES
	(1, '1', '超級管理員', '所有許可權都有', '2019-08-07 11:24:00'),
	(2, '1', '普通操作員', '檢視許可權,新增許可權', '2019-08-07 11:24:00'),
	(3, '1', '遊客', '檢視許可權', '2019-08-14 22:56:00');
INSERT INTO `sys_permission` (`id`, `parent_id`, `parent_ids`, `permission`, `resource_type`, `url`, `name`, `available`)
VALUES
	(1, NULL, NULL, 'user:selectSysUser', 'menu', NULL, '使用者列表', '1'),
	(2, 1, NULL, 'user:addSysUser', 'button', NULL, '新增使用者', '1'),
	(3, NULL, NULL, 'user:updateSysUser', 'button', NULL, '更改狀態', '0');
INSERT INTO `sys_user_role` (`uid`, `role_id`)
VALUES
	(1, 1),
	(2, 2),
	(3, 3);
INSERT INTO `sys_role_permission` (`role_id`, `permission_id`)
VALUES
	(1, 1),
	(1, 2),
	(2, 1),
	(1, 3),
	(2, 2),
	(3, 1);

下面是pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>study.springboot</groupId>
    <artifactId>springbootShiro</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>

        <!-- 引入 freemarker 模板依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.mybatis.generator/mybatis-generator-core  程式碼生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>2.3</version>
        </dependency>

        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.0</version>
        </dependency>


        <!-- 整合shiro框架 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- shiro-thymeleaf 2.0.0-->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

        <dependency>
            <groupId>net.mingsoft</groupId>
            <artifactId>shiro-freemarker-tags</artifactId>
            <version>0.1</version>
            <exclusions>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>javax.servlet-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>

        <!--配置reids-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Redis-Jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>



        <!--hutool 工具類 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>4.6.1</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置檔案 

############################################################
#
# freemarker配置
#
############################################################
#模板檔案路徑(不推薦使用)
spring.freemarker.template-loader-path=classpath:/templates
#關閉快取即時重新整理,生產環境需要改成true;
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.request-context-attribute=request
spring.freemarker.suffix=.html

############################################################
#
# 資料庫 druid配置
#
############################################################
# 資料庫訪問配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=gbk&zeroDateTimeBehavior=convertToNull&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
# 下面為連線池的補充設定,應用到上面所有資料來源中
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置獲取連線等待超時的時間
spring.datasource.maxWait=60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連線,單位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一個連線在池中最小生存的時間,單位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 配置監控統計攔截的filters,去掉後監控介面sql無法統計,'wall'用於防火牆
spring.datasource.filters=stat,wall,log4j
spring.datasource.logSlowSql=true


# 注意:對應實體類的路徑,多個package之間可以用逗號
#mybatis.type-aliases-package=study.springboot.demo.entity
#注意:一定要對應mapper對映xml檔案的所在路徑
#mybatis.mapper-locations=classpath*:mapper/*.xml

mybatis-plus.mapper-locations=classpath:/mapper/*.xml
mybatis-plus.typeAliasesPackage=study.springboot.demo.entity
mybatis-plus.configuration.mapUnderscoreToCamelCase=true
#sql日誌輸出
logging.level.study.springboot.demo.dao=debug 



############################################################
#
# 快取  redis配置
#
############################################################
# REDIS (RedisProperties)
# Redis資料庫索引(預設為0)
spring.redis.database=0
# Redis伺服器地址
spring.redis.host=localhost
# Redis伺服器連線埠
spring.redis.port=6379
# Redis伺服器連線密碼(預設為空)
spring.redis.password=
# 連線池最大連線數(使用負值表示沒有限制)
spring.redis.jedis.pool.max-active=8
# 連線池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.jedis.pool.max-wait=-1
# 連線池中的最大空閒連線
spring.redis.jedis.pool.max-idle=8
# 連線池中的最小空閒連線
spring.redis.jedis.pool.min-idle=0
# 連線超時時間(毫秒)
spring.redis.timeout=0

ShiroConfig的配置,可以看成是shiro的核心 

package study.springboot.demo.config;


import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import study.springboot.demo.shiro.RedisSessionDAO;
import study.springboot.demo.shiro.ShiroRedisCacheManager;
import study.springboot.demo.shiro.ShiroRealm;

import java.util.*;

/**
 * 參考部落格:https://blog.csdn.net/qq_34021712/article/details/80294417
 *
 * @Author wangbiao
 * @Date 2019-07-29 20:43
 * @Decripition TODO
 **/
@Configuration
public class ShiroConfig {
    /**
     * ShiroFilterFactoryBean 處理攔截資原始檔問題。
     * 注意:初始化ShiroFilterFactoryBean的時候需要注入:SecurityManager
     * Web應用中,Shiro可控制的Web請求必須經過Shiro主過濾器的攔截
     * @param securityManager
     * @return
     */
    @Bean(name = "shirFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //必須設定 SecurityManager,Shiro的核心安全介面
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //這裡的/login是後臺的介面名,非頁面,如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
        shiroFilterFactoryBean.setLoginUrl("/login");
        //這裡的/index是後臺的介面名,非頁面,登入成功後要跳轉的連結
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //未授權介面,該配置無效,並不會進行頁面跳轉
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

        //自定義攔截器限制併發人數,參考部落格:
        //LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
        //限制同一帳號同時線上的個數
        //filtersMap.put("kickout", kickoutSessionControlFilter());
        //shiroFilterFactoryBean.setFilters(filtersMap);

        // 配置訪問許可權 必須是LinkedHashMap,因為它必須保證有序
        // 過濾鏈定義,從上向下順序執行,一般將 /**放在最為下邊 一定要注意順序,否則就不好使了
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //配置不登入可以訪問的資源,anon 表示資源都可以匿名訪問
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/toLogin", "anon");
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/druid/**", "anon");
        //logout是shiro提供的過濾器
        filterChainDefinitionMap.put("/logout", "anon");
        filterChainDefinitionMap.put("/novelMain/*", "anon");
        //此時訪問/userInfo/del需要del許可權,在自定義Realm中為使用者授權。
        //filterChainDefinitionMap.put("/userInfo/del", "perms[\"userInfo:del\"]");

        //其他資源都需要認證  authc 表示需要認證才能進行訪問 user表示配置記住我或認證通過可以訪問的地址
        filterChainDefinitionMap.put("/**", "user");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }





    /**
     *
     * 配置核心安全事務管理器,用來協調shiro的各個組建,她相當於是個大管家
     *
     * @param shiroRealm
     * @return
     */
    @Bean(name="securityManager")
    public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        //設定自定義realm.用於許可權的驗證和授權
        securityManager.setRealm(shiroRealm);
        //配置記住我 ,利用cookie記住使用者的登入資訊,即使關掉瀏覽器,下次開啟網頁也是登入狀態
        securityManager.setRememberMeManager(rememberMeManager());

        //配置 redis快取管理器 參考部落格:https://blog.csdn.net/qq_31897023/article/details/89082541
        //這樣做的目的是將許可權資訊存入到資料庫中,避免了每次許可權驗證的時候都需要去從資料庫裡面查詢這個使用者對應的許可權
        securityManager.setCacheManager(cacheManager());

        //配置自定義session管理,使用redis 如果在叢集的基礎上實現shiro session共享
        //securityManager.setSessionManager(sessionManager());

        return securityManager;
    }


    /**
     * 起到授權和驗證的作用,在登入時驗證使用者名稱和密碼
     * 在訪問的時候起到許可權的作用
     *  身份認證realm; (這個需要自己寫,賬號密碼校驗;許可權等)
     * @return
     */
    @Bean
    public ShiroRealm shiroRealm(){
        ShiroRealm shiroRealm = new ShiroRealm();
        return shiroRealm;
    }

    /**
     * 開啟Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證
     * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能
     * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /** * Shiro生命週期處理器 * @return */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){ return new LifecycleBeanPostProcessor(); }



    /**
     * 解決: 無許可權頁面不跳轉 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized") 無效
     * shiro的原始碼ShiroFilterFactoryBean.Java定義的filter必須滿足filter instanceof AuthorizationFilter,
     * 只有perms,roles,ssl,rest,port才是屬於AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,
     * 所以unauthorizedUrl設定後頁面不跳轉 Shiro註解模式下,登入失敗與沒有許可權都是通過丟擲異常。
     * 並且預設並沒有去處理或者捕獲這些異常。在SpringMVC下需要配置捕獲相應異常來通知使用者資訊
     * @return
     */
    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver();
        Properties properties=new Properties();
        //這裡的 /unauthorized 是頁面,不是訪問的路徑
        properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/unauthorized");
        properties.setProperty("org.apache.shiro.authz.UnauthenticatedException","/unauthorized");
        simpleMappingExceptionResolver.setExceptionMappings(properties);
        return simpleMappingExceptionResolver;
    }



    /**
     * 記住我功能在各各網站是比較常見的,實現起來也都差不多,主要就是利用cookie來實現,而shiro對記住我功能的實現也是比較簡單的,只需要幾步即可。
     * Shiro提供了記住我(RememberMe)的功能,比如訪問一些網站時,關閉了瀏覽器下次再開啟時還是能記住你是誰,下次訪問時無需再登入即可訪問,基本流程如下:
     * (1)、首先在登入頁面選中RememberMe然後登入成功;如果是瀏覽器登入,一般會把RememberMe的Cookie寫到客戶端並儲存下來;
     * (2)、關閉瀏覽器再重新開啟;會發現瀏覽器還是記住你的;
     * (3)、訪問一般的網頁伺服器端還是知道你是誰,且能正常訪問;
     *
     *
     *
     * cookie物件;會話Cookie模板 ,預設為: JSESSIONID 問題: 與SERVLET容器名衝突,重新定義為sid或rememberMe,自定義
     * @return
     */
    @Bean
    public SimpleCookie rememberMeCookie(){
        //這個引數是cookie的名稱,對應前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //setcookie的httponly屬性如果設為true的話,會增加對xss防護的安全係數。它有以下特點:

        //setcookie()的第七個引數
        //設為true後,只能通過http訪問,javascript無法訪問
        //防止xss讀取cookie
        simpleCookie.setHttpOnly(true);
        simpleCookie.setPath("/");
        //<!-- 記住我cookie生效時間30天 ,單位秒;-->
        simpleCookie.setMaxAge(2592000);
        return simpleCookie;
    }


    /**
     * cookie管理物件;記住我功能,rememberMe管理器
     * @return
     */
    @Bean
    public CookieRememberMeManager rememberMeManager(){
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        //rememberMe cookie加密的金鑰 建議每個專案都不一樣 預設AES演算法 金鑰長度(128 256 512 位)
        cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
        return cookieRememberMeManager;
    }

    /**
     * FormAuthenticationFilter 過濾器 過濾記住我
     * @return
     */
    @Bean
    public FormAuthenticationFilter formAuthenticationFilter(){
        FormAuthenticationFilter formAuthenticationFilter = new FormAuthenticationFilter();
        //對應前端的checkbox的name = rememberMe
        formAuthenticationFilter.setRememberMeParam("rememberMe");
        return formAuthenticationFilter;
    }



    /*******start************下面是使用redis進行快取管理********start*********/
    @Bean
    public ShiroRedisCacheManager cacheManager(){
        return new ShiroRedisCacheManager();
    }


    /**
     * Session Manager
     * 使用的是shiro-redis開源外掛
     */
    @Bean
    public RedisSessionDAO redisSessionDAO(){
        return new RedisSessionDAO();
    }

    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        sessionManager.setGlobalSessionTimeout(60*60*24);
        sessionManager.setCacheManager(cacheManager());
        sessionManager.setDeleteInvalidSessions(true);//刪除過期的session
        sessionManager.setSessionIdCookieEnabled(true);
        sessionManager.setSessionIdCookie(sessionIdCookie());
        return sessionManager;
    }

    //設定cookie
    @Bean
    public Cookie sessionIdCookie(){
        Cookie sessionIdCookie=new SimpleCookie("STID");
        sessionIdCookie.setMaxAge(-1);
        sessionIdCookie.setHttpOnly(true);
        return sessionIdCookie;
    }

}

shiroRealm,使用者使用者驗證和許可權授權的,連結資料庫裡面的使用者資訊
 

package study.springboot.demo.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import study.springboot.demo.dao.SysPermissionMapper;
import study.springboot.demo.dao.SysRoleMapper;
import study.springboot.demo.dao.SysUserMapper;
import study.springboot.demo.entity.SysPermission;
import study.springboot.demo.entity.SysRole;
import study.springboot.demo.entity.SysUser;

import java.util.List;


/**
 * @Author wangbiao
 * @Date 2019-07-29 21:17
 * @Decripition TODO 在Shiro中,最終是通過Realm來獲取應用程式中的使用者、角色及許可權資訊的
 *   在Realm中會直接從我們的資料來源中獲取Shiro需要的驗證資訊。可以說,Realm是專用於安全框架的DAO.
 **/
public class ShiroRealm extends AuthorizingRealm {
    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private SysRoleMapper sysRoleMapper;

    @Autowired
    private SysPermissionMapper sysPermissionMapper;

    @Override
    public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken;
        String name = usernamePasswordToken.getUsername();
        String password = new String(usernamePasswordToken.getPassword());
        SysUser sysUserQuery  = new SysUser();
        sysUserQuery.setUsername(name);
        SysUser sysUser = sysUserMapper.selectOne(sysUserQuery);
        if(sysUser==null){
            throw new UnknownAccountException("使用者名稱或密碼錯誤!");
        }
        if(!password.equals(sysUser.getPassword())){
            throw new IncorrectCredentialsException("使用者名稱或密碼錯誤!");
        }
        if ("0".equals(sysUser.getState())) {
            throw new LockedAccountException("賬號已被鎖定,請聯絡管理員!");
        }
        //鹽值加密
        ByteSource byteSource = ByteSource.Util.bytes(sysUser.getUsername());
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(sysUser,sysUser.getPassword(),byteSource,getName());
        return info;
    }


    /**
     * 授權使用者許可權
     * 授權的方法是在碰到<shiro:hasPermission name=''></shiro:hasPermission>標籤的時候呼叫的
     * 它會去檢測shiro框架中的許可權(這裡的permissions)是否包含有該標籤的name值,如果有,裡面的內容顯示
     * 如果沒有,裡面的內容不予顯示(這就完成了對於許可權的認證.)
     *
     * shiro的許可權授權是通過繼承AuthorizingRealm抽象類,過載doGetAuthorizationInfo();
     * 當訪問到頁面的時候,連結配置了相應的許可權或者shiro標籤才會執行此方法否則不會執行
     * 所以如果只是簡單的身份認證沒有許可權的控制的話,那麼這個方法可以不進行實現,直接返回null即可。
     *
     * 在這個方法中主要是使用類:SimpleAuthorizationInfo 進行角色的新增和許可權的新增。
     * authorizationInfo.addRole(role.getRole()); authorizationInfo.addStringPermission(p.getPermission());
     *
     * 當然也可以新增set集合:roles是從資料庫查詢的當前使用者的角色,stringPermissions是從資料庫查詢的當前使用者對應的許可權
     * authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(stringPermissions);
     *
     * 就是說如果在shiro配置檔案中新增了filterChainDefinitionMap.put("/add", "perms[許可權新增]");
     * 就說明訪問/add這個連結必須要有“許可權新增”這個許可權才可以訪問
     *
     * 如果在shiro配置檔案中新增了filterChainDefinitionMap.put("/add", "roles[100002],perms[許可權新增]");
     * 就說明訪問/add這個連結必須要有 "許可權新增" 這個許可權和具有 "100002" 這個角色才可以訪問
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        //獲取使用者
        SysUser user = (SysUser) SecurityUtils.getSubject().getPrincipal();

        //獲取使用者角色
        List<SysRole> roles =this.sysRoleMapper.findRolesByUserId(user.getUid());
        //新增角色
        SimpleAuthorizationInfo authorizationInfo =  new SimpleAuthorizationInfo();
        for (SysRole role : roles) {
            authorizationInfo.addRole(role.getRole());
        }

        //獲取使用者許可權
        List<SysPermission> permissions = this.sysPermissionMapper.findPermissionsByRoleId(roles);
        //新增許可權
        for (SysPermission permission:permissions) {
            authorizationInfo.addStringPermission(permission.getPermission());
        }

        return authorizationInfo;
    }
}

登入控制類
 

package study.springboot.demo.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap;
import study.springboot.demo.entity.SysUser;

/**
 * @Author wangbiao
 * @Date 2019-08-06 09:14
 * @Decripition TODO
 **/
@Controller
@RequestMapping("/")
public class LoginController {

    @RequestMapping("/login")
    public String toLogin(){
        return "login";
    }

    /**
     * 登出  這個方法沒用到,用的是shiro預設的logout
     * @param model
     * @return
     */
    @RequestMapping("/logout")
    public String logout(Model model) {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        model.addAttribute("msg","安全退出!");
        return "login";
    }

    @RequestMapping("/toLogin")
    public String login(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam(value = "rememberMe",required = false)String rememberMe,RedirectAttributesModelMap model){
        boolean rememberMeB = false;
        if("true".equals(rememberMe)){
            rememberMeB = true;
        }
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
        try{
            usernamePasswordToken.setRememberMe(rememberMeB);//記住我
            subject.login(usernamePasswordToken);
            Session session = subject.getSession();
            SysUser user=(SysUser) subject.getPrincipal();
            //更新使用者登入時間,也可以在ShiroRealm裡面做
            session.setAttribute("user", user);
            session.setTimeout(360000);
        }catch (UnknownAccountException uae) {
            model.addFlashAttribute("error", "未知使用者");
            return redirectTo("/login");
        } catch (IncorrectCredentialsException ice) {
            model.addFlashAttribute("error", "密碼錯誤");
            return redirectTo("/login");
        } catch (LockedAccountException lae) {
            model.addFlashAttribute("error", "賬號已鎖定");
            return redirectTo("/login");
        }
        catch (AuthenticationException ae) {
            //unexpected condition?  error?
            model.addFlashAttribute("error", "伺服器繁忙");
            return redirectTo("/login");
        }
        subject.login(usernamePasswordToken);
        return "index";
    }



    /**
     * 重定向至地址 url
     *
     * @param url
     *            請求地址
     * @return
     */
    protected String redirectTo( String url ) {
        StringBuffer rto = new StringBuffer("redirect:");
        rto.append(url);
        return rto.toString();
    }




}

 

/**
 *   整合html頁面的Shiro標籤  ,freemark為解析器的時候一定要用到這個
 * Created by Administrator on 2016/3/15.
 */
@Component
public class ShiroTagFreeMarkerConfigurer implements InitializingBean {

    @Autowired
    private Configuration configuration;

    @Autowired
    private FreeMarkerViewResolver resolver;

    @Override
    public void afterPropertiesSet()  {
        // 加上這句後,可以在頁面上使用shiro標籤
        configuration.setSharedVariable("shiro", new ShiroTags());
        // 加上這句後,可以在頁面上用${context.contextPath}獲取contextPath
        resolver.setRequestContextAttribute("context");
    }

}
package study.springboot.demo.shiro;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import study.springboot.demo.redis.JedisUtil;
import study.springboot.demo.redis.SerializableUtil;

import java.util.*;

/**
 * 重寫Shiro的Cache儲存讀取
 * @author Wang926454
 * @date 2018/9/4 17:31
 */
public class ShiroRedisCache<K,V> implements Cache<K,V> {

    /**
     * redis-key-字首-shiro:cache:
     */
    public final static String PREFIX_SHIRO_CACHE = "shiro:cache:";

    /**
     * 過期時間-5分鐘
     */
    private static final Integer EXPIRE_TIME = 5 * 60 * 1000;

    /**
     * 快取的key名稱獲取為shiro:cache:account
     * @param key
     * @return java.lang.String
     * @author Wang926454
     * @date 2018/9/4 18:33
     */
    private String getKey(Object key){
        return PREFIX_SHIRO_CACHE + key.toString();
    }

    /**
     * 獲取快取
     */
    @Override
    public Object get(Object key) throws CacheException {
        if(!JedisUtil.exists(this.getKey(key))){
            return null;
        }
        return JedisUtil.getObject(this.getKey(key));
    }

    /**
     * 儲存快取
     */
    @Override
    public Object put(Object key, Object value) throws CacheException {
        // 設定Redis的Shiro快取
        return JedisUtil.setObject(this.getKey(key), value, EXPIRE_TIME);
    }

    /**
     * 移除快取
     */
    @Override
    public Object remove(Object key) throws CacheException {
        if(!JedisUtil.exists(this.getKey(key))){
            return null;
        }
        JedisUtil.delKey(this.getKey(key));
        return null;
    }

    /**
     * 清空所有快取
     */
    @Override
    public void clear() throws CacheException {
        JedisUtil.getJedis().flushDB();
    }

    /**
     * 快取的個數
     */
    @Override
    public int size() {
        Long size = JedisUtil.getJedis().dbSize();
        return size.intValue();
    }

    /**
     * 獲取所有的key
     */
    @Override
    public Set keys() {
        Set<byte[]> keys = JedisUtil.getJedis().keys(new String("*").getBytes());
        Set<Object> set = new HashSet<Object>();
        for (byte[] bs : keys) {
            set.add(SerializableUtil.unserializable(bs));
        }
        return set;
    }

    /**
     * 獲取所有的value
     */
    @Override
    public Collection values() {
        Set keys = this.keys();
        List<Object> values = new ArrayList<Object>();
        for (Object key : keys) {
            values.add(JedisUtil.getObject(this.getKey(key)));
        }
        return values;
    }
}
package study.springboot.demo.shiro;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;

/**
 * @Author wangbiao
 * @Date 2019-08-16 18:13
 * @Decripition TODO
 **/
public class ShiroRedisCacheManager implements CacheManager {
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return new ShiroRedisCache<>();
    }
}
package study.springboot.demo.shiro;



import java.io.Serializable;


import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import study.springboot.demo.redis.JedisUtil;


/**
 * @Author wangbiao
 * @Date 2019-08-17 23:48
 * @Decripition TODO
 **/

public class RedisSessionDAO extends EnterpriseCacheSessionDAO {

    @Override
    public void delete(Session session) {
        if(session == null || session.getId() == null){
            System.out.println("Session is null");
            return;
        }
        JedisUtil.delKey(session.getId().toString());
    }



    @Override
    public void update(Session session) throws UnknownSessionException {
        if(session == null || session.getId() == null){
            System.out.println("Session is null");
            return;
        }
        Serializable sessionId = session.getId();
        JedisUtil.setObject(sessionId.toString(), session);
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        //新增進redis
        JedisUtil.setObject(sessionId.toString(), session);

        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        return (Session)JedisUtil.getObject(sessionId.toString());
    }



}
package study.springboot.demo.redis;

import study.springboot.demo.common.CustomException;

import java.io.*;

/**
 * Serializable工具(JDK)(也可以使用Protobuf自行百度)
 * @author Wang926454
 * @date 2018/9/4 15:13
 */
public class SerializableUtil {

    /**
     * 序列化
     * @param object
     * @return byte[]
     * @author Wang926454
     * @date 2018/9/4 15:14
     */
    public static byte[] serializable(Object object) {
        ByteArrayOutputStream baos = null;
        ObjectOutputStream oos = null;
        try {
            baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);
            oos.writeObject(object);
            byte[] bytes = baos.toByteArray();
            return bytes;
        } catch (IOException e) {
            e.printStackTrace();
            throw new CustomException("SerializableUtil工具類序列化出現IOException異常");
        } finally {
            try {
                if(oos != null) {
                    oos.close();
                }
                if(baos != null) {
                    baos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 反序列化
     * @param bytes
     * @return java.lang.Object
     * @author Wang926454
     * @date 2018/9/4 15:14
     */
    public static Object unserializable(byte[] bytes) {
        ByteArrayInputStream bais = null;
        ObjectInputStream ois = null;
        try {
            bais = new ByteArrayInputStream(bytes);
            ois = new ObjectInputStream(bais);
            return ois.readObject();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new CustomException("SerializableUtil工具類反序列化出現ClassNotFoundException異常");
        } catch (IOException e) {
            e.printStackTrace();
            throw new CustomException("SerializableUtil工具類反序列化出現IOException異常");
        } finally {
            try {
                if(ois != null) {
                    ois.close();
                }
                if(bais != null) {
                    bais.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
package study.springboot.demo.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import study.springboot.demo.common.CustomException;

import java.util.Set;

/**
 * JedisUtil(推薦存Byte陣列,存Json字串效率更慢)
 * @author Wang926454
 * @date 2018/9/4 15:45
 */
@Component
public class JedisUtil {

    /**
     * Logger
     */
    private static Logger logger = LoggerFactory.getLogger(JedisUtil.class);

    /**
     * Status-OK
     */
    public final static String OK = "OK";

    /**
     * 靜態注入JedisPool連線池
     * 本來是正常注入JedisUtil,可以在Controller和Service層使用,但是重寫Shiro的CustomCache無法注入JedisUtil
     * 現在改為靜態注入JedisPool連線池,JedisUtil直接呼叫靜態方法即可
     * https://blog.csdn.net/W_Z_W_888/article/details/79979103
     */
    private static JedisPool jedisPool;

    @Autowired
    public void setJedisPool(JedisPool jedisPool) {
        JedisUtil.jedisPool = jedisPool;
    }

    /**
     * 獲取Jedis例項
     * @param
     * @return redis.clients.jedis.Jedis
     * @author Wang926454
     * @date 2018/9/4 15:47
     */
    public static synchronized Jedis getJedis() {
        try {
            if (jedisPool != null) {
                Jedis resource = jedisPool.getResource();
                return resource;
            } else {
                return null;
            }
        } catch (Exception e) {
            throw new CustomException("釋放Jedis資源異常:" + e.getMessage());
        }
    }

    /**
     * 釋放Jedis資源
     * @param
     * @return void
     * @author Wang926454
     * @date 2018/9/5 9:16
     */
    public static void closePool() {
        try {
            jedisPool.close();
        }catch (Exception e){
            throw new CustomException("釋放Jedis資源異常:" + e.getMessage());
        }
    }

    /**
     * 獲取redis鍵值-object
     * @param key
     * @return java.lang.Object
     * @author Wang926454
     * @date 2018/9/4 15:47
     */
    public static Object getObject(String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            byte[] bytes = jedis.get(key.getBytes());
            if(StringUtil.isNotNull(bytes)) {
                return SerializableUtil.unserializable(bytes);
            }
        } catch (Exception e) {
            throw new CustomException("獲取Redis鍵值getObject方法異常:key=" + key + " cause=" + e.getMessage());
        } finally {
            if(jedis != null) {
                jedis.close();
            }
        }
        return null;
    }

    /**
     * 設定redis鍵值-object
     * @param key
     * @param value
     * @return java.lang.String
     * @author Wang926454
     * @date 2018/9/4 15:49
     */
    public static String setObject(String key, Object value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.set(key.getBytes(), SerializableUtil.serializable(value));
        } catch (Exception e) {
            throw new CustomException("設定Redis鍵值setObject方法異常:key=" + key + " value=" + value + " cause=" + e.getMessage());
        } finally {
            if(jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 設定redis鍵值-object-expiretime
     * @param key
     * @param value
     * @param expiretime
     * @return java.lang.String
     * @author Wang926454
     * @date 2018/9/4 15:50
     */
    public static String setObject(String key, Object value, int expiretime) {
        String result = "";
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            result = jedis.set(key.getBytes(), SerializableUtil.serializable(value));
            if(OK.equals(result)) {
                jedis.expire(key.getBytes(), expiretime);
            }
            return result;
        } catch (Exception e) {
            throw new CustomException("設定Redis鍵值setObject方法異常:key=" + key + " value=" + value + " cause=" + e.getMessage());
        } finally {
            if(jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 獲取redis鍵值-Json
     * @param key
     * @return java.lang.Object
     * @author Wang926454
     * @date 2018/9/4 15:47
     */
    public static String getJson(String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.get(key);
        } catch (Exception e) {
            throw new CustomException("獲取Redis鍵值getJson方法異常:key=" + key + " cause=" + e.getMessage());
        } finally {
            if(jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 設定redis鍵值-Json
     * @param key
     * @param value
     * @return java.lang.String
     * @author Wang926454
     * @date 2018/9/4 15:49
     */
    public static String setJson(String key, String value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.set(key, value);
        } catch (Exception e) {
            throw new CustomException("設定Redis鍵值setJson方法異常:key=" + key + " value=" + value + " cause=" + e.getMessage());
        } finally {
            if(jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 設定redis鍵值-Json-expiretime
     * @param key
     * @param value
     * @param expiretime
     * @return java.lang.String
     * @author Wang926454
     * @date 2018/9/4 15:50
     */
    public static String setJson(String key, String value, int expiretime) {
        String result = "";
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            result = jedis.set(key, value);
            if(OK.equals(result)) {
                jedis.expire(key, expiretime);
            }
            return result;
        } catch (Exception e) {
            throw new CustomException("設定Redis鍵值setJson方法異常:key=" + key + " value=" + value + " cause=" + e.getMessage());
        } finally {
            if(jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 刪除key
     * @param key
     * @return java.lang.Long
     * @author Wang926454
     * @date 2018/9/4 15:50
     */
    public static Long delKey(String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.del(key.getBytes());
        }catch(Exception e) {
            throw new CustomException("刪除Redis的鍵delKey方法異常:key=" + key + " cause=" + e.getMessage());
        }finally{
            if(jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * key是否存在
     * @param key
     * @return java.lang.Boolean
     * @author Wang926454
     * @date 2018/9/4 15:51
     */
    public static Boolean exists(String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.exists(key.getBytes());
        }catch(Exception e) {
            throw new CustomException("查詢Redis的鍵是否存在exists方法異常:key=" + key + " cause=" + e.getMessage());
        }finally{
            if(jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 模糊查詢獲取key集合
     * @param key
     * @return java.util.Set<java.lang.String>
     * @author Wang926454
     * @date 2018/9/6 9:43
     */
    public static Set<String> keysS(String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.keys(key);
        }catch(Exception e) {
            throw new CustomException("模糊查詢Redis的鍵集合keysS方法異常:key=" + key + " cause=" + e.getMessage());
        }finally{
            if(jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 模糊查詢獲取key集合
     * @param key
     * @return java.util.Set<java.lang.String>
     * @author Wang926454
     * @date 2018/9/6 9:43
     */
    public static Set<byte[]> keysB(String key) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return jedis.keys(key.getBytes());
        }catch(Exception e) {
            throw new CustomException("模糊查詢Redis的鍵集合keysB方法異常:key=" + key + " cause=" + e.getMessage());
        }finally{
            if(jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 獲取過期剩餘時間
     * @param key
     * @return java.lang.String
     * @author Wang926454
     * @date 2018/9/11 16:26
     */
    public static Long getExpireTime(String key) {
        Long result = -2L;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            result = jedis.ttl(key);
            return result;
        } catch (Exception e) {
            throw new CustomException("獲取Redis鍵過期剩餘時間getExpireTime方法異常:key=" + key + " cause=" + e.getMessage());
        } finally {
            if(jedis != null) {
                jedis.close();
            }
        }
    }
}
package study.springboot.demo.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Jedis配置,專案啟動注入JedisPool
 * http://www.cnblogs.com/GodHeng/p/9301330.html
 * @author Wang926454
 * @date 2018/9/5 10:35
 */
@Component
@Configuration
@EnableAutoConfiguration
@PropertySource("classpath:application.properties")
@ConfigurationProperties(prefix = "redis")
public class JedisConfig {

    private static Logger logger = LoggerFactory.getLogger(JedisConfig.class);
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.timeout}")
    private int timeout;

    @Value("${spring.redis.jedis.pool.max-active}")
    private int maxActive;

    @Value("${spring.redis.jedis.pool.max-wait}")
    private int maxWait;

    @Value("${spring.redis.jedis.pool.max-idle}")
    private int maxIdle;

    @Value("${spring.redis.jedis.pool.min-idle}")
    private int minIdle;

    @Bean
    public JedisPool redisPoolFactory(){
        try {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxIdle(maxIdle);
            jedisPoolConfig.setMaxWaitMillis(maxWait);
            jedisPoolConfig.setMaxTotal(maxActive);
            jedisPoolConfig.setMinIdle(minIdle);
            // JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
            JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, null);
            logger.info("初始化Redis連線池JedisPool成功!" + " Redis地址: " + host + ":" + port);
            return jedisPool;
        } catch (Exception e) {
            logger.error("初始化Redis連線池JedisPool異常:" + e.getMessage());
        }
        return null;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public int getMaxActive() {
        return maxActive;
    }

    public void setMaxActive(int maxActive) {
        this.maxActive = maxActive;
    }

    public int getMaxWait() {
        return maxWait;
    }

    public void setMaxWait(int maxWait) {
        this.maxWait = maxWait;
    }

    public int getMaxIdle() {
        return maxIdle;
    }

    public void setMaxIdle(int maxIdle) {
        this.maxIdle = maxIdle;
    }

    public int getMinIdle() {
        return minIdle;
    }

    public void setMinIdle(int minIdle) {
        this.minIdle = minIdle;
    }
}

 

package study.springboot.demo.common;


import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;

/**
 * 讓錯誤跳轉到同一頁面
 * 解決spring-boot Whitelabel Error Page
 * @Author wangbiao
 * @Date 2019-08-15 00:07
 * @Decripition TODO
 **/


@Controller
class ErrorPageConfig implements ErrorController {

    @RequestMapping("/error")
    public String handleError(HttpServletRequest request){
        //獲取statusCode:401,404,500
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if(statusCode == 401){
            return "/401";
        }else if(statusCode == 404){
            return "/404";
        }else if(statusCode == 403){
            return "/403";
        }else{
            return "/500";
        }

    }
    @Override
    public String getErrorPath() {
        return "/error";
    }
}

 

package study.springboot.demo.common;

/**
 * @Author wangbiao
 * @Date 2019-08-17 19:34
 * @Decripition TODO
 **/
public class CustomException extends RuntimeException{

    public CustomException(String msg){
        super(msg);
    }

    public CustomException() {
        super();
    }
}
package study.springboot.demo.controller;


import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestParam;
import study.springboot.demo.entity.SysUser;
import study.springboot.demo.service.SysUserService;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.List;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author wangbiao
 * @since 2019-07-29
 */
@Controller
@RequestMapping("/sysUser")
public class SysUserController {
    @Autowired
    private SysUserService sysUserService;



    @RequiresPermissions("user:selectSysUser")
    @RequestMapping("/selectSysUser")
    public String selectSysUser(@RequestParam(value = "username", required = false) String username, Model model){
        Wrapper<SysUser> sysUserWrapper = new EntityWrapper<>();
        if(StringUtils.isNotBlank(username)){
            sysUserWrapper.eq("username",username);
        }
        List<SysUser> sysUserList = sysUserService.selectList(sysUserWrapper);
        model.addAttribute("sysUserList",sysUserList);
        return "userList";
    }

    @RequiresPermissions("user:addSysUser")
    @RequestMapping("/toAddSysUser")
    public String toAddSysUser(){
        return "addUser";
    }


    @RequiresPermissions("user:addSysUser")
    @RequestMapping("/addSysUser")
    public String addSysUser(SysUser sysUser, Model model) throws Exception{
        sysUser.setCreateTime(new Date());
        boolean result = sysUserService.insert(sysUser);
        model.addAttribute("result",result);
        return "redirect:/sysUser/selectSysUser";
    }


    @RequiresPermissions("user:updateSysUser")
    @RequestMapping("/updateSysUser/{id}")
    public String addSysUser(@PathVariable("id") Integer id) throws Exception{
        SysUser sysUser = sysUserService.selectById(id);
        if(sysUser!=null){
            if("1".equals(sysUser.getState())){
                sysUser.setState("-1");
            }else{
                sysUser.setState("1");
            }
            sysUserService.updateById(sysUser);
        }
        return "redirect:/sysUser/selectSysUser";
    }

}

package study.springboot.demo.service;

import study.springboot.demo.entity.SysUser;
import com.baomidou.mybatisplus.service.IService;

/**
 * <p>
 *  服務類
 * </p>
 *
 * @author wangbiao
 * @since 2019-07-29
 */
public interface SysUserService extends IService<SysUser> {

}
package study.springboot.demo.service.impl;

import study.springboot.demo.entity.SysUser;
import study.springboot.demo.dao.SysUserMapper;
import study.springboot.demo.service.SysUserService;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * <p>
 *  服務實現類
 * </p>
 *
 * @author wangbiao
 * @since 2019-07-29
 */
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

}

dao和map.xml檔案我就不貼出來了
下面是html檔案
登入頁面 login.html
 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style type="text/css">
        #tt{
            height:400px;
            width:400px;
            top:50%;
            left:50%;
            position:absolute;
            margin-top:-100px;
            margin-left:-100px;
        }
    </style>
</head>
<body>
<div id="tt">
<form method="post" action="/toLogin">
    <table border="1" cellspacing="0" cellpadding="0">
        <caption align="top">使用者登入</caption>
        <tr><td>使用者名稱</td><td><input name="username" type="text"/></td></tr>
        <tr><td>密碼</td><td><input name="password" type="password"/></td></tr>
        <tr><td><input type="submit" value="登入"/></td>
            <td><input type="checkbox" name="rememberMe" value="true"/>記住我</td>
        </tr>
        <#if error??>
        <font size="3" color="red">${error}</font>
        </#if>
    </table>
</form>
</div>
</body>
</html>

主頁面:index.html
 

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <style type="text/css">
        #tt{
            height:200px;
            width:200px;
            top:50%;
            left:50%;
            position:absolute;
            margin-top:-100px;
            margin-left:-100px;
        }
    </style>
</head>
<body>
<div id="tt">
     <h1> 恭喜登入成功 </h1>
     <a href="http://localhost:8080/sysUser/selectSysUser">檢視使用者列表</a>
     <a href="http://localhost:8080/logout">退出登入</a>
</div>

<ul>
<!-- 使用者沒有身份驗證時顯示相應資訊,即遊客訪問資訊 -->
<@shiro.guest>遊客顯示的資訊</@shiro.guest><br/>
<!-- 使用者已經身份驗證/記住我登入後顯示相應的資訊 -->
<@shiro.user>使用者已經登入過了</@shiro.user><br/>
<!-- 使用者已經身份驗證通過,即Subject.login登入成功,不是記住我登入的 -->
<@shiro.authenticated>不是記住我登入</@shiro.authenticated><br/>
<!-- 顯示使用者身份資訊,通常為登入帳號資訊,預設呼叫Subject.getPrincipal()獲取,即Primary Principal -->
<@shiro.principal></@shiro.principal><br/>
<!--使用者已經身份驗證通過,即沒有呼叫Subject.login進行登入,包括記住我自動登入的也屬於未進行身份驗證,與guest標籤的區別是,該標籤包含已記住使用者 -->
<@shiro.notAuthenticated>已記住使用者</@shiro.notAuthenticated><br/>
<!-- 相當於Subject.getPrincipals().oneByType(String.class) -->
<@shiro.principal type="java.lang.String"/><br/>
<!-- 相當於((User)Subject.getPrincipals()).getUsername() -->
<@shiro.principal property="username"/><br/>
<!-- 如果當前Subject有角色將顯示body體內容 name="角色名" -->
<@shiro.hasRole name="admin">這是admin角色</@shiro.hasRole><br/>
<!-- 如果當前Subject有任意一個角色(或的關係)將顯示body體內容。 name="角色名1,角色名2..." -->
<@shiro.hasAnyRoles name="admin,vip">使用者擁有admin角色 或者 vip角色</@shiro.hasAnyRoles><br/>
<!-- 如果當前Subject沒有角色將顯示body體內容 -->
<@shiro.lacksRole name="admin">如果不是admin角色,顯示內容</@shiro.lacksRole><br/>
<!-- 如果當前Subject有許可權將顯示body體內容 name="許可權名" -->
<@shiro.hasPermission name="userInfo:add">使用者擁有新增許可權</@shiro.hasPermission><br/>
<!-- 如果當前Subject沒有許可權將顯示body體內容 name="許可權名" -->
<@shiro.lacksPermission name="userInfo:add">如果使用者沒有新增許可權,顯示的內容</@shiro.lacksPermission><br/>
</ul>

</body>
</html>

使用者列表頁面:userList.html
 

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">


<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style type="text/css">
        #tt{
            height:400px;
            width:400px;
            top:50%;
            left:50%;
            position:absolute;
            margin-top:-100px;
            margin-left:-100px;
        }
    </style>
</head>
<body>

<div id="tt">
    <@shiro.hasPermission name="user:addSysUser">
    <a href="http://localhost:8080/sysUser/toAddSysUser">新增使用者</a>
    </@shiro.hasPermission>
    &nbsp;&nbsp;<a href="http://localhost:8080/logout">退出登入</a>
    <table border="1" cellspacing="0" cellpadding="0">
        <caption align="top">使用者列表</caption>
        <tr>
            <td>使用者名稱</td><td>真實姓名</td><td>狀態</td><td>建立時間</td><td>操作</td>
        </tr>
        <#list sysUserList as user>
        <tr>
            <td>${user.username}</td>
            <td>${user.name}</td>
            <td>
                <#if user.state=='1' >
                <font color="red"> 禁用</font>
                <#else>
                開啟
            </#if>
            </td>
            <td>${user.createTime?string('yyyy-MM-dd HH:mm:ss')}</td>
            <td>
                <@shiro.hasPermission name="user:updateSysUser">
                   <a href="/sysUser/updateSysUser/${user.uid}">
                       <#if user.state=='1' >
                          開啟
                       <#else>
                          禁用
                       </#if>
                   </a>
               </@shiro.hasPermission>

            </td>
        </tr>
    </#list>
    </table>
</div>
</body>
</html>

新增使用者頁面:addUser.html
 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>新增使用者</title>
    <style type="text/css">
        #tt{
            height:300px;
            width:300px;
            top:50%;
            left:50%;
            position:absolute;
            margin-top:-100px;
            margin-left:-100px;
        }
    </style>
</head>
<body>
<div id="tt">
    <form method="post" action="/sysUser/addSysUser">
        <table border="1" cellspacing="0" cellpadding="0">
            <caption align="top">新增使用者</caption>
            <tr><td>使用者名稱</td><td><input name="username"></td></tr>
            <tr><td>密碼</td><td><input type="password" name="password"></td></tr>
            <tr><td>真實姓名</td><td><input name="name"></td></tr>
            <tr><td>身份證號</td><td><input name="idCardNum"></td></tr>
            <tr><td>狀態</td><td><input type="radio" name="state" value="1">正常
                <input type="radio" name="state" value="-1">禁用
            </td></tr>
            <tr><td><input type="submit" value="提交"></td><td><input type="reset" value="取消"></td></tr>
        </table>
    </form>
</div>
</body>
</html>

404錯誤統一展示頁面:404.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8" />
    <title>Insert title here</title>
</head>
<body>
<h1>對不起,404了,你迷路了</h1>
</body>
</html>

500錯誤統一展示頁面  500.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8" />
    <title>Insert title here</title>
</head>
<body>
<h1>對不起,500了,伍佰來了,唱首挪威森林吧</h1>
</body>
</html>

無許可權統一展示頁面:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8" />
    <title>Insert title here</title>
</head>
<body>
<h1>對不起,您沒有許可權</h1>
</body>
</html>

下面是操作介面:
輸入http://localhost:8080/login ,顯示:

登入完成後跳轉到主頁面:
 張三的賬戶點選檢視使用者列表按鈕跳轉到下面的頁面

 

 shiro其實最直觀的表現就是根據賬戶資訊的不同控制著這個賬戶可以訪問那些url,可以看到頁面上的那些選單和按鈕。
這個工程參考了網上很多其他人的程式碼,但大體框架是自己搭建的,中間踩過很多坑,過程不是一帆風順,但總算完成了基本功能。後面開啟springboot的學習。
原始碼地址:https://pan.baidu.com/s/1HmVPdGURrzJM64KalN92ug


參考部落格:https://blog.csdn.net/qq_34021712/article/details/80294096
                 https://blog.csdn.net/qq_34021712/article/details/80294417

相關文章