(八) SpringBoot起飛之路-整合Shiro詳細教程(MyBatis、Thymeleaf)

興趣的朋友能夠去了解一下前幾篇,你的贊就是對我最大的支持,感謝你們!css

(一) SpringBoot起飛之路-HelloWorldhtml

(二) SpringBoot起飛之路-入門原理分析前端

(三) SpringBoot起飛之路-YAML配置小結(入門必知必會)java

(四) SpringBoot起飛之路-靜態資源處理mysql

(五) SpringBoot起飛之路-Thymeleaf模板引擎git

(六) SpringBoot起飛之路-整合JdbcTemplate-Druid-MyBatisgithub

(七) SpringBoot起飛之路-整合SpringSecurityweb

說明:spring

  • 這一篇的目的仍是整合,也就是一個具體的實操體驗,原理性的沒涉及到,我自己也沒有深刻研究過,就不獻醜了
  • SpringBoot 起飛之路 系列文章的源碼,均同步上傳到 github 了,有須要的小夥伴,隨意去 downsql

  • 才疏學淺,就會點淺薄的知識,你們權當一篇工具文來看啦,不喜勿憤哈 ~

(一) 初識 Shiro

(1) 引言

權限以及安全問題,雖然並非一個影響到程序、項目運行的必須條件,可是倒是開發中的一項重要考慮因素,例如某些資源咱們不想被訪問到或者咱們某些方法想要知足指定身份才能夠訪問,咱們可使用 AOP 或者過濾器來實現要求,可是實際上,若是代碼涉及的邏輯比較多之後,代碼是極其繁瑣,冗餘的,而有不少開發框架,例如 Spring Security,Shiro,已經爲咱們提供了這種功能,咱們只須要知道如何正確配置以及使用它了

(2) 基本介紹

官網:http://shiro.apache.org/

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.

Apache Shiro™是一個功能強大且易於使用的Java安全框架,可執行身份驗證、受權、加密和會話管理。經過Shiro易於理解的API,您能夠快速、輕鬆地保護任何應用程序——從最小的移動應用程序到最大的web和企業應用程序。

簡單梳理一下:

  • Shiro 和 Spring Security 性質是同樣的,都是一款權限框架,用來保證應用的權限安全問題
  • Shiro 可執行身份驗證、受權、加密和會話管理,Web集成,緩存等
  • Shiro 不只能夠應用到 JavaEE 環境下,甚至 JavaSE 也能夠

(3) 基本功能

這部分的內容,說實話,剛入門簡單掃兩眼就好了,只有你真的敲過一次代碼了,你才大概對其中某些部分能有個印象,再繼續深刻研究纔可能有比較好的掌握

A:官方架構圖

  • Authentication:用戶認證就是指這個用戶身份是否合法,通常咱們的用戶認證就是經過校驗用戶名密碼,來判斷用戶身份的合法性,肯定身份合法後,用戶就能夠訪問該系統
  • Authorization:若是不一樣的用戶須要有不一樣等級的權限,就涉及到用戶受權,用戶受權就是對用戶能訪問的資源,所能執行的操做進行控制,根據不一樣用戶角色或者對應不一樣權限來劃分不一樣的權限
  • SessionManager:Shior 官網說其提供了一個完整的會話管理解決方案, 它的所會話能夠是普通的Java SE環境, 也能夠是Web環境,不過我有點思惟定式了,仍是用習慣的方式,這塊沒怎麼研究
  • Cryptography:加密明文密碼, 保護數據安全
  • WebSupport:字面意思,其對Web的支持, 使得其能夠很是容易的集成到Web環境;
  • Caching:緩存, 好比用戶登陸後, 其用戶信息, 擁有的角色、權限沒必要每次去查,效率上會好一點
  • Concurrency:Shiro 支持多線程應用的併發驗證,即,如在一個線程中開啓另外一個線程,能把權限自動傳過去
  • Testing:沒什麼好說的,就是支持測試
  • Run As:容許一個用戶僞裝爲另外一個用戶(容許的條件下) 的身份進行訪問資源請求
  • Remember Me:它也有,記住我這個功能

B:三大核心組件

Shiro框架中有三個核心組件:Subject ,SecurityManager和Realms

  1. Subject 是一個安全術語,表明認證主體,通常來講能夠簡單的理解爲,當前操做的用戶,不過用戶這個概念實際上也不是很準確,由於 Subject 實際上不必定是人,也能夠是一些例如第三方進程或者定時做業等等的事物,也就是理解爲,當前同軟件交互的事物。

    • 每個Subject對象都必須被 SecurityManager 進行管理
  2. Subject 接受 SecurityManager 的管理,由於 SecurityManager 管理全部用戶的安全操做,其內部引用了不少安全相關的組件,可是都不對外開放,開發人員更多的是使用 Subject
  3. Realms 這個概念也是重要的,其能夠理解爲 Shiro 與 數據之間的溝通器與中間橋樑認證受權時,就會去此部分找一些內容,從本質上 Realm 就是一個通過了大量封裝的安全 Dao

(4) 用戶|角色|權限的概念

既然 Shiro 是一個安全權限技術,簡單來講,就是對程序中被訪問的資源或者請求進行必定程度的控制,而如何劃分就涉及到這三個概念:用戶、角色、權限

用戶(User):沒啥好說的,表明當前 Subject 認證主體,例如某些內容必須用戶登陸後才能夠訪問

角色(Role):這表明用戶擔任的角色,身份,一個角色能夠有多個權限,例如這一塊只有管理員能夠訪問

權限(Permission):也就是操做資源的具體的權利,例如對數據進行添加、修改、刪除、查看操做

補充:其實能夠簡單的理解,角色就是一些權限的集合組成的,正是這一堆權限已經將這個角色能作的事情限定死了,不用每次都說明這個角色能夠作什麼

(二) 靜態頁面導入 And 頁面環境搭建

(1) 關於靜態頁面

A:頁面介紹

頁面是我本身臨時弄得,有須要的朋友能夠去我 GitHub:ideal-20 下載源碼,簡單說明一下這個頁面

作一個靜態頁面若是嫌麻煩,也能夠單純的本身建立一些簡單的頁面,寫幾個標題文字,能體現出當前是哪一個頁面就行了

我代碼中用的這些頁面,就是拿開源的前端組件框架進行了一點的美化,而後方便講解一些功能,頁面模板主要是配合 Thymeleaf

一、目錄結構

├── index.html                        // 首頁
├── images                            // 首頁圖片,僅美觀,無實際做用
├── css                               
├── js                                
├── views                             // 總子頁面文件夾,權限驗證的關鍵頁面
│   ├── login.html                      // 登陸頁面
│   ├── success.html                  // 成功頁面
│   ├── unauthorized.html              // 未受權頁面:此部分未受權的用戶訪問資源,跳轉到此頁面
│   ├── L-A                              // L-A 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|    ├── L-B                              // L-B 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|    ├── L-C                              // L-C 子頁面文件夾,下含 a b c 三個子頁面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html

B:導入到項目

主要就是把基本一些連接,引入什麼的先替換成 Thymeleaf 的標籤格式,這裏語法用的不是特別多,即便對於 Thymeleaf 不是很熟悉也是很容易看懂的,固然若是仍然感受有點吃力,能夠單純的作成 html,將就一下,或者去看一下我之前的文章哈,裏面有關於 Thymeleaf 入門的講解

css、image、js 放到 resources --> static 下 ,views 和 index.html 放到 resources --> templates下

(2) 環境搭建

A:引入依賴

這一部分引入也好,初始化項目的時候,勾選好自動生成也好,只要依賴正常導入了便可

  • 引入 Spring Security 模塊
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.3</version>
</dependency>

關鍵的依賴主要就是上面這個啓動器,可是還有一些就是常規或者補充的了,例如 web、thymeleaf、devtools 等等,還有一些例如 Mybatis 等我都放進來了,下面的依賴基本已經全了,具體講到某塊,具體再說

thymeleaf-extras-shiro 這個後面講解中會提到,是用來配合 Thymeleaf 整合 Shiro 的

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>

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

    <dependency>
       <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
         <optional>true</optional>
   </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

B:頁面跳轉 Controller

由於咱們用了模板,頁面的跳轉就須要交給 Controller 了,很簡單,首先是首頁的,固然關於頁面這個就無所謂了,我隨便跳轉到了個人博客,接着還有登陸頁面、成功,未受權頁面的跳轉

有一個小 Tip 須要提一下,由於 L-A、L-B、L-C 文件夾下都有3個頁面 a.html 、b.html 、c.html,因此能夠利用 @PathVariable 寫一個較爲通用的跳轉方法

@Controller
public class PageController {

    @RequestMapping({"/", "index"})
    public String index() {
        return "index";
    }

    @RequestMapping("/about")
    public String toAboutPage() {
        return "redirect:http://www.ideal-20.cn";
    }

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

    @RequestMapping("/levelA/{name}")
    public String toLevelAPage(@PathVariable("name") String name) {
        return "views/L-A/" + name;
    }

    @RequestMapping("/levelB/{name}")
    public String toLevelBPage(@PathVariable("name") String name) {
        return "views/L-B/" + name;
    }

    @RequestMapping("/levelC/{name}")
    public String toLevelCPage(@PathVariable("name") String name) {
        return "views/L-C/" + name;
    }
    
    @RequestMapping("/unauthorized")
    public String toUnauthorizedPage() {
        return "views/unauthorized";
    }

    @RequestMapping("/success")
    public String toSuccessPage() {
        return "views/success";
    }
}

C:環境搭建最終效果

  • 爲了貼圖方便,我把頁面拉窄了一點
  • 首頁右上角應該爲登陸的連接,這裏是由於,我運行的是已經寫好的代碼,不登陸頁面例如 L-A-a 等模塊就顯示不出來,因此拿一個定義好的管理員身份登錄了
  • 關於如何使其自動切換顯示登錄仍是登陸後信息,在後面會講解

一、首頁

二、子頁面

L-A、L-B、L-C 下的 a.html 、b.html 、c.html 都是同樣的,只是文字有一點變化

三、登錄頁面

四、成功及未受權頁面

我截了個圖,把兩個頁面拼接到一塊兒了,沒啥好說的,就是兩個很普通的H5頁面

(三) 建立數據庫及實體

(1) 建立數據庫以及表

-- ----------------------------
-- Table structure for role
-- ----------------------------
CREATE TABLE `role`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色表主鍵',
  `role_name` varchar(32) DEFAULT NULL COMMENT '角色名稱',
  PRIMARY KEY (`id`)
);

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'SUPER_ADMIN');
INSERT INTO `role` VALUES (2, 'ADMIN');
INSERT INTO `role` VALUES (3, 'USER');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用戶主鍵',
  `username` varchar(32) NOT NULL COMMENT '用戶名',
  `password` varchar(32) NOT NULL COMMENT '密碼',
  `role_id` int(11) DEFAULT NULL COMMENT '與role角色表聯繫的外鍵',
  PRIMARY KEY (`id`),
  CONSTRAINT `user_role_on_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
);

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'BWH_Steven', '666666', 1);
INSERT INTO `user` VALUES (2, 'admin', '666666', 2);
INSERT INTO `user` VALUES (3, 'zhangsan', '666666', 3);

-- ----------------------------
-- Table structure for permission
-- ----------------------------
CREATE TABLE `permission`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '權限表主鍵',
  `permission_name` varchar(50) NOT NULL COMMENT '權限名',
  `role_id` int(11) DEFAULT NULL COMMENT '與role角色表聯繫的外鍵',
  PRIMARY KEY (`id`),
  CONSTRAINT `permission_role_on_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
);

-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES (1, 'user:*', 1);
INSERT INTO `permission` VALUES (2, 'user:*', 2);
INSERT INTO `permission` VALUES (3, 'user:queryAll', 3);

(2) 實體

在數據庫中角色表,在用戶表和權限表分別是有一個外鍵的概念,因此在實體中就寫成了引用的形式

角色類

@Data
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Role {
    private int id;
    private String roleName;
}

用戶類,說明:因爲我在其餘模塊下有一些同名的類,調用的時候常常會有一些誤會,因此就稍微改了下名字 --> UserPojo,這裏你們起 User 就 OK

@Data
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class UserPojo {
    private int id;
    private String username;
    private String password;
    private Role role;
}

權限類

@Data
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Permission {
    private Integer id;
    private String permissionName;
    private Role role;
}

(四) 整合 MyBatis

今天要作的內容,實際上本身隨便模擬兩個數據也是能夠的,不過爲了貼近現實,仍是引入了 Mybaits

(1) 引入依賴及進行配置

先引入 MyBatis 依賴,還有驅動依賴

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

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

鏈接池啥的就不折騰了,想本身換就本身配置一下哈

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_shiro_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  type-aliases-package: cn.ideal.pojo

server:
  port: 8080

具體的 Mapper 這裏還沒寫,講解的過程當中,按照流須要,再寫上去

(2) 編寫 Mapper

由於代碼是在文章以前寫好的,咱們在後面會用到利用 username 進行查詢用戶和權限的方法,因此,咱們就按這樣寫就行了

@Mapper
public interface UserMapper {
    UserPojo queryUserByUsername(@Param("username") String username);

    Permission queryPermissionByUsername(@Param("username") String username);
}

具體的 XML 配置 sql

這部分涉及到多表的一個稍複雜的查詢,若是感受有點吃力,能夠去回顧一下前面的知識,或者乾脆無論也能夠,接着看後面的,純瞭解 Shiro 也能夠

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.ideal.mapper.UserMapper">

    <!-- 定義封裝 User和 role 的 resultMap -->
    <resultMap id="userRoleMap" type="cn.ideal.pojo.UserPojo">
        <id property="id" column="id"/>
        <result property="username" column="username"></result>
        <result property="password" column="password"></result>
        <!-- 配置封裝 UserPojo 的內容 -->
        <association property="role" javaType="cn.ideal.pojo.Role">
            <id property="id" column="id"></id>
            <result property="roleName" column="role_name"></result>
        </association>
    </resultMap>

    <!-- 定義封裝 permission 和 role 的 resultMap -->
    <resultMap id="permissionRoleMap" type="cn.ideal.pojo.Permission">
        <id property="id" column="id"/>
        <result property="permissionName" column="permission_name"></result>
        <!-- 配置封裝 Role 的內容 -->
        <association property="role" javaType="cn.ideal.pojo.Role">
            <id property="id" column="id"></id>
            <result property="roleName" column="role_name"></result>
        </association>
    </resultMap>

    <select id="queryUserByUsername" resultMap="userRoleMap">
        SELECT u.*,r.role_name FROM `user` u, `role` r
          WHERE username = #{username} AND u.role_id = r.id;
    </select>

    <select id="queryPermissionByUsername" resultMap="permissionRoleMap">
        SELECT p.* ,r.role_name FROM `user` u, `role` r, `permission` p
          WHERE username = #{username} AND u.role_id = r.id AND p.role_id = r.id;
    </select>

</mapper>

(3) 代碼測試

@SpringBootTest
class Springboot13ShiroMybatisApplicationTests {

    @Autowired
    private UserMapper userMapper;
    
    @Test
    void contextLoads() {
        UserPojo admin = userMapper.queryUserByUsername("admin");
        System.out.println(admin.toString());
        Permission permission = userMapper.queryPermissionByUsername("admin");
        System.out.println(permission.toString());
    }
}

(五) Spring Boot 整合 Shiro

(1) 自定義認證和受權(Realm)

首先咱們須要建立Shiro的配置類,在config包下建立一個名爲 ShiroConfig 的配置類

@Configuration
public class ShiroConfig {
    // 一、ShiroFilterFactoryBean
    // 二、DefaultWebSecurityManager
    // 三、Realm 對象(自定義)
}

上面註釋能夠看出,咱們須要在配置類中建立這樣幾個內容,因爲他們幾個之間存在關聯,例如在 Manager 中關聯本身建立的 Realm,在最上面的過濾器,又關聯了中間這個 Manager,因此咱們選擇倒着寫,先寫後面的(也就是被引用最先的 Realm),這樣就能夠一層一層的在前面引用後面已經寫好的,會更舒服一些

首先,在 ShiroConfig 配置類中編寫一個方法用來獲取 Realm ,直接返回一個實例化的 userRealm() 就能夠了

/**
 * 建立 realm 對象,須要本身定義
 *
 * @return
 */
@Bean
public UserRealm userRealm() {
    return new UserRealm();
}

具體內容,咱們須要建立一個新的類來定義

咱們自定義了一個 UserRealm類,同時繼承 AuthorizingRealm 類,接着就須要實現兩個方法:

  • doGetAuthenticationInfo() 認證方法:查看用戶是否能經過認證,可簡單理解爲登陸是否成功
  • doGetAuthorizationInfo() 受權方法:給當前已經登陸成功的用戶劃分權限以及分配角色

根據上面的介紹也很好理解,確定是認證先行,接着纔會執行受權方法,因此咱們先來編寫認證的代碼

A:認證

認證首先就要先獲取到咱們前臺傳來的數據,這塊很顯然,交給 Controller 來作,咱們先來完成這個內容,再回來編寫認證

說明:獲取前臺的數據就是下面的 login 方法,同時在其中調用了認證的方法,其餘幾個方法,只是爲了後期演示的時候使用,一塊給出來了,同時下面登陸方法中我捕獲了全部異常,你們能夠本身更細緻的劃分,同時因爲爲了演示重點,我前臺沒有作太多的處理,例如session中傳入一些登陸失敗等的字符串,徹底不寫也是能夠的哈

@Controller
public class UserController {
    @RequestMapping("/user/queryAll")
    @ResponseBody
    public String queryAll() {
        return "這是 user/queryAll 方法";
    }

    @RequestMapping("/user/admin/add")
    @ResponseBody
    public String adminAdd() {
        return "這是 user/adminAdd 方法";
    }

    @RequestMapping("/login")
    public String login(String username, String password, HttpServletRequest request) {
        // 因爲是根據name參數獲取的,我這裏封裝了一下
        UserPojo user = new UserPojo();
        user.setUsername(username);
        user.setPassword(password);
        // 建立出一個 Token 內容本質基於前臺的用戶名和密碼(不必定正確)
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 獲取 subject 認證主體(這裏也就是如今登陸的用戶)
        Subject subject = SecurityUtils.getSubject();
        try{
            // 認證開始,這裏會跳轉到自定義的 UserRealm 中
            subject.login(token);
            // 能夠存儲到 session 中
            request.getSession().setAttribute("user", user);
            return "views/success";
        }catch(Exception e){
            // 捕獲異常
            e.printStackTrace();
            request.getSession().setAttribute("user", user);
            request.setAttribute("errorMsg", "兄弟,用戶名或密碼錯誤");
            return "views/login";
        }
    }
}

UserRealm 下的認證方法:

說明:經過方法參數中的 token 就能夠獲取到咱們剛纔的那個 token信息,最方便的方法就是下面,直接經過 getPrincipal() 獲取到用戶名(Object 轉 String),還有一種方法就是,將 Token 強轉了 UsernamePasswordToken 類型,接着須要用戶名或者密碼等信息均可以經過 getxxx 的方法獲取到

能夠看到,咱們只須要將數據庫中查詢到的數據交給 Shiro 去作認證就能夠了,具體細節都被封裝了

補充:userService.queryUserByUsername(username) 方法只是調用返回了 UserMapper 中根據用戶名查詢用戶信息的方法,只是爲告終構完整,沒涉及任何業務,若是不清楚,能夠去 GitHub 看一下源碼

/**
 * 認證
 *
 * @param authenticationToken
 * @return
 * @throws AuthenticationException
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    // 根據在接受前臺數據建立的 Token 獲取用戶名
    String username = (String) authenticationToken.getPrincipal();
    //  UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;
    //  System.out.println(userToken.getPrincipal());
    //  System.out.println(userToken.getUsername());
    //  System.out.println(userToken.getPassword());
    
    // 經過用戶名查詢相關的用戶信息(實體)
    UserPojo user = userService.queryUserByUsername(username);
    if (user != null) {
        // 存入 Session,可選
        SecurityUtils.getSubject().getSession().setAttribute("user", user);
        // 密碼認證的工做,Shiro 來作
        AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), "userRealm");
        return authenticationInfo;
     } else {
        // 返回 null 即會拋異常
        return null;
     }
}

B:受權

受權,也就是在用戶認證後,來設置用戶的權限或者角色信息,這裏主要是獲取到用戶名之後,經過 service 中調用 mapper 接着根據用戶名查詢用戶或者權限,因爲返回的是用戶或者權限實體對象,因此配合 getxxx等方法就能夠獲取到須要的值了

固然了,最主要的仍是根據本身 mapper 以及表的返回狀況設置,這裏只要能獲取到角色以及權限信息(這裏是 String 類型)就能夠了,若是是多個角色,就要使用 setRoles() 方法了,具體須要能夠看參數和返回值,或者查閱文檔,這裏演示都是單個的

/**
 * 受權
 *
 * @param principalCollection
 * @return
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    // 獲取用戶名信息
    String username = (String) principalCollection.getPrimaryPrincipal();
    // 建立一個簡單受權驗證信息
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    // 給這個用戶設置從 role 表獲取到的角色信息
    authorizationInfo.addRole(userService.queryUserByUsername(username).getRole().getRoleName());
    //給這個用戶設置從 permission 表獲取的權限信息
    authorizationInfo.addStringPermission(userService.queryPermissionByUsername(username).getPermissionName());
    return authorizationInfo;
}

(2) Shiro 配置

受權和配置就寫好了,也就是說 Realm 完事了,一個大頭內容完成了,咱們接着就能夠回到 Shiro 的配置中去了,繼續倒着寫,開始寫關於第二點 Manager 的內容

@Configuration
public class ShiroConfig {
    // 一、ShiroFilterFactoryBean
    // 二、DefaultWebSecurityManager
    
    // 三、Realm 對象(自定義)
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }
}

A:配置安全管理器

接着就來配置安全管理器(SecurityManager),這裏就須要將剛纔寫好的 Realm 引入進來,這樣 Shiro 就能夠訪問 Realm 了,而後接着返回

/**
 * 配置安全管理器 SecurityManager
 *
 * @return
 */
 @Bean
 public DefaultWebSecurityManager securityManager() {
    // 將自定義 Realm 加進來
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 關聯 Realm
    securityManager.setRealm(userRealm());
    return securityManager;
}

若是,setRealm 的時候直接調用下面的 userRealm() 出現了問題,那麼能夠考慮在方法參數中配合 @Qualifier 使用,它會自動去找下面 public UserRealm userRealm() 方法的方法名 userRealm,userRealm 中的註解不指定name也行,這裏只是爲了讓你們看得更明白

@Bean
public DefaultWebSecurityManager securityManager(@Qualifier("userRealm") UserRealm userRealm) {
    // 將自定義 Realm 加進來
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 關聯 Realm
    securityManager.setRealm(userRealm);
    return securityManager;
}

@Bean(name="userRealm")
public UserRealm userRealm() {
    return new UserRealm();
}

B:配置過濾器

這又是一個關鍵的地方,首先建立一個 ShiroFilterFactoryBean 確定是毋庸置疑的,最後畢竟要返回這個對象,首先就是將剛纔的 securityManager 關聯進來了,也就是說層層調用,最終把 Realm 關聯過來了,接着要寫的就是重頭戲了,咱們接着須要設置一些本身定義的內容

  • 自定義登陸頁面
  • 成功頁面
  • 未受權界面
  • 一個自定義的 Map 用來存儲須要放行或者攔截的請求
  • 註銷頁面

重點說一下攔截放行(Map)這塊:經過 map 鍵值對的形式存儲,key 存儲 URL ,value 存儲對應的一些權限或者角色等等,其實 key 這塊仍是很好理解的,例如 :/css/** /user/admin/** 分別表明 css 文件夾下的全部文件,以及請求路徑前綴爲 /user/admin/ URL,而對應的 value 就有必定的規範了

關鍵:

  • anon:無需認證,便可訪問,也就是遊客也能夠訪問
  • authc:必須認證,才能訪問,也就是例如須要登陸後
  • roles[xxx] :好比擁有某種角色身份才能訪問 ,注:xxx爲角色參數
  • perms[xxx]:必須擁有對某個請求、資源的相關權限才能訪問,注:xxx爲權限參數

補充:

  • user:必須使用【記住我】這個功能才能訪問
  • logout:註銷,執行後跳轉到設置好的登陸頁面去
/**
 * 配置 Shiro 過濾器
 *
 * @param securityManager
 * @return
 */
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
    // 定義 shiroFactoryBean
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

    // 關聯 securityManager
    shiroFilterFactoryBean.setSecurityManager(securityManager);

    // 自定義登陸頁面,若是登陸的時候,就會執行這個請求,即跳轉到登陸頁
    shiroFilterFactoryBean.setLoginUrl("/toLoginPage");
    // 指定成功頁面
     shiroFilterFactoryBean.setSuccessUrl("/success");
    // 指定未受權界面
    shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

    // LinkedHashMap 是有序的,進行順序攔截器配置
    Map<String, String> filterChainMap = new LinkedHashMap<>();

    // 配置能夠匿名訪問的地址,能夠根據實際狀況本身添加,放行一些靜態資源等,anon 表示放行
    filterChainMap.put("/css/**", "anon");
    filterChainMap.put("/img/**", "anon");
    filterChainMap.put("/js/**", "anon");
    // 指定頁面放行,例如登陸頁面容許全部人登陸
    filterChainMap.put("/toLoginPage", "anon");

    // 以「/user/admin」 開頭的用戶須要身份認證,authc 表示要進行身份認證
    filterChainMap.put("/user/admin/**", "authc");

    filterChainMap.put("/levelA/**", "roles[USER]");
    filterChainMap.put("/levelB/**", "roles[ADMIN]");
    filterChainMap.put("/levelC/**", "roles[SUPER_ADMIN]");

    // /user/admin/ 下的全部請求都要通過權限認證,只有權限爲 user:[*] 的能夠訪問,也能夠具體設置到 user:xxx
    filterChainMap.put("/user/admin/**", "perms[user:*]");

    // 配置註銷過濾器
    filterChainMap.put("/logout", "logout");

    // 將Map 存入過濾器
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
    return shiroFilterFactoryBean;
}

C:解決多身份問題

其實上面的內容已經基本健全了,可是還有一個很棘手的問題,那就是,例如我主頁中的三個模塊,超級管理員A、B、C均可以訪問,管理員能訪問 A 和 B,而登陸後的普通用戶只能訪問 A,如何寫呢?是否是像下面這樣呢?

filterChainMap.put("/levelA/**", "roles[USER,ADMIN,SUPER_ADMIN]");
filterChainMap.put("/levelB/**", "roles[ADMIN,SUPER_ADMIN]");
filterChainMap.put("/levelC/**", "roles[SUPER_ADMIN]");

可是你一用,確定會發現問題,咱們來看一下關於 Role相關的過濾器代碼,很顯然關於 Role 的驗證居然是經過 hasAllRoles 實現的,也就是說,咱們要知足全部的身份才能訪問,不能達到,任選其一便可的效果

/**
 * Filter that allows access if the current user has the roles specified by the mapped value, or denies access
 * if the user does not have all of the roles specified.
 *
 * @since 0.9
 */
public class RolesAuthorizationFilter extends AuthorizationFilter {

    //TODO - complete JavaDoc

    @SuppressWarnings({"unchecked"})
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {

        Subject subject = getSubject(request, response);
        String[] rolesArray = (String[]) mappedValue;

        if (rolesArray == null || rolesArray.length == 0) {
            //no roles specified, so nothing to check - allow access.
            return true;
        }

        Set<String> roles = CollectionUtils.asSet(rolesArray);
        return subject.hasAllRoles(roles);
    }

}

自定義一個 Fileter,從新定義關於 Role 的驗證方式,改爲 hasRole 的方式

public class MyRolesAuthorizationFilter extends AuthorizationFilter {

    @SuppressWarnings({"unchecked"})
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {

        Subject subject = getSubject(request, response);
        String[] rolesArray = (String[]) mappedValue;

        if (rolesArray == null || rolesArray.length == 0) {
            return false;
        }

        List<String> roles = CollectionUtils.asList(rolesArray);
        boolean[] hasRoles = subject.hasRoles(roles);
        for (boolean hasRole : hasRoles) {
            if (hasRole) {
                return true;
            }
        }
        return false;
    }
}

有了這個從新修改了規則的角色過濾器,咱們就能夠繼續回到配置中去,經過下面三行代碼就能夠講這個新的規則的過濾器設置進去

// 設置自定義 filter
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("anyRoleFilter", new MyRolesAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filterMap);

天然,原來相應的Map定義就要變化了,配合自定義過濾器,改爲多個角色的的形式

// 頁面 -用戶須要角色認證
filterChainMap.put("/levelA/**", "anyRoleFilter[USER,ADMIN,SUPER_ADMIN]");
filterChainMap.put("/levelB/**", "anyRoleFilter[ADMIN,SUPER_ADMIN]");
filterChainMap.put("/levelC/**", "anyRoleFilter[SUPER_ADMIN]");

(六) Shiro 整合 Thymeleaf

主要內容已經結束了,不過由於在前面 Spring Security 中,講過如何搭配 Thymeleaf 使用,因此接着補充一點關於如何用 Shiro 配合 Thymeleaf 的方法

A:引入

首先引入二者整合的依賴:

<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

這個版本已是最新的了(仍是很舊)2016年,具體能夠去 maven repository 官網中查一下

注:這個依賴須要 thymeleaf 是 3.0 的版本,咱們的 Springboot 是用的最新的啓動器,天然是 3.0 不過仍是提一下

接着在 Shiro 的主配置 ShiroConfig 類中加入這樣的代碼,這樣,咱們就能夠在 thymeleaf 中使用 Shiro 的自定義標籤

/**
 * 整合 thymeleaf
 * @return
 */
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
    return new ShiroDialect();
}

B:修改頁面

操做結束後,咱們就能夠開始修改頁面了,首先引入頭部約束 xmlns:shiro="http://www.pollix.at/thymeleaf/shiro「

<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

這裏解決的問題,主要是登陸先後,頂部導航欄的一個顯示問題,例如登陸前就應該顯示登錄,登陸後,就顯示用戶名和註銷,若是須要更多的信息,我就建議存到 session ,這裏我是直接使用 shiro:principal 標籤獲取的用戶名

<div>
   <!-- 這裏表明別的代碼,下面只是節選 -->
    
   <!--登陸註銷-->
    <div class="right menu">
      <!--若是未登陸-->
      <!--<div shiro:authorize="!isAuthenticated()">-->
      <div shiro:notAuthenticated="">
        <a class="item" th:href="@{/toLoginPage}">
          <i class="address card icon"></i> 登陸
        </a>
      </div>

      <!--若是已登陸-->
      <div shiro:authenticated="">
        <a class="item">
          <i class="address card icon"></i>
          用戶名:<span shiro:principal></span>
          <!--角色:<span sec:authentication="principal.authorities"></span>-->
        </a>
      </div>

      <div shiro:authenticated="">
        <a class="item" th:href="@{/logout}">
          <i class="address card icon"></i> 註銷
        </a>
      </div>
    </div> 
</div>

下面就是用來只顯示對應模塊的,例如用戶登陸就只有 A能夠訪問,因此 B 和 C模塊 就不給他顯示了,反正這個模塊他也不能訪問

<div class="ui stackable three column grid">
    <div class="column" shiro:hasAnyRoles="USER,ADMIN,SUPER_ADMIN">
      <div class="ui raised segments">
        <div class="ui segment">
          <a th:href="@{/levelA/a}">L-A-a</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelA/b}">L-A-b</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelA/c}">L-A-c</a>
        </div>
      </div>
    </div>
    <div class="column" shiro:hasAnyRoles="ADMIN,SUPER_ADMIN">
      <div class="ui raised segments">
        <div class="ui segment">
          <a th:href="@{/levelB/a}">L-B-a</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelB/b}">L-B-b</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelB/c}">L-B-c</a>
        </div>
      </div>
    </div>
    <div class="column" shiro:hasRole="SUPER_ADMIN">
      <div class="ui raised segments">
        <div class="ui segment">
          <a th:href="@{/levelC/a}">L-C-a</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelC/b}">L-C-b</a>
        </div>
        <div class="ui segment">
          <a th:href="@{/levelC/c}">L-C-c</a>
        </div>
      </div>
    </div>
  </div>

C:看一下效果

普通管理員登陸後,顯示帳號和註銷,同時只有超級管理員才能訪問的 C模塊 就不給予顯示

(七) 結尾

若是文章中有什麼不足,歡迎你們留言交流,感謝朋友們的支持!

若是能幫到你的話,那就來關注我吧!若是您更喜歡微信文章的閱讀方式,能夠關注個人公衆號

在這裏的咱們素不相識,卻都在爲了本身的夢而努力 ❤

一個堅持推送原創開發技術文章的公衆號:理想二旬不止

相關文章
相關標籤/搜索