Shiro安全框架【快速入門】就這一篇!

我沒有三顆心臟發表於2019-01-06

Shiro安全框架【快速入門】就這一篇!

Shiro 簡介

照例又去官網扒了扒介紹:

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,您可以快速、輕鬆地獲得任何應用程式——從最小的移動應用程式到最大的網路和企業應用程式。

簡而言之,Apache Shiro 是一個強大靈活的開源安全框架,可以完全處理身份驗證、授權、加密和會話管理。

Shiro能到底能做些什麼呢?

  • 驗證使用者身份
  • 使用者訪問許可權控制,比如:1、判斷使用者是否分配了一定的安全形色。2、判斷使用者是否被授予完成某個操作的許可權
  • 在非 Web 或 EJB 容器的環境下可以任意使用Session API
  • 可以響應認證、訪問控制,或者 Session 生命週期中發生的事件
  • 可將一個或以上使用者安全資料來源資料組合成一個複合的使用者 “view”(檢視)
  • 支援單點登入(SSO)功能
  • 支援提供“Remember Me”服務,獲取使用者關聯資訊而無需登入
    ···

為什麼是 Shiro?

使用 Shiro 官方給了許多令人信服的原因,因為 Shiro 具有以下幾個特點:

  • 易於使用——易用性是專案的最終目標。應用程式安全非常令人困惑和沮喪,被認為是“不可避免的災難”。如果你讓它簡化到新手都可以使用它,它就將不再是一種痛苦了。
  • 全面——沒有其他安全框架的寬度範圍可以同Apache Shiro一樣,它可以成為你的“一站式”為您的安全需求提供保障。
  • 靈活——Apache Shiro可以在任何應用程式環境中工作。雖然在網路工作、EJB和IoC環境中可能並不需要它。但Shiro的授權也沒有任何規範,甚至沒有許多依賴關係。
  • Web支援——Apache Shiro擁有令人興奮的web應用程式支援,允許您基於應用程式的url建立靈活的安全策略和網路協議(例如REST),同時還提供一組JSP庫控制頁面輸出。
  • 低耦合——Shiro乾淨的API和設計模式使它容易與許多其他框架和應用程式整合。你會看到Shiro無縫地整合Spring這樣的框架, 以及Grails, Wicket, Tapestry, Mule, Apache Camel, Vaadin…等。
  • 被廣泛支援——Apache Shiro是Apache軟體基金會的一部分。專案開發和使用者組都有友好的網民願意幫助。這樣的商業公司如果需要Katasoft還提供專業的支援和服務。

有興趣的可以去仔細看看官方的文件:【傳送門】

Apache Shiro Features 特性

Apache Shiro是一個全面的、蘊含豐富功能的安全框架。下圖為描述Shiro功能的框架圖:

Shiro安全框架【快速入門】就這一篇!

Authentication(認證), Authorization(授權), Session Management(會話管理), Cryptography(加密)被 Shiro 框架的開發團隊稱之為應用安全的四大基石。那麼就讓我們來看看它們吧:

  • Authentication(認證):使用者身份識別,通常被稱為使用者“登入”
  • Authorization(授權):訪問控制。比如某個使用者是否具有某個操作的使用許可權。
  • Session Management(會話管理):特定於使用者的會話管理,甚至在非web 或 EJB 應用程式。
  • Cryptography(加密):在對資料來源使用加密演算法加密的同時,保證易於使用。

還有其他的功能來支援和加強這些不同應用環境下安全領域的關注點。特別是對以下的功能支援:

  • Web支援:Shiro的Web支援API有助於保護Web應用程式。
  • 快取:快取是Apache Shiro API中的第一級,以確保安全操作保持快速和高效。
  • 併發性:Apache Shiro支援具有併發功能的多執行緒應用程式。
  • 測試:存在測試支援,可幫助您編寫單元測試和整合測試,並確保程式碼按預期得到保障。
  • “執行方式”:允許使用者承擔另一個使用者的身份(如果允許)的功能,有時在管理方案中很有用。
  • “記住我”:記住使用者在會話中的身份,所以使用者只需要強制登入即可。

注意: Shiro不會去維護使用者、維護許可權,這些需要我們自己去設計/提供,然後通過相應的介面注入給Shiro

High-Level Overview 高階概述

在概念層,Shiro 架構包含三個主要的理念:Subject,SecurityManager和 Realm。下面的圖展示了這些元件如何相互作用,我們將在下面依次對其進行描述。

Shiro安全框架【快速入門】就這一篇!

  • Subject:當前使用者,Subject 可以是一個人,但也可以是第三方服務、守護程式帳戶、時鐘守護任務或者其它–當前和軟體互動的任何事件。
  • SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架構的核心,配合內部安全元件共同組成安全傘。
  • Realms:用於進行許可權資訊的驗證,我們自己實現。Realm 本質上是一個特定的安全 DAO:它封裝與資料來源連線的細節,得到Shiro 所需的相關的資料。在配置 Shiro 的時候,你必須指定至少一個Realm 來實現認證(authentication)和/或授權(authorization)。

我們需要實現Realms的Authentication 和 Authorization。其中 Authentication 是用來驗證使用者身份,Authorization 是授權訪問控制,用於對使用者進行的操作授權,證明該使用者是否允許進行當前操作,如訪問某個連結,某個資原始檔等。

Shiro 認證過程

Shiro安全框架【快速入門】就這一篇!

上圖展示了 Shiro 認證的一個重要的過程,為了加深我們的印象,我們來自己動手來寫一個例子,來驗證一下,首先我們新建一個Maven工程,然後在pom.xml中引入相關依賴:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

新建一個【AuthenticationTest】測試類:

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;

public class AuthenticationTest {

    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

    @Before // 在方法開始前新增一個使用者
    public void addUser() {
        simpleAccountRealm.addAccount("wmyskxz", "123456");
    }

    @Test
    public void testAuthentication() {

        // 1.構建SecurityManager環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        // 2.主體提交認證請求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 設定SecurityManager環境
        Subject subject = SecurityUtils.getSubject(); // 獲取當前主體

        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登入

        // subject.isAuthenticated()方法返回一個boolean值,用於判斷使用者是否認證成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出true

        subject.logout(); // 登出

        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出false
    }
}

執行之後可以看到預想中的效果,先輸出isAuthenticated:true表示登入認證成功,然後再輸出isAuthenticated:false表示認證失敗退出登入,再來一張圖加深一下印象:

Shiro安全框架【快速入門】就這一篇!

流程如下:

  1. 首先呼叫 Subject.login(token) 進行登入,其會自動委託給 Security Manager,呼叫之前必須通過 SecurityUtils.setSecurityManager() 設定;
  2. SecurityManager 負責真正的身份驗證邏輯;它會委託給 Authenticator 進行身份驗證;
  3. Authenticator 才是真正的身份驗證者,Shiro API 中核心的身份認證入口點,此處可以自定義插入自己的實現;
  4. Authenticator 可能會委託給相應的 AuthenticationStrategy 進行多 Realm 身份驗證,預設 ModularRealmAuthenticator 會呼叫 AuthenticationStrategy 進行多 Realm 身份驗證;
  5. Authenticator 會把相應的 token 傳入 Realm,從 Realm 獲取身份驗證資訊,如果沒有返回 / 丟擲異常表示身份驗證失敗了。此處可以配置多個 Realm,將按照相應的順序及策略進行訪問。

Shiro 授權過程

Shiro安全框架【快速入門】就這一篇!

跟認證過程大致相似,下面我們仍然通過程式碼來熟悉一下過程(引入包類似這裡節約篇幅就不貼出來了):

public class AuthenticationTest {

    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

    @Before // 在方法開始前新增一個使用者,讓它具備admin和user兩個角色
    public void addUser() {
        simpleAccountRealm.addAccount("wmyskxz", "123456", "admin", "user");
    }

    @Test
    public void testAuthentication() {

        // 1.構建SecurityManager環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        // 2.主體提交認證請求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 設定SecurityManager環境
        Subject subject = SecurityUtils.getSubject(); // 獲取當前主體

        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登入

        // subject.isAuthenticated()方法返回一個boolean值,用於判斷使用者是否認證成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出true
        // 判斷subject是否具有admin和user兩個角色許可權,如沒有則會報錯
        subject.checkRoles("admin","user");
//        subject.checkRole("xxx"); // 報錯
    }
}

執行測試,能夠正確看到效果。

自定義 Realm

從上面我們瞭解到實際進行許可權資訊驗證的是我們的 Realm,Shiro 框架內部預設提供了兩種實現,一種是查詢.ini檔案的IniRealm,另一種是查詢資料庫的JdbcRealm,這兩種來說都相對簡單,感興趣的可以去【這裡】瞄兩眼,我們著重就來介紹介紹自定義實現的 Realm 吧。

有了上面的對認證和授權的理解,我們先在合適的包下建立一個【MyRealm】類,繼承 Shirot 框架的 AuthorizingRealm 類,並實現預設的兩個方法:

package com.wmyskxz.demo.realm;

import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.*;

public class MyRealm extends AuthorizingRealm {

    /**
     * 模擬資料庫資料
     */
    Map<String, String> userMap = new HashMap<>(16);

    {
        userMap.put("wmyskxz", "123456");
        super.setName("myRealm"); // 設定自定義Realm的名稱,取什麼無所謂..
    }

    /**
     * 授權
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String userName = (String) principalCollection.getPrimaryPrincipal();
        // 從資料庫獲取角色和許可權資料
        Set<String> roles = getRolesByUserName(userName);
        Set<String> permissions = getPermissionsByUserName(userName);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 模擬從資料庫中獲取許可權資料
     *
     * @param userName
     * @return
     */
    private Set<String> getPermissionsByUserName(String userName) {
        Set<String> permissions = new HashSet<>();
        permissions.add("user:delete");
        permissions.add("user:add");
        return permissions;
    }

    /**
     * 模擬從資料庫中獲取角色資料
     *
     * @param userName
     * @return
     */
    private Set<String> getRolesByUserName(String userName) {
        Set<String> roles = new HashSet<>();
        roles.add("admin");
        roles.add("user");
        return roles;
    }

    /**
     * 認證
     *
     * @param authenticationToken 主體傳過來的認證資訊
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1.從主體傳過來的認證資訊中,獲得使用者名稱
        String userName = (String) authenticationToken.getPrincipal();

        // 2.通過使用者名稱到資料庫中獲取憑證
        String password = getPasswordByUserName(userName);
        if (password == null) {
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("wmyskxz", password, "myRealm");
        return authenticationInfo;
    }

    /**
     * 模擬從資料庫取憑證的過程
     *
     * @param userName
     * @return
     */
    private String getPasswordByUserName(String userName) {
        return userMap.get(userName);
    }
}

然後我們編寫測試類,來驗證是否正確:

import com.wmyskxz.demo.realm.MyRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class AuthenticationTest {

    @Test
    public void testAuthentication() {

        MyRealm myRealm = new MyRealm(); // 實現自己的 Realm 例項

        // 1.構建SecurityManager環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(myRealm);

        // 2.主體提交認證請求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 設定SecurityManager環境
        Subject subject = SecurityUtils.getSubject(); // 獲取當前主體

        UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
        subject.login(token); // 登入

        // subject.isAuthenticated()方法返回一個boolean值,用於判斷使用者是否認證成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出true
        // 判斷subject是否具有admin和user兩個角色許可權,如沒有則會報錯
        subject.checkRoles("admin", "user");
//        subject.checkRole("xxx"); // 報錯
        // 判斷subject是否具有user:add許可權
        subject.checkPermission("user:add");
    }
}

執行測試,完美。

Shiro 加密

在之前的學習中,我們在資料庫中儲存的密碼都是明文的,一旦資料庫資料洩露,那就會造成不可估算的損失,所以我們通常都會使用非對稱加密,簡單理解也就是不可逆的加密,而 md5 加密演算法就是符合這樣的一種演算法。

Shiro安全框架【快速入門】就這一篇!

如上面的 123456 用 Md5 加密後,得到的字串:e10adc3949ba59abbe56e057f20f883e,就無法通過計算還原回 123456,我們把這個加密的字串儲存在資料庫中,等下次使用者登入時我們把密碼通過同樣的演算法加密後再從資料庫中取出這個字串進行比較,就能夠知道密碼是否正確了,這樣既保留了密碼驗證的功能又大大增加了安全性,但是問題是:雖然無法直接通過計算反推回密碼,但是我們仍然可以通過計算一些簡單的密碼加密後的 Md5 值進行比較,推算出原來的密碼

比如我的密碼是 123456,你的密碼也是,通過 md5 加密之後的字串一致,所以你也就能知道我的密碼了,如果我們把常用的一些密碼都做 md5 加密得到一本字典,那麼就可以得到相當一部分的人密碼,這也就相當於“破解”了一樣,所以其實也沒有我們想象中的那麼“安全”。

加鹽 + 多次加密

既然相同的密碼 md5 一樣,那麼我們就讓我們的原始密碼再加一個隨機數,然後再進行 md5 加密,這個隨機數就是我們說的鹽(salt),這樣處理下來就能得到不同的 Md5 值,當然我們需要把這個隨機數鹽也儲存進資料庫中,以便我們進行驗證。

另外我們可以通過多次加密的方法,即使黑客通過一定的技術手段拿到了我們的密碼 md5 值,但它並不知道我們到底加密了多少次,所以這也使得破解工作變得艱難。

在 Shiro 框架中,對於這樣的操作提供了簡單的程式碼實現:

String password = "123456";
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
int times = 2;  // 加密次數:2
String alogrithmName = "md5";   // 加密演算法

String encodePassword = new SimpleHash(alogrithmName, password, salt, times).toString();

System.out.printf("原始密碼是 %s , 鹽是: %s, 運算次數是: %d, 運算出來的密文是:%s ",password,salt,times,encodePassword);

輸出:

原始密碼是 123456 , 鹽是: f5GQZsuWjnL9z585JjLrbQ==, 運算次數是: 2, 運算出來的密文是:55fee80f73537cefd6b3c9a920993c25 

SpringBoot 簡單例項

通過上面的學習,我們現在來著手搭建一個簡單的使用 Shiro 進行許可權驗證授權的一個簡單系統

第一步:新建SpringBoot專案,搭建基礎環境

pom包:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</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>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

application.properties檔案:

#thymeleaf 配置
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.servlet.content-type=text/html
#快取設定為false, 這樣修改之後馬上生效,便於除錯
spring.thymeleaf.cache=false

#資料庫
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/testdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
#顯示SQL語句
spring.jpa.show-sql=true
#不加下面這句則不會預設建立MyISAM引擎的資料庫
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#自己重寫的配置類,預設使用utf8編碼
spring.jpa.properties.hibernate.dialect=com.wmyskxz.demo.shiro.config.MySQLConfig

第二步:新建實體類

新建一個【entity】包,在下面建立以下實體:

使用者資訊:

@Entity
public class UserInfo {
    @Id
    @GeneratedValue
    private Long id; // 主鍵.
    @Column(unique = true)
    private String username; // 登入賬戶,唯一.
    private String name; // 名稱(匿名或真實姓名),用於UI顯示
    private String password; // 密碼.
    private String salt; // 加密密碼的鹽
    @JsonIgnoreProperties(value = {"userInfos"})
    @ManyToMany(fetch = FetchType.EAGER) // 立即從資料庫中進行載入資料
    @JoinTable(name = "SysUserRole", joinColumns = @JoinColumn(name = "uid"), inverseJoinColumns = @JoinColumn(name = "roleId"))
    private List<SysRole> roles; // 一個使用者具有多個角色

    /** getter and setter */
}

角色資訊:

@Entity
public class SysRole {
    @Id
    @GeneratedValue
    private Long id; // 主鍵.
    private String name; // 角色名稱,如 admin/user
    private String description; // 角色描述,用於UI顯示

    // 角色 -- 許可權關係:多對多
    @JsonIgnoreProperties(value = {"roles"})
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "permissionId")})
    private List<SysPermission> permissions;

    // 使用者 -- 角色關係:多對多
    @JsonIgnoreProperties(value = {"roles"})
    @ManyToMany
    @JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "uid")})
    private List<UserInfo> userInfos;// 一個角色對應多個使用者

    /** getter and setter */
}

許可權資訊:

@Entity
public class SysPermission {
    @Id
    @GeneratedValue
    private Long id; // 主鍵.
    private String name; // 許可權名稱,如 user:select
    private String description; // 許可權描述,用於UI顯示
    private String url; // 許可權地址.
    @JsonIgnoreProperties(value = {"permissions"})
    @ManyToMany
    @JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "permissionId")}, inverseJoinColumns = {@JoinColumn(name = "roleId")})
    private List<SysRole> roles; // 一個許可權可以被多個角色使用

    /** getter and setter */
}

注意:這裡有一個坑,還纏了我蠻久感覺,就是當我們想要使用RESTful風格返回給前臺JSON資料的時候,這裡有一個關於多對多無限迴圈的坑,比如當我們想要返回給前臺一個使用者資訊時,由於一個使用者擁有多個角色,一個角色又擁有多個許可權,而許可權跟角色也是多對多的關係,也就是造成了 查使用者→查角色→查許可權→查角色→查使用者… 這樣的無限迴圈,導致傳輸錯誤,所以我們根據這樣的邏輯在每一個實體類返回JSON時使用了一個@JsonIgnoreProperties註解,來排除自己對自己無線引用的過程,也就是打斷這樣的無限迴圈。

根據以上的程式碼會自動生成user_info(使用者資訊表)、sys_role(角色表)、sys_permission(許可權表)、sys_user_role(使用者角色表)、sys_role_permission(角色許可權表)這五張表,為了方便測試我們給這五張表插入一些初始化資料:

INSERT INTO `user_info` (`id`,`name`,`password`,`salt`,`username`) VALUES (1, `管理員`,`951cd60dec2104024949d2e0b2af45ae`, `xbNIxrQfn6COSYn1/GdloA==`, `wmyskxz`);
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (1,`查詢使用者`,`userInfo:view`,`/userList`);
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (2,`增加使用者`,`userInfo:add`,`/userAdd`);
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (3,`刪除使用者`,`userInfo:delete`,`/userDelete`);
INSERT INTO `sys_role` (`id`,`description`,`name`) VALUES (1,`管理員`,`admin`);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);

第三步:配置 Shiro

新建一個【config】包,在下面建立以下檔案:

MySQLConfig:

public class MySQLConfig extends MySQL5InnoDBDialect {
    @Override
    public String getTableTypeString() {
        return "ENGINE=InnoDB DEFAULT CHARSET=utf8";
    }
}

這個檔案關聯的是配置檔案中最後一個配置,是讓 Hibernate 預設建立 InnoDB 引擎並預設使用 utf-8 編碼

MyShiroRealm:

public class MyShiroRealm extends AuthorizingRealm {
    @Resource
    private UserInfoService userInfoService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 能進入這裡說明使用者已經通過驗證了
        UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (SysRole role : userInfo.getRoles()) {
            simpleAuthorizationInfo.addRole(role.getName());
            for (SysPermission permission : role.getPermissions()) {
                simpleAuthorizationInfo.addStringPermission(permission.getName());
            }
        }
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 獲取使用者輸入的賬戶
        String username = (String) authenticationToken.getPrincipal();
        System.out.println(authenticationToken.getPrincipal());
        // 通過username從資料庫中查詢 UserInfo 物件
        // 實際專案中,這裡可以根據實際情況做快取,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
        UserInfo userInfo = userInfoService.findByUsername(username);
        if (null == userInfo) {
            return null;
        }

        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                userInfo, // 使用者名稱
                userInfo.getPassword(), // 密碼
                ByteSource.Util.bytes(userInfo.getSalt()), // salt=username+salt
                getName() // realm name
        );
        return simpleAuthenticationInfo;
    }
}

自定義的 Realm ,方法跟上面的認證授權過程一致

ShiroConfig:

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 攔截器.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不會被攔截的連結 順序判斷
        filterChainDefinitionMap.put("/static/**", "anon");
        // 配置退出 過濾器,其中的具體的退出程式碼Shiro已經替我們實現了
        filterChainDefinitionMap.put("/logout", "logout");
        // <!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊 -->:這是一個坑呢,一不小心程式碼就不好使了;
        // <!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問-->
        filterChainDefinitionMap.put("/**", "authc");
        // 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登入成功後要跳轉的連結
        shiroFilterFactoryBean.setSuccessUrl("/index");

        //未授權介面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 憑證匹配器
     * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了)
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 雜湊演算法:這裡使用MD5演算法;
        hashedCredentialsMatcher.setHashIterations(2); // 雜湊的次數,比如雜湊兩次,相當於 md5(md5(""));
        return hashedCredentialsMatcher;
    }

    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }


    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

    /**
     * 開啟shiro aop註解支援.
     * 使用代理方式;所以需要開啟程式碼支援;
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean(name = "simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver
    createSimpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        mappings.setProperty("DatabaseException", "databaseError"); // 資料庫異常處理
        mappings.setProperty("UnauthorizedException", "403");
        r.setExceptionMappings(mappings);  // None by default
        r.setDefaultErrorView("error");    // No default
        r.setExceptionAttribute("ex");     // Default is "exception"
        //r.setWarnLogCategory("example.MvcLogger");     // No default
        return r;
    }
}

Apache Shiro 的核心通過 Filter 來實現,就好像 SpringMvc 通過 DispachServlet 來主控制一樣。 既然是使用 Filter 一般也就能猜到,是通過URL規則來進行過濾和許可權校驗,所以我們需要定義一系列關於URL的規則和訪問許可權。

Filter Chain定義說明:

  • 1、一個URL可以配置多個Filter,使用逗號分隔
  • 2、當設定多個過濾器時,全部驗證通過,才視為通過
  • 3、部分過濾器可指定引數,如perms,roles

Shiro內建的FilterChain

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter
  • anon:所有url都都可以匿名訪問
  • authc: 需要認證才能進行訪問
  • user:配置記住我或認證通過可以訪問

第四步:準備 DAO 層和 Service 層

新建【dao】包,在下面建立【UserInfoDao】介面:

public interface UserInfoDao extends JpaRepository<UserInfo, Long> {
    /** 通過username查詢使用者資訊*/
    public UserInfo findByUsername(String username);
}

新建【service】包,建立【UserInfoService】介面:

public interface UserInfoService {
    /** 通過username查詢使用者資訊;*/
    public UserInfo findByUsername(String username);
}

並在該包下再新建一個【impl】包,新建【UserInfoServiceImpl】實現類:

@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Resource
    UserInfoDao userInfoDao;

    @Override
    public UserInfo findByUsername(String username) {
        return userInfoDao.findByUsername(username);
    }
}

第五步:controller層

新建【controller】包,然後在下面建立以下檔案:

HomeController:

@Controller
public class HomeController {

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

    @RequestMapping("/login")
    public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
        System.out.println("HomeController.login()");
        // 登入失敗從request中獲取shiro處理的異常資訊。
        // shiroLoginFailure:就是shiro異常類的全類名.
        String exception = (String) request.getAttribute("shiroLoginFailure");
        System.out.println("exception=" + exception);
        String msg = "";
        if (exception != null) {
            if (UnknownAccountException.class.getName().equals(exception)) {
                System.out.println("UnknownAccountException -- > 賬號不存在:");
                msg = "UnknownAccountException -- > 賬號不存在:";
            } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
                System.out.println("IncorrectCredentialsException -- > 密碼不正確:");
                msg = "IncorrectCredentialsException -- > 密碼不正確:";
            } else if ("kaptchaValidateFailed".equals(exception)) {
                System.out.println("kaptchaValidateFailed -- > 驗證碼錯誤");
                msg = "kaptchaValidateFailed -- > 驗證碼錯誤";
            } else {
                msg = "else >> "+exception;
                System.out.println("else -- >" + exception);
            }
        }
        map.put("msg", msg);
        // 此方法不處理登入成功,由shiro進行處理
        return "/login";
    }

    @RequestMapping("/403")
    public String unauthorizedRole(){
        System.out.println("------沒有許可權-------");
        return "403";
    }
}

這裡邊的地址對應我們在設定 Shiro 時設定的地址

UserInfoController:

@RestController
public class UserInfoController {

    @Resource
    UserInfoService userInfoService;

    /**
     * 按username賬戶從資料庫中取出使用者資訊
     *
     * @param username 賬戶
     * @return
     */
    @GetMapping("/userList")
    @RequiresPermissions("userInfo:view") // 許可權管理.
    public UserInfo findUserInfoByUsername(@RequestParam String username) {
        return userInfoService.findByUsername(username);
    }

    /**
     * 簡單模擬從資料庫新增使用者資訊成功
     *
     * @return
     */
    @PostMapping("/userAdd")
    @RequiresPermissions("userInfo:add")
    public String addUserInfo() {
        return "addUserInfo success!";
    }

    /**
     * 簡單模擬從資料庫刪除使用者成功
     *
     * @return
     */
    @DeleteMapping("/userDelete")
    @RequiresPermissions("userInfo:delete")
    public String deleteUserInfo() {
        return "deleteUserInfo success!";
    }
}

第六步:準備頁面

新建三個頁面用來測試:

index.html:首頁

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>首頁</title>
</head>
<body>
index - 首頁
</body>
</html>

login.html:登入頁

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>登入頁</title>
</head>
<body>
錯誤資訊:<h4 th:text="${msg}"></h4>
<form action="" method="post">
    <p>賬號:<input type="text" name="username" value="wmyskxz"/></p>
    <p>密碼:<input type="text" name="password" value="123456"/></p>
    <p><input type="submit" value="登入"/></p>
</form>
</body>
</html>

403.html:沒有許可權的頁面

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>403錯誤頁</title>
</head>
<body>
錯誤頁面
</body>
</html>

第七步:測試

  1. 編寫好程式後就可以啟動,首先訪問http://localhost:8080/userList?username=wmyskxz頁面,由於沒有登入就會跳轉到我們配置好的http://localhost:8080/login頁面。登陸之後就會看到正確返回的JSON資料,上面這些操作時候觸發MyShiroRealm.doGetAuthenticationInfo()這個方法,也就是登入認證的方法。
  2. 登入之後,我們還能訪問http://localhost:8080/userAdd頁面,因為我們在資料庫中提前配置好了許可權,能夠看到正確返回的資料,但是我們訪問http://localhost:8080/userDelete時,就會返回錯誤頁面.

注意:以上測試需要在REST工具中測試,因為在Controller層中配置了方法,大家也可以不用REST風格來測試一下看看!


完成了以上的學習,我們就差不多對 Shiro 框架有了一定了解了,更多的東西以後再分享再學習吧.

參考資料:
springboot(十四):springboot整合shiro-登入認證和許可權管理——純潔的微笑
Shiro安全框架入門 – 慕課視訊教程
Shiro 系列教程 —— how2j網站

歡迎轉載,轉載請註明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微訊號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加qq群:3382693

相關文章