自定義的 Realm 使得我們使用安全資料源更加靈活,在自定義的 Realm 中,我們就可以自己控制認證和授權的邏輯了。同時我們還簡單介紹了一下在多個 Realm 同時存在的情況下,我們可以配置我們的認證政策來滿足我們的需求。
前面兩節我們已經介紹過 IniRealm 和 JdbcRealm,這一節我們介紹自定義的 Realm 實作我們自己的安全資料源。
方式一:implements Realm (這種方式不太常用,隻是為了說明知識)
這種方式實作的 Realm 僅隻能實作認證操作,并不能實作授權操作。
代碼:
public class MapRealm implements Realm {
private static Map<String,String> users;
static{
users = new HashMap<>();
users.put("liwei","123456");
users.put("zhouguang","666666");
}
/**
* 傳回一個唯一的 Realm 名字
* @return
*/
public String getName() {
System.out.println("Map Realm 中設定 Realm 名字的方法");
return "MyStaticRealm";
}
/**
* 判斷此 Realm 是否支援此 Token
* @param authenticationToken
* @return
*/
public boolean supports(AuthenticationToken authenticationToken) {
System.out.println("Map Realm 中給出支援的 Token 的方法");
// 表示僅支援 UsernamePasswordToken 類型的 Token
return authenticationToken instanceof UsernamePasswordToken;
}
/**
* 根據 Token 擷取認證資訊
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("Map Realm 中傳回認證資訊的方法");
String userName = (String)authenticationToken.getPrincipal();
String password = new String((char[])authenticationToken.getCredentials());
System.out.println("token 中的使用者名:" + userName);
System.out.println("token 中的密碼:" + password);
if(!users.containsKey(userName)){
throw new UnknownAccountException("沒有這個使用者!");
}else if(!password.equals(users.get(userName))){
throw new IncorrectCredentialsException("密碼錯誤!");
}
return new SimpleAuthenticationInfo(userName,password,getName());
}
}
接下來我們要在 shiro.ini 檔案中聲明我們要是用的這個 Realm。
[main]
#聲明了我們自己定義的一個 Realm
myMapRealm=com.liwei.realm.MapRealm
#将我們自己定義的 Realm 注入到 securityManager 的 realms 屬性中去
securityManager.realms=$myMapRealm
方式二:extends AuthorizingRealm(比較常用的一種方式,因為這樣做既可以實作認證操作,也可以實作授權操作)
示例代碼:
package com.shiro.test;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
*
* @ClassName: MyRealm
* @Description: 自定義的realm
* @author cheng
* @date
public class MyRealm extends AuthorizingRealm
/**
* 用于認證
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// token是使用者輸入的
// 1.從token中取出使用者資訊
String userCode = (String) token.getPrincipal();
// 2.依據使用者資訊從資料庫中查詢
// 如果沒有查詢到,傳回null,抛出UnknownAccountException
// if(userCode != null){
// return null;
// }
// ......
// 模拟從資料庫中查詢得到密碼
String password = "111111";
// 如果查詢到了,傳回AuthenticationInfo
// 如果密碼不比對,抛出IncorrectCredentialsException
// 如果查詢到傳回認證資訊AuthenticationInfo
//activeUser就是使用者身份資訊(ActiveUser為自定義的pojo)
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid("zhangsan");
activeUser.setUsercode("zhangsan");
activeUser.setUsername("張三");
//..
//根據使用者id取出菜單
//通過service取出菜單
List<SysPermission> menus = null;
try {
menus = sysService.findMenuListByUserId("zhangsan");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//将使用者菜單 設定到activeUser
activeUser.setMenus(menus);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
activeUser, password, this.getName());
return simpleAuthenticationInfo;
}
/**
* 用于授權
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principal) {
// 從 principal擷取主身份資訊
// 将getPrimaryPrincipal方法傳回值轉為真實身份類型
// 在上邊的doGetAuthenticationInfo認證通過填充到SimpleAuthenticationInfo中身份類型
String userCode = (String) principal.getPrimaryPrincipal();
// 根據身份資訊擷取權限資訊
// 連接配接資料庫...
// 模拟從資料庫擷取到資料
List<String> permissions = new ArrayList<String>();
permissions.add("user:create");// 使用者的建立
permissions.add("items:add");// 商品添權重限
// ....
// 查到權限資料,傳回授權資訊(要包括 上邊的permissions)
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 将上邊查詢到授權資訊填充到simpleAuthorizationInfo對象中
simpleAuthorizationInfo.addStringPermissions(permissions);
return
說明:上面的寫作也隻是為了測試,真正在生産環境中,應該通過查詢資料庫去完成認證和授權的相關操作。
擷取使用者資訊
//從shiro的session中取activeUser
Subject subject = SecurityUtils.getSubject();
//取身份資訊
ActiveUser activeUser = (ActiveUser) subject.getPrincipal();
//通過model傳到頁面
model.addAttribute("activeUser", activeUser);
下面這一行說明的事實是很重要的:
在 doGetAuthenticationInfo() 方法中須要傳回一個正确的 SimpleAuthenticationInfo 對象,這樣 Shiro 就會和 Subject 的 login() 方法中傳入的 token 資訊進行比對,完成認證的操作。
然後我們在 shiro.ini 中也要配置這個自定義的 Realm:
代碼:
[main]
#自定義的realm
myRealm=com.shiro.test.MyRealm
#注入自定義的realm
securityManager.realms=$myRealm
知識點:配置認證政策
這時候,我們會有一個疑問,securityManager 的屬性既然是 realms,說明可以設定若幹個 Realm,它們認證的順序是如何的呢。
Shiro 會按照我們聲明的順序,依次驗證。在使用了 ini 檔案啟動 Shiro 的方式中,IniRealm 在 Shiro 中是預設使用的(我個人覺得應該是第一個使用的,即使在我們不聲明的情況下)。
那麼對于若幹個 Realm,Shiro 提供了一種配置方式,讓我們來決定在多個 Reaml 同時聲明的情況下,采用哪些 Realm 傳回的認證資訊的方式,這就是我們的認證政策。
認證政策主要有以下三種:
1、FirstSuccessfulStrategy:隻要有一個 Realm 驗證成功即可,隻傳回第一個 Realm 身份驗證成功的認證資訊,其他的忽略;
2、AtLeastOneSuccessfulStrategy: (這是預設使用的認證政策,即在不配置情況下 Shiro 所采用的認證政策)隻要有一個 Realm 驗證成功即可, 和 FirstSuccessfulStrategy 不同,傳回所有 Realm 身份驗證成功的認證資訊;
3、AllSuccessfulStrategy:所有 Realm 驗證成功才算成功,且傳回所有 Realm 身份驗證成功的認證資訊,如果有一個失敗就失敗了。
配置示例:
配置認證政策
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrateg
配置好以後,我們可以通過以下的方法來驗證我們剛剛配置的 Shiro 的認證政策。
currentSubject.login(token);
PrincipalCollection ps = currentSubject.getPrincipals();
System.out.println(ps.asList());
System.out.println(ps.getRealmNames());
System.out.println(currentSubject.getPrincipals());
自定義 Realm 就介紹到這裡了。自定義 Realm 是很重要的,特别是 extends AuthorizingRealm 這種方式。
學習到這裡想再強調一下 extends AuthorizingRealm 這種方式覆寫的 doGetAuthenticationInfo() 方法,一開始我不是很明白,這個方法到底是做什麼的,就上面我們舉例,我想再解釋一下。
一般地,我們從參數 AuthenticationToken 對象中取出使用者填寫的使用者名和密碼資訊,這個 token 其實就是 Subject 使用 login() 方法中傳入的 UsernamePasswordToken 對象,我們通過這個 UsernamePasswordToken 對象獲得使用者填寫的使用者名和密碼,然後我們應該通過使用者的使用者名去資料庫查詢資料庫是否有這個使用者名,如果沒有,抛出一個使用者名不存在異常;如果使用者名存在,傳回一個使用者對象(帶密碼的),再用資料庫傳回的密碼資料和使用者填寫的密碼資料進行比對,如果錯誤,就抛出異常,如果正确,就要把正确的使用者名和密碼資訊封裝成一個 SimpleAuthenticationInfo 對象傳回,這才是一個比較完整并且正确的流程。
另外再補充一下,我在上面的例子中說到“使用者填寫的使用者名和密碼”是一種為了友善了解的說法。下面介紹官方正确的說法。
principals:身份,即主體的辨別屬性,可以是任何東西,如使用者名、郵箱等,唯一即可。一個主體可以有多個 principals, 但隻有一個 Primary principals, 一般是使用者名/密碼/手機号。
credentials:證明/憑證,即隻有主體知道的安全值,如密碼/數字證書等。