
Shiro 簡(jiǎn)介
照例又去官網(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?是一個(gè)強(qiáng)大且易用的Java安全框架,能夠用于身份驗(yàn)證、授權(quán)、加密和會(huì)話管理。Shiro擁有易于理解的API,您可以快速、輕松地獲得任何應(yīng)用程序——從最小的移動(dòng)應(yīng)用程序到最大的網(wǎng)絡(luò)和企業(yè)應(yīng)用程序。
簡(jiǎn)而言之,Apache Shiro 是一個(gè)強(qiáng)大靈活的開(kāi)源安全框架,可以完全處理身份驗(yàn)證、授權(quán)、加密和會(huì)話管理。
Shiro能到底能做些什么呢?
- 驗(yàn)證用戶身份
- 用戶訪問(wèn)權(quán)限控制,比如:1、判斷用戶是否分配了一定的安全角色。2、判斷用戶是否被授予完成某個(gè)操作的權(quán)限
- 在非 Web 或 EJB 容器的環(huán)境下可以任意使用Session API
- 可以響應(yīng)認(rèn)證、訪問(wèn)控制,或者 Session 生命周期中發(fā)生的事件
- 可將一個(gè)或以上用戶安全數(shù)據(jù)源數(shù)據(jù)組合成一個(gè)復(fù)合的用戶 “view”(視圖)
- 支持單點(diǎn)登錄(SSO)功能
- 支持提供“Remember Me”服務(wù),獲取用戶關(guān)聯(lián)信息而無(wú)需登錄
···
為什么是 Shiro?
使用 Shiro 官方給了許多令人信服的原因,因?yàn)?Shiro 具有以下幾個(gè)特點(diǎn):
- 易于使用——易用性是項(xiàng)目的最終目標(biāo)。應(yīng)用程序安全非常令人困惑和沮喪,被認(rèn)為是“不可避免的災(zāi)難”。如果你讓它簡(jiǎn)化到新手都可以使用它,它就將不再是一種痛苦了。
- 全面——沒(méi)有其他安全框架的寬度范圍可以同Apache Shiro一樣,它可以成為你的“一站式”為您的安全需求提供保障。
- 靈活——Apache Shiro可以在任何應(yīng)用程序環(huán)境中工作。雖然在網(wǎng)絡(luò)工作、EJB和IoC環(huán)境中可能并不需要它。但Shiro的授權(quán)也沒(méi)有任何規(guī)范,甚至沒(méi)有許多依賴(lài)關(guān)系。
- Web支持——Apache Shiro擁有令人興奮的web應(yīng)用程序支持,允許您基于應(yīng)用程序的url創(chuàng)建靈活的安全策略和網(wǎng)絡(luò)協(xié)議(例如REST),同時(shí)還提供一組JSP庫(kù)控制頁(yè)面輸出。
- 低耦合——Shiro干凈的API和設(shè)計(jì)模式使它容易與許多其他框架和應(yīng)用程序集成。你會(huì)看到Shiro無(wú)縫地集成Spring這樣的框架, 以及Grails, Wicket, Tapestry, Mule, Apache Camel, Vaadin...等。
- 被廣泛支持——Apache Shiro是Apache軟件基金會(huì)的一部分。項(xiàng)目開(kāi)發(fā)和用戶組都有友好的網(wǎng)民愿意幫助。這樣的商業(yè)公司如果需要Katasoft還提供專(zhuān)業(yè)的支持和服務(wù)。
有興趣的可以去仔細(xì)看看官方的文檔:【傳送門(mén)】
Apache Shiro Features 特性
Apache Shiro是一個(gè)全面的、蘊(yùn)含豐富功能的安全框架。下圖為描述Shiro功能的框架圖:

Authentication(認(rèn)證), Authorization(授權(quán)), Session Management(會(huì)話管理), Cryptography(加密)被 Shiro 框架的開(kāi)發(fā)團(tuán)隊(duì)稱(chēng)之為應(yīng)用安全的四大基石。那么就讓我們來(lái)看看它們吧:
- Authentication(認(rèn)證):用戶身份識(shí)別,通常被稱(chēng)為用戶“登錄”
- Authorization(授權(quán)):訪問(wèn)控制。比如某個(gè)用戶是否具有某個(gè)操作的使用權(quán)限。
- Session Management(會(huì)話管理):特定于用戶的會(huì)話管理,甚至在非web 或 EJB 應(yīng)用程序。
- Cryptography(加密):在對(duì)數(shù)據(jù)源使用加密算法加密的同時(shí),保證易于使用。
還有其他的功能來(lái)支持和加強(qiáng)這些不同應(yīng)用環(huán)境下安全領(lǐng)域的關(guān)注點(diǎn)。特別是對(duì)以下的功能支持:
- Web支持:Shiro的Web支持API有助于保護(hù)Web應(yīng)用程序。
- 緩存:緩存是Apache Shiro API中的第一級(jí),以確保安全操作保持快速和高效。
- 并發(fā)性:Apache Shiro支持具有并發(fā)功能的多線程應(yīng)用程序。
- 測(cè)試:存在測(cè)試支持,可幫助您編寫(xiě)單元測(cè)試和集成測(cè)試,并確保代碼按預(yù)期得到保障。
- “運(yùn)行方式”:允許用戶承擔(dān)另一個(gè)用戶的身份(如果允許)的功能,有時(shí)在管理方案中很有用。
- “記住我”:記住用戶在會(huì)話中的身份,所以用戶只需要強(qiáng)制登錄即可。
注意: Shiro不會(huì)去維護(hù)用戶、維護(hù)權(quán)限,這些需要我們自己去設(shè)計(jì)/提供,然后通過(guò)相應(yīng)的接口注入給Shiro
High-Level Overview 高級(jí)概述
在概念層,Shiro 架構(gòu)包含三個(gè)主要的理念:Subject,SecurityManager和 Realm。下面的圖展示了這些組件如何相互作用,我們將在下面依次對(duì)其進(jìn)行描述。

- Subject:當(dāng)前用戶,Subject 可以是一個(gè)人,但也可以是第三方服務(wù)、守護(hù)進(jìn)程帳戶、時(shí)鐘守護(hù)任務(wù)或者其它–當(dāng)前和軟件交互的任何事件。
- SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架構(gòu)的核心,配合內(nèi)部安全組件共同組成安全傘。
- Realms:用于進(jìn)行權(quán)限信息的驗(yàn)證,我們自己實(shí)現(xiàn)。Realm 本質(zhì)上是一個(gè)特定的安全 DAO:它封裝與數(shù)據(jù)源連接的細(xì)節(jié),得到Shiro 所需的相關(guān)的數(shù)據(jù)。在配置 Shiro 的時(shí)候,你必須指定至少一個(gè)Realm 來(lái)實(shí)現(xiàn)認(rèn)證(authentication)和/或授權(quán)(authorization)。
我們需要實(shí)現(xiàn)Realms的Authentication 和 Authorization。其中 Authentication 是用來(lái)驗(yàn)證用戶身份,Authorization 是授權(quán)訪問(wèn)控制,用于對(duì)用戶進(jìn)行的操作授權(quán),證明該用戶是否允許進(jìn)行當(dāng)前操作,如訪問(wèn)某個(gè)鏈接,某個(gè)資源文件等。
Shiro 認(rèn)證過(guò)程

上圖展示了 Shiro 認(rèn)證的一個(gè)重要的過(guò)程,為了加深我們的印象,我們來(lái)自己動(dòng)手來(lái)寫(xiě)一個(gè)例子,來(lái)驗(yàn)證一下,首先我們新建一個(gè)Maven工程,然后在pom.xml中引入相關(guān)依賴(lài):
<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>
新建一個(gè)【AuthenticationTest】測(cè)試類(lèi):
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 // 在方法開(kāi)始前添加一個(gè)用戶
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)證請(qǐng)求
SecurityUtils.setSecurityManager(defaultSecurityManager); // 設(shè)置SecurityManager環(huán)境
Subject subject = SecurityUtils.getSubject(); // 獲取當(dāng)前主體
UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
subject.login(token); // 登錄
// subject.isAuthenticated()方法返回一個(gè)boolean值,用于判斷用戶是否認(rèn)證成功
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出true
subject.logout(); // 登出
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出false
}
}
運(yùn)行之后可以看到預(yù)想中的效果,先輸出isAuthenticated:true表示登錄認(rèn)證成功,然后再輸出isAuthenticated:false表示認(rèn)證失敗退出登錄,再來(lái)一張圖加深一下印象:

流程如下:
- 首先調(diào)用 Subject.login(token) 進(jìn)行登錄,其會(huì)自動(dòng)委托給 Security Manager,調(diào)用之前必須通過(guò) SecurityUtils.setSecurityManager() 設(shè)置;
- SecurityManager 負(fù)責(zé)真正的身份驗(yàn)證邏輯;它會(huì)委托給 Authenticator 進(jìn)行身份驗(yàn)證;
- Authenticator 才是真正的身份驗(yàn)證者,Shiro API 中核心的身份認(rèn)證入口點(diǎn),此處可以自定義插入自己的實(shí)現(xiàn);
- Authenticator 可能會(huì)委托給相應(yīng)的 AuthenticationStrategy 進(jìn)行多 Realm 身份驗(yàn)證,默認(rèn) ModularRealmAuthenticator 會(huì)調(diào)用 AuthenticationStrategy 進(jìn)行多 Realm 身份驗(yàn)證;
- Authenticator 會(huì)把相應(yīng)的 token 傳入 Realm,從 Realm 獲取身份驗(yàn)證信息,如果沒(méi)有返回 / 拋出異常表示身份驗(yàn)證失敗了。此處可以配置多個(gè) Realm,將按照相應(yīng)的順序及策略進(jìn)行訪問(wèn)。
Shiro 授權(quán)過(guò)程

跟認(rèn)證過(guò)程大致相似,下面我們?nèi)匀煌ㄟ^(guò)代碼來(lái)熟悉一下過(guò)程(引入包類(lèi)似這里節(jié)約篇幅就不貼出來(lái)了):
public class AuthenticationTest {
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
@Before // 在方法開(kāi)始前添加一個(gè)用戶,讓它具備admin和user兩個(gè)角色
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)證請(qǐng)求
SecurityUtils.setSecurityManager(defaultSecurityManager); // 設(shè)置SecurityManager環(huán)境
Subject subject = SecurityUtils.getSubject(); // 獲取當(dāng)前主體
UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
subject.login(token); // 登錄
// subject.isAuthenticated()方法返回一個(gè)boolean值,用于判斷用戶是否認(rèn)證成功
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出true
// 判斷subject是否具有admin和user兩個(gè)角色權(quán)限,如沒(méi)有則會(huì)報(bào)錯(cuò)
subject.checkRoles("admin","user");
// subject.checkRole("xxx"); // 報(bào)錯(cuò)
}
}
運(yùn)行測(cè)試,能夠正確看到效果。
自定義 Realm
從上面我們了解到實(shí)際進(jìn)行權(quán)限信息驗(yàn)證的是我們的 Realm,Shiro 框架內(nèi)部默認(rèn)提供了兩種實(shí)現(xiàn),一種是查詢(xún).ini文件的IniRealm,另一種是查詢(xún)數(shù)據(jù)庫(kù)的JdbcRealm,這兩種來(lái)說(shuō)都相對(duì)簡(jiǎn)單,感興趣的可以去【這里】瞄兩眼,我們著重就來(lái)介紹介紹自定義實(shí)現(xiàn)的 Realm 吧。
有了上面的對(duì)認(rèn)證和授權(quán)的理解,我們先在合適的包下創(chuàng)建一個(gè)【MyRealm】類(lèi),繼承 Shirot 框架的 AuthorizingRealm 類(lèi),并實(shí)現(xiàn)默認(rèn)的兩個(gè)方法:
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ù)庫(kù)數(shù)據(jù)
*/
Map<String, String> userMap = new HashMap<>(16);
{
userMap.put("wmyskxz", "123456");
super.setName("myRealm"); // 設(shè)置自定義Realm的名稱(chēng),取什么無(wú)所謂..
}
/**
* 授權(quán)
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String userName = (String) principalCollection.getPrimaryPrincipal();
// 從數(shù)據(jù)庫(kù)獲取角色和權(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ù)庫(kù)中獲取權(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ù)庫(kù)中獲取角色數(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 主體傳過(guò)來(lái)的認(rèn)證信息
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1.從主體傳過(guò)來(lái)的認(rèn)證信息中,獲得用戶名
String userName = (String) authenticationToken.getPrincipal();
// 2.通過(guò)用戶名到數(shù)據(jù)庫(kù)中獲取憑證
String password = getPasswordByUserName(userName);
if (password == null) {
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("wmyskxz", password, "myRealm");
return authenticationInfo;
}
/**
* 模擬從數(shù)據(jù)庫(kù)取憑證的過(guò)程
*
* @param userName
* @return
*/
private String getPasswordByUserName(String userName) {
return userMap.get(userName);
}
}
然后我們編寫(xiě)測(cè)試類(lèi),來(lái)驗(yàn)證是否正確:
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(); // 實(shí)現(xiàn)自己的 Realm 實(shí)例
// 1.構(gòu)建SecurityManager環(huán)境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(myRealm);
// 2.主體提交認(rèn)證請(qǐng)求
SecurityUtils.setSecurityManager(defaultSecurityManager); // 設(shè)置SecurityManager環(huán)境
Subject subject = SecurityUtils.getSubject(); // 獲取當(dāng)前主體
UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
subject.login(token); // 登錄
// subject.isAuthenticated()方法返回一個(gè)boolean值,用于判斷用戶是否認(rèn)證成功
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 輸出true
// 判斷subject是否具有admin和user兩個(gè)角色權(quán)限,如沒(méi)有則會(huì)報(bào)錯(cuò)
subject.checkRoles("admin", "user");
// subject.checkRole("xxx"); // 報(bào)錯(cuò)
// 判斷subject是否具有user:add權(quán)限
subject.checkPermission("user:add");
}
}
運(yùn)行測(cè)試,完美。
Shiro 加密
在之前的學(xué)習(xí)中,我們?cè)跀?shù)據(jù)庫(kù)中保存的密碼都是明文的,一旦數(shù)據(jù)庫(kù)數(shù)據(jù)泄露,那就會(huì)造成不可估算的損失,所以我們通常都會(huì)使用非對(duì)稱(chēng)加密,簡(jiǎn)單理解也就是不可逆的加密,而 md5 加密算法就是符合這樣的一種算法。

如上面的 123456 用 Md5 加密后,得到的字符串:e10adc3949ba59abbe56e057f20f883e,就無(wú)法通過(guò)計(jì)算還原回 123456,我們把這個(gè)加密的字符串保存在數(shù)據(jù)庫(kù)中,等下次用戶登錄時(shí)我們把密碼通過(guò)同樣的算法加密后再?gòu)臄?shù)據(jù)庫(kù)中取出這個(gè)字符串進(jìn)行比較,就能夠知道密碼是否正確了,這樣既保留了密碼驗(yàn)證的功能又大大增加了安全性,但是問(wèn)題是:雖然無(wú)法直接通過(guò)計(jì)算反推回密碼,但是我們?nèi)匀豢梢酝ㄟ^(guò)計(jì)算一些簡(jiǎn)單的密碼加密后的 Md5 值進(jìn)行比較,推算出原來(lái)的密碼
比如我的密碼是 123456,你的密碼也是,通過(guò) md5 加密之后的字符串一致,所以你也就能知道我的密碼了,如果我們把常用的一些密碼都做 md5 加密得到一本字典,那么就可以得到相當(dāng)一部分的人密碼,這也就相當(dāng)于“破解”了一樣,所以其實(shí)也沒(méi)有我們想象中的那么“安全”。
加鹽 + 多次加密
既然相同的密碼 md5 一樣,那么我們就讓我們的原始密碼再加一個(gè)隨機(jī)數(shù),然后再進(jìn)行 md5 加密,這個(gè)隨機(jī)數(shù)就是我們說(shuō)的鹽(salt),這樣處理下來(lái)就能得到不同的 Md5 值,當(dāng)然我們需要把這個(gè)隨機(jī)數(shù)鹽也保存進(jìn)數(shù)據(jù)庫(kù)中,以便我們進(jìn)行驗(yàn)證。
另外我們可以通過(guò)多次加密的方法,即使黑客通過(guò)一定的技術(shù)手段拿到了我們的密碼 md5 值,但它并不知道我們到底加密了多少次,所以這也使得破解工作變得艱難。
在 Shiro 框架中,對(duì)于這樣的操作提供了簡(jiǎn)單的代碼實(shí)現(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, 運(yùn)算次數(shù)是: %d, 運(yùn)算出來(lái)的密文是:%s ",password,salt,times,encodePassword);
輸出:
原始密碼是 123456 , 鹽是: f5GQZsuWjnL9z585JjLrbQ==, 運(yùn)算次數(shù)是: 2, 運(yùn)算出來(lái)的密文是:55fee80f73537cefd6b3c9a920993c25
SpringBoot 簡(jiǎn)單實(shí)例
通過(guò)上面的學(xué)習(xí),我們現(xiàn)在來(lái)著手搭建一個(gè)簡(jiǎn)單的使用 Shiro 進(jìn)行權(quán)限驗(yàn)證授權(quán)的一個(gè)簡(jiǎn)單系統(tǒng)
第一步:新建SpringBoot項(xiàng)目,搭建基礎(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ù)庫(kù)
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語(yǔ)句
spring.jpa.show-sql=true
#不加下面這句則不會(huì)默認(rèn)創(chuàng)建MyISAM引擎的數(shù)據(jù)庫(kù)
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#自己重寫(xiě)的配置類(lèi),默認(rèn)使用utf8編碼
spring.jpa.properties.hibernate.dialect=com.wmyskxz.demo.shiro.config.MySQLConfig
第二步:新建實(shí)體類(lèi)
新建一個(gè)【entity】包,在下面創(chuàng)建以下實(shí)體:
用戶信息:
@Entity
public class UserInfo {
@Id
@GeneratedValue
private Long id; // 主鍵.
@Column(unique = true)
private String username; // 登錄賬戶,唯一.
private String name; // 名稱(chēng)(匿名或真實(shí)姓名),用于UI顯示
private String password; // 密碼.
private String salt; // 加密密碼的鹽
@JsonIgnoreProperties(value = {"userInfos"})
@ManyToMany(fetch = FetchType.EAGER) // 立即從數(shù)據(jù)庫(kù)中進(jìn)行加載數(shù)據(jù)
@JoinTable(name = "SysUserRole", joinColumns = @JoinColumn(name = "uid"), inverseJoinColumns = @JoinColumn(name = "roleId"))
private List<SysRole> roles; // 一個(gè)用戶具有多個(gè)角色
/** getter and setter */
}
角色信息:
@Entity
public class SysRole {
@Id
@GeneratedValue
private Long id; // 主鍵.
private String name; // 角色名稱(chēng),如 admin/user
private String description; // 角色描述,用于UI顯示
// 角色 -- 權(quán)限關(guān)系:多對(duì)多
@JsonIgnoreProperties(value = {"roles"})
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "permissionId")})
private List<SysPermission> permissions;
// 用戶 -- 角色關(guān)系:多對(duì)多
@JsonIgnoreProperties(value = {"roles"})
@ManyToMany
@JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "uid")})
private List<UserInfo> userInfos;// 一個(gè)角色對(duì)應(yīng)多個(gè)用戶
/** getter and setter */
}
權(quán)限信息:
@Entity
public class SysPermission {
@Id
@GeneratedValue
private Long id; // 主鍵.
private String name; // 權(quán)限名稱(chēng),如 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; // 一個(gè)權(quán)限可以被多個(gè)角色使用
/** getter and setter */
}
注意:這里有一個(gè)坑,還纏了我蠻久感覺(jué),就是當(dāng)我們想要使用RESTful風(fēng)格返回給前臺(tái)JSON數(shù)據(jù)的時(shí)候,這里有一個(gè)關(guān)于多對(duì)多無(wú)限循環(huán)的坑,比如當(dāng)我們想要返回給前臺(tái)一個(gè)用戶信息時(shí),由于一個(gè)用戶擁有多個(gè)角色,一個(gè)角色又擁有多個(gè)權(quán)限,而權(quán)限跟角色也是多對(duì)多的關(guān)系,也就是造成了 查用戶→查角色→查權(quán)限→查角色→查用戶... 這樣的無(wú)限循環(huán),導(dǎo)致傳輸錯(cuò)誤,所以我們根據(jù)這樣的邏輯在每一個(gè)實(shí)體類(lèi)返回JSON時(shí)使用了一個(gè)
@JsonIgnoreProperties注解,來(lái)排除自己對(duì)自己無(wú)線引用的過(guò)程,也就是打斷這樣的無(wú)限循環(huán)。
根據(jù)以上的代碼會(huì)自動(dòng)生成user_info(用戶信息表)、sys_role(角色表)、sys_permission(權(quán)限表)、sys_user_role(用戶角色表)、sys_role_permission(角色權(quán)限表)這五張表,為了方便測(cè)試我們給這五張表插入一些初始化數(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,'查詢(xún)用戶','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
新建一個(gè)【config】包,在下面創(chuàng)建以下文件:
MySQLConfig:
public class MySQLConfig extends MySQL5InnoDBDialect {
@Override
public String getTableTypeString() {
return "ENGINE=InnoDB DEFAULT CHARSET=utf8";
}
}
這個(gè)文件關(guān)聯(lián)的是配置文件中最后一個(gè)配置,是讓 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ìn)入這里說(shuō)明用戶已經(jīng)通過(guò)驗(yàn)證了
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());
// 通過(guò)username從數(shù)據(jù)庫(kù)中查找 UserInfo 對(duì)象
// 實(shí)際項(xiàng)目中,這里可以根據(jù)實(shí)際情況做緩存,如果不做,Shiro自己也是有時(shí)間間隔機(jī)制,2分鐘內(nèi)不會(huì)重復(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)過(guò)程一致
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>();
// 配置不會(huì)被攔截的鏈接 順序判斷
filterChainDefinitionMap.put("/static/**", "anon");
// 配置退出 過(guò)濾器,其中的具體的退出代碼Shiro已經(jīng)替我們實(shí)現(xiàn)了
filterChainDefinitionMap.put("/logout", "logout");
// <!-- 過(guò)濾鏈定義,從上向下順序執(zhí)行,一般將/**放在最為下邊 -->:這是一個(gè)坑呢,一不小心代碼就不好使了;
// <!-- authc:所有url都必須認(rèn)證通過(guò)才可以訪問(wèn); anon:所有url都都可以匿名訪問(wèn)-->
filterChainDefinitionMap.put("/**", "authc");
// 如果不設(shè)置默認(rèn)會(huì)自動(dòng)尋找Web工程根目錄下的"/login.jsp"頁(yè)面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登錄成功后要跳轉(zhuǎn)的鏈接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授權(quán)界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 憑證匹配器
* (由于我們的密碼校驗(yàn)交給Shiro的SimpleAuthenticationInfo進(jìn)行處理了)
*
* @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;
}
/**
* 開(kāi)啟shiro aop注解支持.
* 使用代理方式;所以需要開(kāi)啟代碼支持;
*
* @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ù)庫(kù)異常處理
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 的核心通過(guò) Filter 來(lái)實(shí)現(xiàn),就好像 SpringMvc 通過(guò) DispachServlet 來(lái)主控制一樣。 既然是使用 Filter 一般也就能猜到,是通過(guò)URL規(guī)則來(lái)進(jìn)行過(guò)濾和權(quán)限校驗(yàn),所以我們需要定義一系列關(guān)于URL的規(guī)則和訪問(wèn)權(quán)限。
Filter Chain定義說(shuō)明:
- 1、一個(gè)URL可以配置多個(gè)Filter,使用逗號(hào)分隔
- 2、當(dāng)設(shè)置多個(gè)過(guò)濾器時(shí),全部驗(yàn)證通過(guò),才視為通過(guò)
- 3、部分過(guò)濾器可指定參數(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都都可以匿名訪問(wèn)
- authc: 需要認(rèn)證才能進(jìn)行訪問(wèn)
- user:配置記住我或認(rèn)證通過(guò)可以訪問(wèn)
第四步:準(zhǔn)備 DAO 層和 Service 層
新建【dao】包,在下面創(chuàng)建【UserInfoDao】接口:
public interface UserInfoDao extends JpaRepository<UserInfo, Long> {
/** 通過(guò)username查找用戶信息*/
public UserInfo findByUsername(String username);
}
新建【service】包,創(chuàng)建【UserInfoService】接口:
public interface UserInfoService {
/** 通過(guò)username查找用戶信息;*/
public UserInfo findByUsername(String username);
}
并在該包下再新建一個(gè)【impl】包,新建【UserInfoServiceImpl】實(shí)現(xiàn)類(lèi):
@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異常類(lèi)的全類(lèi)名.
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 -- > 賬號(hào)不存在:");
msg = "UnknownAccountException -- > 賬號(hào)不存在:";
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
System.out.println("IncorrectCredentialsException -- > 密碼不正確:");
msg = "IncorrectCredentialsException -- > 密碼不正確:";
} else if ("kaptchaValidateFailed".equals(exception)) {
System.out.println("kaptchaValidateFailed -- > 驗(yàn)證碼錯(cuò)誤");
msg = "kaptchaValidateFailed -- > 驗(yàn)證碼錯(cuò)誤";
} else {
msg = "else >> "+exception;
System.out.println("else -- >" + exception);
}
}
map.put("msg", msg);
// 此方法不處理登錄成功,由shiro進(jìn)行處理
return "/login";
}
@RequestMapping("/403")
public String unauthorizedRole(){
System.out.println("------沒(méi)有權(quán)限-------");
return "403";
}
}
這里邊的地址對(duì)應(yīng)我們?cè)谠O(shè)置 Shiro 時(shí)設(shè)置的地址
UserInfoController:
@RestController
public class UserInfoController {
@Resource
UserInfoService userInfoService;
/**
* 按username賬戶從數(shù)據(jù)庫(kù)中取出用戶信息
*
* @param username 賬戶
* @return
*/
@GetMapping("/userList")
@RequiresPermissions("userInfo:view") // 權(quán)限管理.
public UserInfo findUserInfoByUsername(@RequestParam String username) {
return userInfoService.findByUsername(username);
}
/**
* 簡(jiǎn)單模擬從數(shù)據(jù)庫(kù)添加用戶信息成功
*
* @return
*/
@PostMapping("/userAdd")
@RequiresPermissions("userInfo:add")
public String addUserInfo() {
return "addUserInfo success!";
}
/**
* 簡(jiǎn)單模擬從數(shù)據(jù)庫(kù)刪除用戶成功
*
* @return
*/
@DeleteMapping("/userDelete")
@RequiresPermissions("userInfo:delete")
public String deleteUserInfo() {
return "deleteUserInfo success!";
}
}
第六步:準(zhǔn)備頁(yè)面
新建三個(gè)頁(yè)面用來(lái)測(cè)試:
index.html:首頁(yè)
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>首頁(yè)</title>
</head>
<body>
index - 首頁(yè)
</body>
</html>
login.html:登錄頁(yè)
<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>登錄頁(yè)</title>
</head>
<body>
錯(cuò)誤信息:<h4 th:text="${msg}"></h4>
<form action="" method="post">
<p>賬號(hào):<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:沒(méi)有權(quán)限的頁(yè)面
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>403錯(cuò)誤頁(yè)</title>
</head>
<body>
錯(cuò)誤頁(yè)面
</body>
</html>
第七步:測(cè)試
- 編寫(xiě)好程序后就可以啟動(dòng),首先訪問(wèn)
http://localhost:8080/userList?username=wmyskxz頁(yè)面,由于沒(méi)有登錄就會(huì)跳轉(zhuǎn)到我們配置好的http://localhost:8080/login頁(yè)面。登陸之后就會(huì)看到正確返回的JSON數(shù)據(jù),上面這些操作時(shí)候觸發(fā)MyShiroRealm.doGetAuthenticationInfo()這個(gè)方法,也就是登錄認(rèn)證的方法。 - 登錄之后,我們還能訪問(wèn)
http://localhost:8080/userAdd頁(yè)面,因?yàn)槲覀冊(cè)跀?shù)據(jù)庫(kù)中提前配置好了權(quán)限,能夠看到正確返回的數(shù)據(jù),但是我們?cè)L問(wèn)http://localhost:8080/userDelete時(shí),就會(huì)返回錯(cuò)誤頁(yè)面.
注意:以上測(cè)試需要在REST工具中測(cè)試,因?yàn)樵贑ontroller層中配置了方法,大家也可以不用REST風(fēng)格來(lái)測(cè)試一下看看!
完成了以上的學(xué)習(xí),我們就差不多對(duì) Shiro 框架有了一定了解了,更多的東西以后再分享再學(xué)習(xí)吧.
參考資料:
springboot(十四):springboot整合shiro-登錄認(rèn)證和權(quán)限管理——純潔的微笑
Shiro安全框架入門(mén) - 慕課視頻教程
Shiro 系列教程 —— how2j網(wǎng)站
歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處!
簡(jiǎn)書(shū)ID:@我沒(méi)有三顆心臟
github:wmyskxz
歡迎關(guān)注公眾微信號(hào):wmyskxz
分享自己的學(xué)習(xí) & 學(xué)習(xí)資料 & 生活
想要交流的朋友也可以加qq群:3382693