springboot+shiro整合

 

本篇博客主要是學習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

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