1. 程式人生 > 實用技巧 >(八) SpringBoot起飛之路-整合Shiro詳細教程(MyBatis、Thymeleaf)

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

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

(一) SpringBoot起飛之路-HelloWorld

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

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

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

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

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

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

說明:

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

  • 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模組 就不給予顯示

(七) 結尾

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

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

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

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