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

Shiro 簡介

照例又去官網(wǎng)扒了扒介紹:

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安全框架,能夠用于身份驗證、授權(quán)疗韵、加密和會話管理瓣赂。Shiro擁有易于理解的API,您可以快速、輕松地獲得任何應(yīng)用程序——從最小的移動應(yīng)用程序到最大的網(wǎng)絡(luò)和企業(yè)應(yīng)用程序鲤竹。

簡而言之浪读,Apache Shiro 是一個強大靈活的開源安全框架,可以完全處理身份驗證辛藻、授權(quán)碘橘、加密和會話管理。

Shiro能到底能做些什么呢吱肌?

  • 驗證用戶身份
  • 用戶訪問權(quán)限控制痘拆,比如:1、判斷用戶是否分配了一定的安全角色氮墨。2纺蛆、判斷用戶是否被授予完成某個操作的權(quán)限
  • 在非 Web 或 EJB 容器的環(huán)境下可以任意使用Session API
  • 可以響應(yīng)認(rèn)證吐葵、訪問控制,或者 Session 生命周期中發(fā)生的事件
  • 可將一個或以上用戶安全數(shù)據(jù)源數(shù)據(jù)組合成一個復(fù)合的用戶 “view”(視圖)
  • 支持單點登錄(SSO)功能
  • 支持提供“Remember Me”服務(wù)桥氏,獲取用戶關(guān)聯(lián)信息而無需登錄
    ···

為什么是 Shiro温峭?

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

  • 易于使用——易用性是項目的最終目標(biāo)字支。應(yīng)用程序安全非常令人困惑和沮喪,被認(rèn)為是“不可避免的災(zāi)難”凤藏。如果你讓它簡化到新手都可以使用它,它就將不再是一種痛苦了。
  • 全面——沒有其他安全框架的寬度范圍可以同Apache Shiro一樣,它可以成為你的“一站式”為您的安全需求提供保障堕伪。
  • 靈活——Apache Shiro可以在任何應(yīng)用程序環(huán)境中工作清笨。雖然在網(wǎng)絡(luò)工作、EJB和IoC環(huán)境中可能并不需要它刃跛。但Shiro的授權(quán)也沒有任何規(guī)范,甚至沒有許多依賴關(guān)系抠艾。
  • Web支持——Apache Shiro擁有令人興奮的web應(yīng)用程序支持,允許您基于應(yīng)用程序的url創(chuàng)建靈活的安全策略和網(wǎng)絡(luò)協(xié)議(例如REST),同時還提供一組JSP庫控制頁面輸出。
  • 低耦合——Shiro干凈的API和設(shè)計模式使它容易與許多其他框架和應(yīng)用程序集成桨昙。你會看到Shiro無縫地集成Spring這樣的框架, 以及Grails, Wicket, Tapestry, Mule, Apache Camel, Vaadin...等检号。
  • 被廣泛支持——Apache Shiro是Apache軟件基金會的一部分。項目開發(fā)和用戶組都有友好的網(wǎng)民愿意幫助蛙酪。這樣的商業(yè)公司如果需要Katasoft還提供專業(yè)的支持和服務(wù)齐苛。

有興趣的可以去仔細(xì)看看官方的文檔:【傳送門】

Apache Shiro Features 特性

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

Authentication(認(rèn)證), Authorization(授權(quán)), Session Management(會話管理), Cryptography(加密)被 Shiro 框架的開發(fā)團隊稱之為應(yīng)用安全的四大基石凹蜂。那么就讓我們來看看它們吧:

  • Authentication(認(rèn)證):用戶身份識別,通常被稱為用戶“登錄”
  • Authorization(授權(quán)):訪問控制阁危。比如某個用戶是否具有某個操作的使用權(quán)限玛痊。
  • Session Management(會話管理):特定于用戶的會話管理,甚至在非web 或 EJB 應(yīng)用程序。
  • Cryptography(加密):在對數(shù)據(jù)源使用加密算法加密的同時狂打,保證易于使用擂煞。

還有其他的功能來支持和加強這些不同應(yīng)用環(huán)境下安全領(lǐng)域的關(guān)注點。特別是對以下的功能支持:

  • Web支持:Shiro的Web支持API有助于保護Web應(yīng)用程序趴乡。
  • 緩存:緩存是Apache Shiro API中的第一級对省,以確保安全操作保持快速和高效。
  • 并發(fā)性:Apache Shiro支持具有并發(fā)功能的多線程應(yīng)用程序晾捏。
  • 測試:存在測試支持蒿涎,可幫助您編寫單元測試和集成測試,并確保代碼按預(yù)期得到保障惦辛。
  • “運行方式”:允許用戶承擔(dān)另一個用戶的身份(如果允許)的功能劳秋,有時在管理方案中很有用。
  • “記住我”:記住用戶在會話中的身份,所以用戶只需要強制登錄即可俗批。

注意: Shiro不會去維護用戶俗或、維護權(quán)限,這些需要我們自己去設(shè)計/提供岁忘,然后通過相應(yīng)的接口注入給Shiro

High-Level Overview 高級概述

在概念層辛慰,Shiro 架構(gòu)包含三個主要的理念:Subject,SecurityManager和 Realm。下面的圖展示了這些組件如何相互作用干像,我們將在下面依次對其進行描述帅腌。

  • Subject:當(dāng)前用戶,Subject 可以是一個人麻汰,但也可以是第三方服務(wù)速客、守護進程帳戶、時鐘守護任務(wù)或者其它–當(dāng)前和軟件交互的任何事件五鲫。
  • SecurityManager:管理所有Subject溺职,SecurityManager 是 Shiro 架構(gòu)的核心,配合內(nèi)部安全組件共同組成安全傘位喂。
  • Realms:用于進行權(quán)限信息的驗證浪耘,我們自己實現(xiàn)。Realm 本質(zhì)上是一個特定的安全 DAO:它封裝與數(shù)據(jù)源連接的細(xì)節(jié)塑崖,得到Shiro 所需的相關(guān)的數(shù)據(jù)七冲。在配置 Shiro 的時候,你必須指定至少一個Realm 來實現(xiàn)認(rèn)證(authentication)和/或授權(quán)(authorization)规婆。

我們需要實現(xiàn)Realms的Authentication 和 Authorization澜躺。其中 Authentication 是用來驗證用戶身份,Authorization 是授權(quán)訪問控制抒蚜,用于對用戶進行的操作授權(quán)掘鄙,證明該用戶是否允許進行當(dāng)前操作,如訪問某個鏈接削锰,某個資源文件等通铲。

Shiro 認(rèn)證過程

上圖展示了 Shiro 認(rèn)證的一個重要的過程,為了加深我們的印象器贩,我們來自己動手來寫一個例子,來驗證一下朋截,首先我們新建一個Maven工程蛹稍,然后在pom.xml中引入相關(guān)依賴:

<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.構(gòu)建SecurityManager環(huán)境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        // 2.主體提交認(rèn)證請求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 設(shè)置SecurityManager環(huán)境
        Subject subject = SecurityUtils.getSubject(); // 獲取當(dāng)前主體

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

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

        subject.logout(); // 登出

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

運行之后可以看到預(yù)想中的效果,先輸出isAuthenticated:true表示登錄認(rèn)證成功部服,然后再輸出isAuthenticated:false表示認(rèn)證失敗退出登錄唆姐,再來一張圖加深一下印象:

流程如下:

  1. 首先調(diào)用 Subject.login(token) 進行登錄,其會自動委托給 Security Manager廓八,調(diào)用之前必須通過 SecurityUtils.setSecurityManager() 設(shè)置奉芦;
  2. SecurityManager 負(fù)責(zé)真正的身份驗證邏輯赵抢;它會委托給 Authenticator 進行身份驗證;
  3. Authenticator 才是真正的身份驗證者声功,Shiro API 中核心的身份認(rèn)證入口點烦却,此處可以自定義插入自己的實現(xiàn);
  4. Authenticator 可能會委托給相應(yīng)的 AuthenticationStrategy 進行多 Realm 身份驗證先巴,默認(rèn) ModularRealmAuthenticator 會調(diào)用 AuthenticationStrategy 進行多 Realm 身份驗證其爵;
  5. Authenticator 會把相應(yīng)的 token 傳入 Realm,從 Realm 獲取身份驗證信息伸蚯,如果沒有返回 / 拋出異常表示身份驗證失敗了摩渺。此處可以配置多個 Realm,將按照相應(yīng)的順序及策略進行訪問剂邮。

Shiro 授權(quán)過程

跟認(rèn)證過程大致相似摇幻,下面我們?nèi)匀煌ㄟ^代碼來熟悉一下過程(引入包類似這里節(jié)約篇幅就不貼出來了):

public class AuthenticationTest {

    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();

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

    @Test
    public void testAuthentication() {

        // 1.構(gòu)建SecurityManager環(huán)境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        // 2.主體提交認(rèn)證請求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 設(shè)置SecurityManager環(huán)境
        Subject subject = SecurityUtils.getSubject(); // 獲取當(dāng)前主體

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

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

運行測試,能夠正確看到效果挥萌。

自定義 Realm

從上面我們了解到實際進行權(quán)限信息驗證的是我們的 Realm囚企,Shiro 框架內(nèi)部默認(rèn)提供了兩種實現(xiàn),一種是查詢.ini文件的IniRealm瑞眼,另一種是查詢數(shù)據(jù)庫的JdbcRealm龙宏,這兩種來說都相對簡單,感興趣的可以去【這里】瞄兩眼伤疙,我們著重就來介紹介紹自定義實現(xiàn)的 Realm 吧银酗。

有了上面的對認(rèn)證和授權(quán)的理解,我們先在合適的包下創(chuàng)建一個【MyRealm】類徒像,繼承 Shirot 框架的 AuthorizingRealm 類黍特,并實現(xiàn)默認(rèn)的兩個方法:

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 {

    /**
     * 模擬數(shù)據(jù)庫數(shù)據(jù)
     */
    Map<String, String> userMap = new HashMap<>(16);

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

    /**
     * 授權(quán)
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String userName = (String) principalCollection.getPrimaryPrincipal();
        // 從數(shù)據(jù)庫獲取角色和權(quán)限數(shù)據(jù)
        Set<String> roles = getRolesByUserName(userName);
        Set<String> permissions = getPermissionsByUserName(userName);

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

    /**
     * 模擬從數(shù)據(jù)庫中獲取權(quán)限數(shù)據(jù)
     *
     * @param userName
     * @return
     */
    private Set<String> getPermissionsByUserName(String userName) {
        Set<String> permissions = new HashSet<>();
        permissions.add("user:delete");
        permissions.add("user:add");
        return permissions;
    }

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

    /**
     * 認(rèn)證
     *
     * @param authenticationToken 主體傳過來的認(rèn)證信息
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1.從主體傳過來的認(rèn)證信息中锯蛀,獲得用戶名
        String userName = (String) authenticationToken.getPrincipal();

        // 2.通過用戶名到數(shù)據(jù)庫中獲取憑證
        String password = getPasswordByUserName(userName);
        if (password == null) {
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("wmyskxz", password, "myRealm");
        return authenticationInfo;
    }

    /**
     * 模擬從數(shù)據(jù)庫取憑證的過程
     *
     * @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(); // 實現(xiàn)自己的 Realm 實例

        // 1.構(gòu)建SecurityManager環(huán)境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(myRealm);

        // 2.主體提交認(rèn)證請求
        SecurityUtils.setSecurityManager(defaultSecurityManager); // 設(shè)置SecurityManager環(huán)境
        Subject subject = SecurityUtils.getSubject(); // 獲取當(dāng)前主體

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

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

運行測試,完美旁涤。

Shiro 加密

在之前的學(xué)習(xí)中翔曲,我們在數(shù)據(jù)庫中保存的密碼都是明文的,一旦數(shù)據(jù)庫數(shù)據(jù)泄露劈愚,那就會造成不可估算的損失瞳遍,所以我們通常都會使用非對稱加密,簡單理解也就是不可逆的加密菌羽,而 md5 加密算法就是符合這樣的一種算法掠械。

如上面的 123456 用 Md5 加密后,得到的字符串:e10adc3949ba59abbe56e057f20f883e,就無法通過計算還原回 123456猾蒂,我們把這個加密的字符串保存在數(shù)據(jù)庫中均唉,等下次用戶登錄時我們把密碼通過同樣的算法加密后再從數(shù)據(jù)庫中取出這個字符串進行比較,就能夠知道密碼是否正確了肚菠,這樣既保留了密碼驗證的功能又大大增加了安全性舔箭,但是問題是:雖然無法直接通過計算反推回密碼,但是我們?nèi)匀豢梢酝ㄟ^計算一些簡單的密碼加密后的 Md5 值進行比較案糙,推算出原來的密碼

比如我的密碼是 123456限嫌,你的密碼也是,通過 md5 加密之后的字符串一致时捌,所以你也就能知道我的密碼了怒医,如果我們把常用的一些密碼都做 md5 加密得到一本字典,那么就可以得到相當(dāng)一部分的人密碼奢讨,這也就相當(dāng)于“破解”了一樣稚叹,所以其實也沒有我們想象中的那么“安全”。

加鹽 + 多次加密

既然相同的密碼 md5 一樣拿诸,那么我們就讓我們的原始密碼再加一個隨機數(shù)扒袖,然后再進行 md5 加密,這個隨機數(shù)就是我們說的鹽(salt)亩码,這樣處理下來就能得到不同的 Md5 值季率,當(dāng)然我們需要把這個隨機數(shù)鹽也保存進數(shù)據(jù)庫中,以便我們進行驗證描沟。

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

在 Shiro 框架中,對于這樣的操作提供了簡單的代碼實現(xiàn):

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

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

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

輸出:

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

SpringBoot 簡單實例

通過上面的學(xué)習(xí)席覆,我們現(xiàn)在來著手搭建一個簡單的使用 Shiro 進行權(quán)限驗證授權(quán)的一個簡單系統(tǒng)

第一步:新建SpringBoot項目史辙,搭建基礎(chǔ)環(huán)境

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
#緩存設(shè)置為false, 這樣修改之后馬上生效,便于調(diào)試
spring.thymeleaf.cache=false

#數(shù)據(jù)庫
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
#不加下面這句則不會默認(rèn)創(chuàng)建MyISAM引擎的數(shù)據(jù)庫
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#自己重寫的配置類佩伤,默認(rèn)使用utf8編碼
spring.jpa.properties.hibernate.dialect=com.wmyskxz.demo.shiro.config.MySQLConfig

第二步:新建實體類

新建一個【entity】包聊倔,在下面創(chuàng)建以下實體:

用戶信息:

@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) // 立即從數(shù)據(jù)庫中進行加載數(shù)據(jù)
    @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顯示

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

    // 用戶 -- 角色關(guān)系:多對多
    @JsonIgnoreProperties(value = {"roles"})
    @ManyToMany
    @JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "uid")})
    private List<UserInfo> userInfos;// 一個角色對應(yīng)多個用戶

    /** getter and setter */
}

權(quán)限信息:

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

    /** getter and setter */
}

注意:這里有一個坑,還纏了我蠻久感覺畦戒,就是當(dāng)我們想要使用RESTful風(fēng)格返回給前臺JSON數(shù)據(jù)的時候方库,這里有一個關(guān)于多對多無限循環(huán)的坑,比如當(dāng)我們想要返回給前臺一個用戶信息時障斋,由于一個用戶擁有多個角色,一個角色又擁有多個權(quán)限,而權(quán)限跟角色也是多對多的關(guān)系垃环,也就是造成了 查用戶→查角色→查權(quán)限→查角色→查用戶... 這樣的無限循環(huán)邀层,導(dǎo)致傳輸錯誤,所以我們根據(jù)這樣的邏輯在每一個實體類返回JSON時使用了一個@JsonIgnoreProperties注解遂庄,來排除自己對自己無線引用的過程寥院,也就是打斷這樣的無限循環(huán)。

根據(jù)以上的代碼會自動生成user_info(用戶信息表)涛目、sys_role(角色表)秸谢、sys_permission(權(quán)限表)、sys_user_role(用戶角色表)霹肝、sys_role_permission(角色權(quán)限表)這五張表估蹄,為了方便測試我們給這五張表插入一些初始化數(shù)據(jù):

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】包,在下面創(chuàng)建以下文件:

MySQLConfig:

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

這個文件關(guān)聯(lián)的是配置文件中最后一個配置沫换,是讓 Hibernate 默認(rèn)創(chuàng)建 InnoDB 引擎并默認(rèn)使用 utf-8 編碼

MyShiroRealm:

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

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 能進入這里說明用戶已經(jīng)通過驗證了
        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從數(shù)據(jù)庫中查找 UserInfo 對象
        // 實際項目中臭蚁,這里可以根據(jù)實際情況做緩存,如果不做讯赏,Shiro自己也是有時間間隔機制垮兑,2分鐘內(nèi)不會重復(fù)執(zhí)行該方法
        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 ,方法跟上面的認(rèn)證授權(quán)過程一致

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已經(jīng)替我們實現(xiàn)了
        filterChainDefinitionMap.put("/logout", "logout");
        // <!-- 過濾鏈定義漱挎,從上向下順序執(zhí)行系枪,一般將/**放在最為下邊 -->:這是一個坑呢,一不小心代碼就不好使了;
        // <!-- authc:所有url都必須認(rèn)證通過才可以訪問; anon:所有url都都可以匿名訪問-->
        filterChainDefinitionMap.put("/**", "authc");
        // 如果不設(shè)置默認(rèn)會自動尋找Web工程根目錄下的"/login.jsp"頁面
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 登錄成功后要跳轉(zhuǎn)的鏈接
        shiroFilterFactoryBean.setSuccessUrl("/index");

        //未授權(quán)界面;
        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); // 散列的次數(shù)磕谅,比如散列兩次私爷,相當(dāng)于 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"); // 數(shù)據(jù)庫異常處理
        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 來實現(xiàn)老翘,就好像 SpringMvc 通過 DispachServlet 來主控制一樣恩敌。 既然是使用 Filter 一般也就能猜到扔茅,是通過URL規(guī)則來進行過濾和權(quán)限校驗旺隙,所以我們需要定義一系列關(guān)于URL的規(guī)則和訪問權(quán)限掂墓。

Filter Chain定義說明:

  • 1却盘、一個URL可以配置多個Filter勃救,使用逗號分隔
  • 2迈套、當(dāng)設(shè)置多個過濾器時宏榕,全部驗證通過拓诸,才視為通過
  • 3、部分過濾器可指定參數(shù)麻昼,如perms奠支,roles

Shiro內(nèi)置的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: 需要認(rèn)證才能進行訪問
  • user:配置記住我或認(rèn)證通過可以訪問

第四步:準(zhǔn)備 DAO 層和 Service 層

新建【dao】包,在下面創(chuàng)建【UserInfoDao】接口:

public interface UserInfoDao extends JpaRepository<UserInfo, Long> {
    /** 通過username查找用戶信息*/
    public UserInfo findByUsername(String username);
}

新建【service】包抚芦,創(chuàng)建【UserInfoService】接口:

public interface UserInfoService {
    /** 通過username查找用戶信息倍谜;*/
    public UserInfo findByUsername(String username);
}

并在該包下再新建一個【impl】包迈螟,新建【UserInfoServiceImpl】實現(xiàn)類:

@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Resource
    UserInfoDao userInfoDao;

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

第五步:controller層

新建【controller】包,然后在下面創(chuàng)建以下文件:

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("------沒有權(quán)限-------");
        return "403";
    }
}

這里邊的地址對應(yīng)我們在設(shè)置 Shiro 時設(shè)置的地址

UserInfoController:

@RestController
public class UserInfoController {

    @Resource
    UserInfoService userInfoService;

    /**
     * 按username賬戶從數(shù)據(jù)庫中取出用戶信息
     *
     * @param username 賬戶
     * @return
     */
    @GetMapping("/userList")
    @RequiresPermissions("userInfo:view") // 權(quán)限管理.
    public UserInfo findUserInfoByUsername(@RequestParam String username) {
        return userInfoService.findByUsername(username);
    }

    /**
     * 簡單模擬從數(shù)據(jù)庫添加用戶信息成功
     *
     * @return
     */
    @PostMapping("/userAdd")
    @RequiresPermissions("userInfo:add")
    public String addUserInfo() {
        return "addUserInfo success!";
    }

    /**
     * 簡單模擬從數(shù)據(jù)庫刪除用戶成功
     *
     * @return
     */
    @DeleteMapping("/userDelete")
    @RequiresPermissions("userInfo:delete")
    public String deleteUserInfo() {
        return "deleteUserInfo success!";
    }
}

第六步:準(zhǔn)備頁面

新建三個頁面用來測試:

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:沒有權(quán)限的頁面

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

第七步:測試

  1. 編寫好程序后就可以啟動答毫,首先訪問http://localhost:8080/userList?username=wmyskxz頁面,由于沒有登錄就會跳轉(zhuǎn)到我們配置好的http://localhost:8080/login頁面季春。登陸之后就會看到正確返回的JSON數(shù)據(jù)洗搂,上面這些操作時候觸發(fā)MyShiroRealm.doGetAuthenticationInfo()這個方法,也就是登錄認(rèn)證的方法载弄。
  2. 登錄之后耘拇,我們還能訪問http://localhost:8080/userAdd頁面,因為我們在數(shù)據(jù)庫中提前配置好了權(quán)限宇攻,能夠看到正確返回的數(shù)據(jù)惫叛,但是我們訪問http://localhost:8080/userDelete時,就會返回錯誤頁面.

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


完成了以上的學(xué)習(xí)亲桥,我們就差不多對 Shiro 框架有了一定了解了洛心,更多的東西以后再分享再學(xué)習(xí)吧.

參考資料:
springboot(十四):springboot整合shiro-登錄認(rèn)證和權(quán)限管理——純潔的微笑
Shiro安全框架入門 - 慕課視頻教程
Shiro 系列教程 —— how2j網(wǎng)站

歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明出處题篷!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號:wmyskxz
分享自己的學(xué)習(xí) & 學(xué)習(xí)資料 & 生活
想要交流的朋友也可以加qq群:3382693

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末词身,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子番枚,更是在濱河造成了極大的恐慌法严,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件葫笼,死亡現(xiàn)場離奇詭異深啤,居然都是意外死亡,警方通過查閱死者的電腦和手機路星,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進店門溯街,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洋丐,你說我怎么就攤上這事呈昔。” “怎么了友绝?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵堤尾,是天一觀的道長。 經(jīng)常有香客問我迁客,道長郭宝,這世上最難降的妖魔是什么辞槐? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮剩蟀,結(jié)果婚禮上催蝗,老公的妹妹穿的比我還像新娘切威。我一直安慰自己育特,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布先朦。 她就那樣靜靜地躺著缰冤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喳魏。 梳的紋絲不亂的頭發(fā)上棉浸,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機與錄音刺彩,去河邊找鬼迷郑。 笑死,一個胖子當(dāng)著我的面吹牛创倔,可吹牛的內(nèi)容都是我干的嗡害。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼畦攘,長吁一口氣:“原來是場噩夢啊……” “哼霸妹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起知押,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤叹螟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后台盯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罢绽,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年静盅,在試婚紗的時候發(fā)現(xiàn)自己被綠了良价。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡温亲,死狀恐怖棚壁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情栈虚,我是刑警寧澤袖外,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站魂务,受9級特大地震影響曼验,放射性物質(zhì)發(fā)生泄漏泌射。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一鬓照、第九天 我趴在偏房一處隱蔽的房頂上張望熔酷。 院中可真熱鬧,春花似錦豺裆、人聲如沸拒秘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽躺酒。三九已至,卻和暖如春蔑歌,著一層夾襖步出監(jiān)牢的瞬間羹应,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工次屠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留园匹,地道東北人。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓劫灶,卻偏偏與公主長得像裸违,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子浑此,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容