Shiro 身份認證實現
一. 身份認證
身份驗證,即在應用中誰能證明他就是他本人。一般提供如他們的身份ID一些標識資訊來表明他就是他本人,如提供身份證,使用者名稱 / 密碼來證明。
在 shiro 中,使用者需要提供 principals (身份)和 credentials(證明)給 shiro,從而應用能驗證使用者身份:
①principals:身份,即主體的標識屬性,可以是任何東西,如使用者名稱、郵箱等,唯一即可。一個主體可以有多個 principals,但只有一個 Primary principals,一般是使用者名稱 / 密碼 / 手機號。
②credentials
最常見的 principals 和 credentials 組合就是使用者名稱 / 密碼了,另外還有兩個相關的概念就是Subject 及 Realm,分別是主體及驗證主體的資料來源。
二. 身份認證流程
身份認證流程如下:
流程如下:
- 首先呼叫 Subject.login(token) 進行登入,其會自動委託給 Security Manager,呼叫之前必須通過 SecurityUtils.setSecurityManager() 設定;
- SecurityManager 負責真正的身份驗證邏輯;它會委託給 Authenticator 進行身份驗證;
- Authenticator 才是真正的身份驗證者,Shiro API 中核心的身份認證入口點,此處可以自定義插入自己的實現;
- Authenticator 可能會委託給相應的 AuthenticationStrategy 進行多 Realm 身份驗證,預設 ModularRealmAuthenticator 會呼叫 AuthenticationStrategy 進行多 Realm 身份驗證;
- Authenticator 會把相應的 token 傳入 Realm,從 Realm 獲取身份驗證資訊,如果沒有返回 / 丟擲異常表示身份驗證失敗了。此處可以配置多個 Realm,將按照相應的順序及策略進行訪問。
三. 身份認證簡單實現
1.環境準備
給專案匯入junit、common-logging 及 shiro-core 依賴。筆者使用的是maven,直接下載jar包然後匯入專案也可以。
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.2</version>
</dependency>
</dependencies>
2.配置使用者身份/憑據
本文簡單程式沒有涉及到資料庫,所以需要在src/main/java下建立shiro.ini檔案,用於儲存使用者賬戶名密碼等資訊。
[users]
zhang=123
wang=456
在這裡使用 ini 配置檔案,通過 [users] 指定了兩個主體:zhang/123、wang/456,模擬資料庫中使用者名稱zhang密碼123、使用者名稱wang密碼456的兩條使用者資訊。
3.編寫測試用例
public class AuthenticationTest {
@Test
public void testLoginAndLogout() {
// 1、獲取SecurityManager工廠,此處使用Ini配置檔案初始化SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
// 2、得到SecurityManager例項 並繫結給SecurityUtils
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 3、得到Subject及建立使用者名稱/密碼身份驗證Token(即使用者身份/憑證,由使用者輸入)
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
try {
// 4、登入,即身份驗證
subject.login(token);
} catch (AuthenticationException e) {
// 5、身份驗證失敗
e.printStackTrace();
}
Assert.assertEquals(true, subject.isAuthenticated()); // 斷言使用者已經登入
// 6、退出
subject.logout();
}
}
上面程式碼過程如下:
- 首先通過 new IniSecurityManagerFactory 並指定一個 ini 配置檔案來建立一個 SecurityManager 工廠;
- 接著獲取 SecurityManager 並繫結到 SecurityUtils,這是一個全域性設定,設定一次即可;
- 通過 SecurityUtils 得到 Subject,其會自動繫結到當前執行緒;如果在 web 環境在請求結束時需要解除繫結;然後獲取身份驗證的 Token,如使用者名稱 / 密碼;
- 呼叫 subject.login 方法進行登入,其會自動委託給 SecurityManager.login 方法進行登入;
- 如果身份驗證失敗請捕獲 AuthenticationException 或其子類,常見的如: DisabledAccountException(禁用的帳號)、LockedAccountException(鎖定的帳號)、UnknownAccountException(錯誤的帳號)、ExcessiveAttemptsException(登入失敗次數過多)、IncorrectCredentialsException (錯誤的憑證)、ExpiredCredentialsException(過期的憑證)等,具體請檢視其繼承關係;對於頁面的錯誤訊息展示,最好使用如 “使用者名稱 / 密碼錯誤” 而不是 “使用者名稱錯誤”/“密碼錯誤”,防止一些惡意使用者非法掃描帳號庫;
- 最後可以呼叫 subject.logout 退出,其會自動委託給 SecurityManager.logout 方法退出。
從如上程式碼可總結出身份驗證的步驟:
①收集使用者身份 / 憑證,即如使用者名稱 / 密碼;
②呼叫 Subject.login 進行登入,如果失敗將得到相應的 AuthenticationException 異常,根據異常提示使用者錯誤資訊;否則登入成功;
③最後呼叫 Subject.logout 進行退出操作。
從上面的測試類中我們發現的幾個問題:
①使用者名稱 / 密碼硬編碼在 ini 配置檔案,以後需要改成如資料庫儲存,且密碼需要加密儲存;
②使用者身份 Token 可能不僅僅是使用者名稱 / 密碼,也可能還有其他的,如登入時允許使用者名稱 / 郵箱 / 手機號同時登入。
上面的例子是直接使用ini配置檔案儲存使用者名稱密碼,但在日常應用開發中我們肯定是要使用資料庫儲存的。而要使用資料庫儲存,如何使用Shiro來訪問資料庫,就需要藉助下面我們要將的Realm了。
四. 身份認證Realm配置
Realm:域,Shiro 從從 Realm 獲取安全資料(如使用者、角色、許可權),就是說 SecurityManager 要驗證使用者身份,那麼它需要從 Realm 獲取相應的使用者進行比較以確定使用者身份是否合法,也需要從 Realm 得到使用者相應的角色 / 許可權進行驗證使用者是否能進行操作。
在Shiro架構中,org.apache.shiro.realm.Realm 介面如下:
String getName(); //返回一個唯一的Realm名字
boolean supports(AuthenticationToken token); //判斷此Realm是否支援此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException; //根據Token獲取認證資訊
1.單Realm配置
通常我們自定義Realm,可以通過實現上面的Realm介面,也可以通過繼承AuthorizingRealm抽象類。
① 實現Realm介面:
public class MyRealm1 implements Realm {
public String getName() {
return "myrealm1";
}
public boolean supports(AuthenticationToken token) {
// 僅支援UsernamePasswordToken型別的Token
return token instanceof UsernamePasswordToken;
}
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal(); // 得到使用者名稱
String password = new String((char[]) token.getCredentials()); // 得到密碼
if (!"zhang".equals(username)) {
throw new UnknownAccountException(); // 如果使用者名稱錯誤
}
if (!"123".equals(password)) {
throw new IncorrectCredentialsException(); // 如果密碼錯誤
}
// 如果身份認證驗證成功,返回一個AuthenticationInfo實現;
return new SimpleAuthenticationInfo(username, password, getName());
}
}
②繼承AuthorizingRealm抽象類:
public class MyRealm2 extends AuthorizingRealm {
//設定realm的名稱
@Override
public void setName(String name) {
super.setName("myrealm2");
}
//用於認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal(); // 得到使用者名稱
String password = new String((char[]) token.getCredentials()); // 得到密碼
if (!"zhang".equals(username)) {
throw new UnknownAccountException(); // 如果使用者名稱錯誤
}
if (!"123".equals(password)) {
throw new IncorrectCredentialsException(); // 如果密碼錯誤
}
// 如果身份認證驗證成功,返回一個AuthenticationInfo實現;
return new SimpleAuthenticationInfo(username, password, getName());
}
//用於授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
無論是通過哪種方式實現的自定義Realm,我們都要配置ini 配置檔案來指定securityManager的realm實現:
#宣告一個realm
myRealm1=org.dan.realm.MyRealm1
#指定securityManager的realms實現
securityManager.realms=$myRealm1
2.多Realm配置
ini配置檔案:
#宣告一個realm
myRealm1=org.dan.realm.MyRealm1
myRealm2=org.dan.realm.MyRealm2
#指定securityManager的realms實現
securityManager.realms=$myRealm1,$myRealm2
securityManager 會按照 realms 指定的順序進行身份認證。此處我們使用顯示指定順序的方式指定了 Realm 的順序,如果刪除 “securityManager.realms=$myRealm1,$myRealm2”,那麼securityManager 會按照 realm 宣告的順序進行使用(即無需設定 realms 屬性,其會自動發現),當我們顯示指定 realm 後,其他沒有指定 realm 將被忽略,如 “securityManager.realms=$myRealm1”,那麼 myRealm2 不會被自動設定進去。
到這裡我們已經能夠使用Shiro完成認證功能了,但是這裡我們是通過使用者輸入的密碼直接去查詢資料庫中的明文密碼,而往往為了保護使用者資訊所以資料庫中的密碼都是經過了MD5的演算法進行加密後才放入資料庫的,所以實際開發中我們需要通過Shiro獲取資料庫中經過md5加密後的密碼來和使用者輸入的密碼進行比對。所以接下來我們將學習Shiro中是如何將使用者輸入的資訊與資料庫中的加密資訊進行對比從而實現的認證。
五. 身份加密認證
實際開發中為了保護使用者資訊的安全,我們需要對使用者在註冊時輸入的密碼進行加密後再儲存到資料庫,當用戶登入時我們也要將使用者輸入的密碼進行加密後再與資料庫中的密碼進行比對。即需要對密碼進行雜湊,常用的雜湊方法有md5、sha。
用md5演算法對密碼進行雜湊的問題:如果知道雜湊後的值可以通過窮舉演算法得到md5密碼對應的明文。解決方法:建議對md5進行雜湊時加salt(鹽),進行加密相當於對原始密碼+鹽進行雜湊。
接下來通過一個測試例子來看怎麼實現加密認證。
1.自定義realm支援雜湊演算法
建立CustomRealmMd5.java類,並在裡面模仿資料庫中的使用者名稱和加密密文,內容如下:
public class CustomRealmMd5 extends AuthorizingRealm {
// 設定realm的名稱
@Override
public void setName(String name) {
super.setName("customRealmMd5");
}
// 用於認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// token是使用者輸入的
// 第一步從token中取出身份資訊
String userCode = (String) token.getPrincipal();
// 第二步:根據使用者輸入的userCode從資料庫查詢
// ....
// 如果查詢不到返回null
// 資料庫中使用者賬號是zhangsansan
/*
* if(!userCode.equals("zhangsansan")){// return null; }
*/
// 模擬根據使用者名稱從資料庫查詢到的密碼,雜湊值
String password = "f3694f162729b7d0254c6e40260bf15c";
// 從資料庫獲取salt
String salt = "qwerty";
//上邊雜湊值和鹽對應的明文:111111
// 如果查詢到返回認證資訊AuthenticationInfo
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
userCode, password, ByteSource.Util.bytes(salt), this.getName());
return simpleAuthenticationInfo;
}
// 用於授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
}
2.定義ini配置檔案
[main]
#自定憑證匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#雜湊的演算法
credentialsMatcher.hashAlgorithmName=md5
#雜湊的次數
credentialsMatcher.hashIterations=1
#將憑證匹配器設定到我們定義的realm
customRealm=realm.CustomRealmMd5
customRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$customRealm
3.測試
public class MD5Test {
//註解用的main方法進行測試,你也可以通過junit.jar進行測試
public static void main(String[] args) {
//模擬使用者輸入的密碼
String source="111111";
//加入我們的鹽salt
String salt="qwerty";
//密碼11111經過雜湊1次得到的密碼:f3694f162729b7d0254c6e40260bf15c
int hashIterations=1;
//構造方法中:
//第一個引數:明文,原始密碼
//第二個引數:鹽,通過使用隨機數
//第三個引數:雜湊的次數,比如雜湊兩次,相當 於md5(md5(''))
Md5Hash md5Hash=new Md5Hash(source,salt,hashIterations);
String password_md5=md5Hash.toString();
System.out.println(password_md5);
}
}
自此,我們就完成了簡單的Shiro身份認證實現(包括普通認證和實際開發中的經過雜湊演算法後的認證)。