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


興趣的朋友可以去了解一下前幾篇,你的贊就是對我最大的支持,感謝大家!

說明:

  • 這一篇的目的還是整合,也就是一個具體的實操體驗,原理性的沒涉及到,我本身也沒有深入研究過,就不獻丑了

  • SpringBoot 起飛之路 系列文章的源碼,均同步上傳到 github 了,有需要的小伙伴,隨意去 down

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

(一) 初識 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

1、目錄結構

├── 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 等模塊就顯示不出來,所以拿一個定義好的管理員身份登陸了
  • 關於如何使其自動切換顯示登陸還是登錄后信息,在后面會講解

1、首頁

2、子頁面

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

3、登陸頁面

4、成功及未授權頁面

我截了個圖,把兩個頁面拼接到一起了,沒啥好說的,就是兩個很普通的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 {
	// 1、ShiroFilterFactoryBean
	// 2、DefaultWebSecurityManager
	// 3、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 {
	// 1、ShiroFilterFactoryBean
	// 2、DefaultWebSecurityManager
    
	// 3、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模塊 就不給予顯示

(七) 結尾

如果文章中有什么不足,歡迎大家留言交流,感謝朋友們的支持!

如果能幫到你的話,那就來關注我吧!如果您更喜歡微信文章的閱讀方式,可以關注我的公眾號

在這里的我們素不相識,卻都在為了自己的夢而努力 ❤

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM