Shiro
權限管理
什麼是權限管理?
基本上涉及到用戶參與的系統都要進行權限管理,權限管理屬於系統安全的範疇,權限管理實現 對用戶訪問系統的控制,按照 安全規則 或者 安全策略 控制用戶可以訪問而且只能訪問自己被授權的資源。
權限管理包括用戶 身份認證 和 授權 兩部分,簡稱 認證授權。對於需要訪問控制的資源用戶首先經過身份認證,認證通過後用戶具有該資源的訪問權限方可訪問。
什麼是身份認證?
身份認證,就是判斷一個用戶是否爲合法用戶的處理過程。最常用的簡單身份認證方式是系統通過覈對用戶輸入的用戶名和口令,看其是否與系統中存儲的該用戶的用戶名和口令一致,來判斷用戶身份是否正確。對於採用指紋等系統,則出示指紋;對於硬件Key等刷卡系統,則需要刷卡。
什麼是授權?
授權,即 訪問控制,控制誰能訪問哪些資源。主體進行身份認證後需要分配權限方可訪問系統的資源,對於某些資源沒有權限是無法訪問的。
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.
Shiro 是一個功能強大且易於使用的Java安全框架,它執行身份驗證、授權、加密和會話管理。使用Shiro易於理解的API,您可以快速輕鬆地保護任何應用程序—從最小的移動應用程序到最大的web和企業應用程序。
簡單來說,Shiro 是 apache 旗下一個 開源框架,它將軟件系統的安全認證相關的功能抽取出來,實現 用戶身份認證,權限授權、加密、會話管理 等功能,組成了一個 通用的安全認證框架。
Shiro 的核心架構
Subject:主體
- 外部應用與 Subject 進行交互,Subject 記錄了當前操作用戶,將用戶的概念理解爲當前操作的主體,可能是一個通過瀏覽器請求的用戶,也可能是一個運行的程序。 Subject 在 Shiro 中是一個接口,接口中定義了很多認證授相關的方法,外部程序通過 Subject 進行認證授權,而 Subject 是通過 SecurityManager 安全管理器進行認證授權;
SecurityManager:安全管理器
- 對全部的 Subject 進行安全管理,它是 Shiro 的核心,負責對所有的 Subject 進行安全管理。通過SecurityManager 可以完成對 Subject 的認證、授權等,實質上 SecurityManager 是通過 Authenticator 進行認證,通過 Authorizer 進行授權,通過 SessionManager 進行會話管理等。
- SecurityManager 是一個接口,繼承了 Authenticator、Authorizer、SessionManager 這三個接口。
Authenticator:認證器
- 對用戶身份進行認證,Authenticator 是一個接口,Shiro 提供 ModularRealmAuthenticator 實現類,通過 ModularRealmAuthenticator 基本上可以滿足大多數需求,也可以 自定義認證器。
Authorizer:授權器
- 用戶通過認證器認證通過,在訪問功能時需要通過授權器 判斷用戶是否有此功能的操作權限。
Realm:領域
- 相當於 datasource 數據源,securityManager 進行安全認證需要 通過 Realm 獲取用戶權限數據,比如:如果用戶身份數據在數據庫那麼 Realm 就需要從數據庫獲取用戶身份信息。
注意:不要把 Realm 理解成只是從數據源取數據,在 Realm 中還有認證授權校驗的相關的代碼。
SessionManager:會話管理
Shiro 框架定義了一套會話管理,它不依賴 web 容器的 session,所以 Shiro 可以使用在非 web 應用上,也可以將分佈式應用的會話集中在一點管理,此特性可使它實現 單點登錄(多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統)。
SessionDAO:會話DAO
SessionDAO 是對 session 會話操作的一套接口,比如要將 session 存儲到數據庫,可以通過 JDBC 將會話存儲到數據庫。
CacheManager:緩存管理
CacheManager 將用戶權限數據存儲在緩存,這樣可以提高性能。
Cryptography:密碼管理
Shiro 提供了一套加密/解密的組件,方便開發。比如提供常用的散列、加/解密等功能。
Shiro 中的認證
身份認證,就是判斷一個用戶是否爲合法用戶的處理過程。最常用的簡單身份認證方式是系統通過覈對用戶輸入的用戶名和口令,看其是否與系統中存儲的該用戶的用戶名和口令一致,來判斷用戶身份是否正確。
認證關鍵對象
- Subject:主體
訪問系統的用戶,主體可以是用戶、程序等,進行認證的都稱爲主體; - Principal:身份信息
身份信息是主體 (subject) 進行身份認證的標識,標識必須具有 唯一性,如用戶名、手機號、郵箱地址等,一個主體可以有多個身份,但是必須有一個主身份(Primary Principal)。 - credential:憑證信息
憑證信息是隻有主體自己知道的安全信息,如密碼、證書等。
認證流程
認證的開發
創建項目並引入依賴:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
引入 shiro.ini 配置文件並添加如下配置:
zhenyu=123
zhangsan=456
編寫進行認證的代碼:
public class TestAuthenticator {
public static void main(String[] args) {
// 創建安全管理器對象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 給安全管理器設置realm, 從配置文件中獲取數據
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
// 給全局安全工具類設置默認安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 獲取主體對象
Subject subject = SecurityUtils.getSubject();
// 創建token令牌
UsernamePasswordToken token = new UsernamePasswordToken("zhenyu", "123");
try {
// 用戶登錄
System.out.println("認證狀態:" + subject.isAuthenticated());
subject.login(token);
System.out.println("認證狀態:" + subject.isAuthenticated());
} catch (UnknownAccountException e) { // 用戶名不存在異常
e.printStackTrace();
System.out.println("認證失敗: 用戶名不存在!");
} catch (IncorrectCredentialsException e) { // 密碼錯誤異常
e.printStackTrace();
System.out.println("認證失敗: 密碼錯誤!");
}
}
}
-
DisabledAccountException(帳號被禁用)
-
LockedAccountException(帳號被鎖定)
-
ExcessiveAttemptsException(登錄失敗次數過多)
-
ExpiredCredentialsException(憑證過期)
-
…
自定義 Realm
SimpleAccountRealm
上邊的程序使用的是 Shiro 自帶的 IniRealm
,IniRealm
從 ini配置文件 中讀取用戶的信息,實際中大部分情況下需要從系統的數據庫中讀取用戶信息,所以需要自定義 Realm。
Shiro 提供過的 Realm:
根據認證源碼,認證使用的是 SimpleAccountRealm:
SimpleAccountRealm 的部分源碼中有兩個方法一個是 認證,一個是 授權;
認證方法 doGetAuthenticationInfo:
開發自定義 Realm
自定義 Realm,編寫一個 CustomerRealm:
/**
* 自定義realm實現 將認證|授權數據的來源轉爲數據庫的實現
*/
public class CustomerRealm extends AuthorizingRealm {
// 授權方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
// 認證方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 在token中獲取用戶名
String principal = (String) token.getPrincipal();
System.out.println(principal);
// 根據身份信息連接數據庫查詢相關數據庫
if ("zhenyu".equals(principal)) {
// 參數1:返回數據庫中正確的用戶名
// 參數2:返回數據庫中正確密碼
// 參數3:提供當前realm的名字 this.getName();
return new SimpleAuthenticationInfo(principal, "123", this.getName());
}
return null;
}
}
使用自定義 Realm 進行認證:
/**
* 使用自定義realm
*/
public class TestCustomerRealmAuthenticator {
public static void main(String[] args) {
// 創建securityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 設置自定義realm
defaultSecurityManager.setRealm(new CustomerRealm());
// 給全局安全工具類設置默認安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 通過安全工具類獲取subject
Subject subject = SecurityUtils.getSubject();
// 創建token
UsernamePasswordToken token = new UsernamePasswordToken("zhenyu", "123");
try {
subject.login(token);
System.out.println("認證狀態: " + subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用戶名錯誤!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密碼錯誤!");
}
}
}
MD5 和 Salt
實際應用是將 鹽 和 散列後的值 存在數據庫中,Realm 從數據庫取出鹽和加密後的值由 Shiro 完成密碼校驗。
Md5Hsah 類的簡單使用:
public class TestShiroMD5 {
public static void main(String[] args) {
// 使用 MD5
Md5Hash md5Hash = new Md5Hash("1234");
System.out.println(md5Hash.toHex());
// 81dc9bdb52d04dc20036dbd8313ed055
// 使用 Md5 + salt
Md5Hash md5Hash1 = new Md5Hash("1234", "X0*7ps");
System.out.println(md5Hash1);
// 6029a2a0be49f2d4f21941c8ae2cea0c
// 使用 Md5 + salt + hash 散列
Md5Hash md5Hash2 = new Md5Hash("1234", "X0*7ps", 1024);
System.out.println(md5Hash2.toHex());
// 67cdf0cac7bdd508f560ef7965e8934c
}
}
自定義 md5 + salt 的 Realm 並驗證
自定義 Realm 類:
/**
* 自定義md5+salt realm
*/
public class CustomerMd5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
if ("zhenyu".equals(principal)) {
String password = "6029a2a0be49f2d4f21941c8ae2cea0c";
String salt = "X0*7ps";
return new SimpleAuthenticationInfo(principal, password,
ByteSource.Util.bytes(salt), this.getName());
}
return null;
}
}
public class TestCustomerRealmAuthenticator {
public static void main(String[] args) {
// 創建securityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 設置爲自定義realm獲取認證數據
CustomerMd5Realm customerMd5Realm = new CustomerMd5Realm();
// 設置md5加密
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5"); // 設置使用的加密算法
credentialsMatcher.setHashIterations(1024); // 設置散列次數
customerMd5Realm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(customerMd5Realm);
// 安裝工具類中設置默認安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 獲取主體對象
Subject subject = SecurityUtils.getSubject();
// 創建token令牌
UsernamePasswordToken token = new UsernamePasswordToken("zhenyu", "1234");
try {
subject.login(token);//用戶登錄
System.out.println("登錄成功~~");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用戶名錯誤!!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密碼錯誤!!!");
}
}
}