SpringBoot 與Shiro 整合系列(三)多Realm驗證和認證策略

12程式猿發表於2020-11-03

系列(三)講述 SpringBoot整合Shiro 實現多Realm驗證以及認證策略


shiro是一個很好的登陸以及許可權管理框架,但是預設是單realm單資料表,但是很多業務需求單realm是很難實現登陸以及許可權管理的功能(比如業務中使用者分佈在不同的資料表),這個時候就需要使用多個Realm。

一、使用場景

存在這樣一種場景,我們可能會把安全資料放到不同的資料庫,比方說 mysql裡有,oracle裡也有,mysql裡面的加密演算法是MD5,oracle裡面的加密演算法是SHA1。這個時候我們進行使用者認證時,就需要同時訪問這兩個資料庫,就需要多個Realm。如果有多個Realm的話,還需要涉及到認證策略的問題。

二、多Realm驗證

多重認證,主要的類是ModularRealmAuthenticator他有兩個需要配置的屬性,一個是Collection(用於儲存Realm),另一個是AuthenticationStrategy(用於儲存驗證的策略 )

通過檢視原始碼可以看到 ModularRealmAuthenticator.class 中的 doAuthenticate方法:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {//一個realm
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {//多個realm
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }

從上面doAuthenticate方法中可以看到:

如果有一個Realm 使用的是:

  • doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);

如果有多個Realm 使用的是:

  • doMultiRealmAuthentication(realms, authenticationToken);

所以我們可以配置多個Realm 給到 ModularRealmAuthenticator 這個bean,將ModularRealmAuthenticator 單獨配置為一個bean,將這個bean 配置給SecurityManager。

配置流程:

1.在原有程式碼的基礎上,新增第二個Realm SecondRealm.java

SecondRealm.java 中的 加密演算法 為 SHA1

package com.koncord.shiro;


import org.apache.shiro.SecurityUtils;
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.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import com.koncord.model.User;
import com.koncord.service.UserService;

/**
 * 自定義 Realm
 * @author Administrator
 *
 */
public class SecondRealm extends AuthorizingRealm{

	@Autowired
	private UserService userService;
	
	/**
	 * 執行授權邏輯
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		System.out.println("SecondRealm 執行授權邏輯");
		//給資源進行授權
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		//新增資源的授權字串
		//info.addStringPermission("user:add");
		
		//導資料庫查詢當前登入使用者的授權字串
		//獲取當前登入使用者
		Subject subject=SecurityUtils.getSubject();
		User user = (User) subject.getPrincipal(); 
		
		User sbUser=userService.findUserByName(user.getName());
		
		info.addStringPermission(sbUser.getPerms());
		return info;
	}

	/**
	 * 執行認證邏輯
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("SecondRealm 執行認證邏輯");
		
		//編寫shiro 判斷邏輯,判斷使用者名稱和密碼
		//1.判斷使用者名稱
		UsernamePasswordToken userToken=(UsernamePasswordToken) token;
		//根據使用者名稱獲取使用者資訊
		User user=userService.findUserByName(userToken.getUsername());
				
		if(user == null){
			//使用者不存在
			return null;//shiro底層會丟擲 UNknowAccountException
		}
		//2.判斷密碼     第一個引數:需要返回給 subject.login方法的資料   第二個引數:資料庫密碼       第三個引數:realm的名字
//		return new SimpleAuthenticationInfo(user, user.getPassword(), "");
		
		//根據使用者的情況,來構建AuthenticationInfo對像並返回,通常使用的實現類是SimpleAuthenticationInfo
		//以下資訊是從資料庫獲取的
		//1).principal:認證的實體類資訊。可以是username,也可以是資料表對應的使用者的體類物件
		Object principal=user;
		//2).credentials:密碼(資料庫獲取的使用者的密碼)
		Object credentials=user.getPassword();
		//3).realmName:當前realm物件的name,呼叫父類的getName()方法即可
		String realmName=getName();
		//4).鹽值
		ByteSource credentialsSalt = ByteSource.Util.bytes(user.getName());//鹽值 要唯一
		return new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
	}

	public static void main(String[] args) {
		String hashAlgorithmName="SHA1";//加密演算法(與配置檔案中的一致)
		String credentials="111111";//密碼
		ByteSource salt=ByteSource.Util.bytes("zhangsan");//鹽值
//		ByteSource salt=null;//鹽值
		int hashIterations=1024;//加密次數(與配置檔案中的一致)
		System.out.println(new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations));
	}
}

第一個自定義Realm的程式碼:

package com.koncord.shiro;


import org.apache.shiro.SecurityUtils;
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.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import com.koncord.model.User;
import com.koncord.service.UserService;

/**
 * 自定義 Realm
 * @author Administrator
 *
 */
public class UserRealm extends AuthorizingRealm{

	@Autowired
	private UserService userService;
	
	/**
	 * 執行授權邏輯
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		System.out.println("UserRealm 執行授權邏輯");
		//給資源進行授權
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		//新增資源的授權字串
		//info.addStringPermission("user:add");
		
		//導資料庫查詢當前登入使用者的授權字串
		//獲取當前登入使用者
		Subject subject=SecurityUtils.getSubject();
		User user = (User) subject.getPrincipal(); 
		
		User sbUser=userService.findUserByName(user.getName());
		
		info.addStringPermission(sbUser.getPerms());
		return info;
	}

	/**
	 * 執行認證邏輯
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("UserRealm 執行認證邏輯");
		
		//編寫shiro 判斷邏輯,判斷使用者名稱和密碼
		//1.判斷使用者名稱
		UsernamePasswordToken userToken=(UsernamePasswordToken) token;
		//根據使用者名稱獲取使用者資訊
		User user=userService.findUserByName(userToken.getUsername());
				
		if(user == null){
			//使用者不存在
			return null;//shiro底層會丟擲 UNknowAccountException
		}
		//2.判斷密碼     第一個引數:需要返回給 subject.login方法的資料   第二個引數:資料庫密碼       第三個引數:realm的名字
//		return new SimpleAuthenticationInfo(user, user.getPassword(), "");
		
		//根據使用者的情況,來構建AuthenticationInfo對像並返回,通常使用的實現類是SimpleAuthenticationInfo
		//以下資訊是從資料庫獲取的
		//1).principal:認證的實體類資訊。可以是username,也可以是資料表對應的使用者的體類物件
		Object principal=user;
		//2).credentials:密碼(資料庫獲取的使用者的密碼)
		Object credentials=user.getPassword();
		//3).realmName:當前realm物件的name,呼叫父類的getName()方法即可
		String realmName=getName();
		//4).鹽值
		ByteSource credentialsSalt = ByteSource.Util.bytes(user.getName());//鹽值 要唯一
		return new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
	}

	public static void main(String[] args) {
		String hashAlgorithmName="MD5";//加密演算法(與配置檔案中的一致)
		String credentials="111111";//密碼
		ByteSource salt=ByteSource.Util.bytes("admin");//鹽值
//		ByteSource salt=null;//鹽值
		int hashIterations=1024;//加密次數(與配置檔案中的一致)
		System.out.println(new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations));
	}
}

2.修改ShiroConfig 配置類

2.1 建立第2個Realm:

	/**
	 * 建立第二個Realm
	 */
	@Bean
	public SecondRealm secondRealm(){
		SecondRealm secondRealm = new SecondRealm();
		//設定 憑證匹配器
		secondRealm.setCredentialsMatcher(hashedCredentialsMatcherSHA());
		return secondRealm;
	}
	/**
	 * 配置 憑證匹配器 ,加密演算法為 SHA1
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcherSHA(){
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
		//設定加密演算法
		hashedCredentialsMatcher.setHashAlgorithmName("SHA1");
		//設定加密次數,比如兩次,相當於SHA1(SHA1())
		hashedCredentialsMatcher.setHashIterations(1024);
		return hashedCredentialsMatcher;
	}

2.2 修改securityManager方法,新增多realms:

	/**
	 * 2.建立 DefaultWebSecurityManager
	 */
	@Bean(name="securityManager")
	public DefaultWebSecurityManager securityManager(@Qualifier("userRealm")UserRealm userRealm){
		DefaultWebSecurityManager securityManager =new DefaultWebSecurityManager();
//		//關聯realm
//		securityManager.setRealm(userRealm);
		//新增多realms
		List<Realm> realms =new ArrayList<Realm>();
		realms.add(userRealm);
		realms.add(secondRealm());
		securityManager.setRealms(realms);
		return securityManager;
	}

ShiroConfig 配置類完整程式碼:

package com.koncord.shiro;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;

/**
 * Shiro配置類
 * @author Administrator
 *
 */
@Configuration
public class ShiroConfig {

	/**
	 * 3.建立ShiroFilterFactoryBean
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
		ShiroFilterFactoryBean shiroFilterFactoryBean =new ShiroFilterFactoryBean();
		
		//設定安全管理器
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		
		//新增Shiro內建過濾器
		/**
		 * Shiro內建過濾器,可以實現許可權相關的攔截
		 *    常用的過濾器:
		 *       anon:無需認證(登入)可以訪問
		 *       authc:必須認證才可以訪問
		 *       user:如果使用rememberMe的功能可以直接訪問
		 *       perms:該資源必須得到資源許可權才可以訪問
		 *       role:該資源必須得到角色許可權才可以訪問
		 */
		Map<String, String> filterMap = new LinkedHashMap<String, String>();
//		filterMap.put("/add", "authc");
//		filterMap.put("/update", "authc");
		filterMap.put("/testThymeleaf", "anon");
		filterMap.put("/login", "anon");
		
		//授權過濾器
		//注意:當前授權攔截後,shiro會自動跳轉到未授權頁面
		filterMap.put("/add", "perms[user:add]");
		filterMap.put("/update", "perms[user:update]");
		
		filterMap.put("/*", "authc");
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
		
		//設定登入跳轉連結
		shiroFilterFactoryBean.setLoginUrl("/toLogin");
		//設定未授權提示頁面
		shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");
		return shiroFilterFactoryBean;
	}
	
	/**
	 * 2.建立 DefaultWebSecurityManager
	 */
	@Bean(name="securityManager")
	public DefaultWebSecurityManager securityManager(@Qualifier("userRealm")UserRealm userRealm){
		DefaultWebSecurityManager securityManager =new DefaultWebSecurityManager();
//		//關聯realm
//		securityManager.setRealm(userRealm);
		//新增多realms
		List<Realm> realms =new ArrayList<Realm>();
		realms.add(userRealm);
		realms.add(secondRealm());
		securityManager.setRealms(realms);
		return securityManager;
	}
	
	/**
	 * 1.建立Realm
	 */
	@Bean
	public UserRealm userRealm(){
		UserRealm userRealm = new UserRealm();
		//設定 憑證匹配器
		userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
		return userRealm;
	}
	
	/**
	 * 建立第二個Realm
	 */
	@Bean
	public SecondRealm secondRealm(){
		SecondRealm secondRealm = new SecondRealm();
		//設定 憑證匹配器
		secondRealm.setCredentialsMatcher(hashedCredentialsMatcherSHA());
		return secondRealm;
	}
	
	/**
	 * 配置ShiroDialect,用於thymeleaf和shiro標籤配合使用
	 */
	@Bean
	public ShiroDialect getShiroDialect(){
		return new ShiroDialect();
	}
	
	/**
	 * 配置 憑證匹配器 (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了,
	 * 所以我們需要修改下doGetAuthenticationInfo中的程式碼;)
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcher(){
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
		//設定加密演算法
		hashedCredentialsMatcher.setHashAlgorithmName("MD5");
		//設定加密次數,比如兩次,相當於md5(md5())
		hashedCredentialsMatcher.setHashIterations(1024);
		return hashedCredentialsMatcher;
	}
	
	/**
	 * 配置 憑證匹配器 ,加密演算法為 SHA1
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcherSHA(){
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
		//設定加密演算法
		hashedCredentialsMatcher.setHashAlgorithmName("SHA1");
		//設定加密次數,比如兩次,相當於SHA1(SHA1())
		hashedCredentialsMatcher.setHashIterations(1024);
		return hashedCredentialsMatcher;
	}
}

這樣流程就配置完成了!

3.測試

打斷點,進行debug除錯,看下執行效果
(1)在UsernamePasswordToken.class 的
在這裡插入圖片描述

處打斷點,會看到斷點停兩次。
(2)在ModularRealmAuthenticator.class的
在這裡插入圖片描述
處,會看到 Realm的個數。
(3)由於兩種都使用的HashedCredentialsMatcher 時的兩種演算法:
在這裡插入圖片描述
在這裡插入圖片描述
測試成功!!!

三、認證策略

如果有多個Realm,怎樣才能認證成功,這就我們所謂的認證策略。

1.概念

(1)當一個應用程式配置了兩個或兩個以上的Realm 時,ModularRealmAuthenticator 依靠內部的AuthenticationStrategy 元件來判定認證的成功或失敗。

AuthenticationStrategy是一個無狀態的元件,它在身份驗證嘗試中被詢問4 次(這4 次互動所需的任何必要的狀態將被作為方法引數):

  1. 在任何Realm 被呼叫之前被詢問;
  2. 在一個單獨的Realm 的getAuthenticationInfo 方法被呼叫之前立即被詢問;
  3. 在一個單獨的Realm 的getAuthenticationInfo 方法被呼叫之後立即被詢問;
  4. 在所有的Realm 被呼叫後詢問。

(2)認證策略的另外一項工作就是聚合所有Realm的結果資訊封裝至一個AuthenticationInfo例項中,並將此資訊返回,以此作為Subject的身份資訊。

2.認證策略的使用

認證策略主要使用的是 AuthenticationStrategy 介面。
在這裡插入圖片描述
這個介面有三個實現類(認證策略的實現):

策略意義
AllSuccessfulStrategy所有Realm驗證成功才算成功,且返回所有Realm身份驗證成功的認證資訊,如果有一個失敗就失敗了
AtLeastOneSuccessfulStrategy(預設)只要有一個Realm驗證成功即可,和FirstSuccessfulStrategy 不同,將返回所有Realm身份驗證成功的認證資訊
FirstSuccessfulStrategy只要有一個 Realm 驗證成功即可只返回第一個 Realm 身份驗證成功的認證資訊,其他的忽略;

ModularRealmAuthenticator內建的認證策略預設實現是AtLeastOneSuccessfulStrategy 方式,因為這種方式也是被廣泛使用的一種認證策略。
在這裡插入圖片描述

2.1 預設使用

驗證下AtLeastOneSuccessfulStrategy:通過原始碼的方法驗證
①為了看效果,修改下 SecondRealm的認證訊息的返回值

SimpleAuthenticationInfo info= new SimpleAuthenticationInfo("SecondRealmName", credentials, credentialsSalt, realmName);

②debug方式啟動專案,輸入 使用者名稱和密碼,點選submit,進入如下斷點,可以看到 認證策略:
在這裡插入圖片描述
預設AtLeastOneSuccessfulStrategy策略下,有一個Realm認證成功就可以了。就算另一個Realm認證失敗也沒關係。

2.1 切換認證策略

(1)如何切換認證策略?
比如:切換成 AllSuccessfulStrategy 即所有認證策略都通過了,才算認證成功。

答:可以看出 認證策略是ModularRealmAuthenticator 類的一個屬性 authenticationStrategy,即在ShiroConfig配置類中新增配置:

	/**
	 * 系統自帶的Realm管理,主要針對多realm
	 */
	@Bean
	public ModularRealmAuthenticator modularRealmAuthenticator(){
		ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
		//設定認證策略
		modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
		return modularRealmAuthenticator;
	}

並在securityManager方法中設定這個modularRealmAuthenticator()方法:
在這裡插入圖片描述

即可。

(2)驗證AllSuccessfulStrategy:
打斷點,debug重啟專案,進行驗證:發現第二個Realm報異常
在這裡插入圖片描述

登入失敗
在這裡插入圖片描述
上一篇:SpringBoot 與Shiro 整合系列(二)實現MD5鹽值加密

延伸:
springboot整合shiro實現多realm不同資料表登陸
Spring Boot 整合Shiro的多realm配置

相關文章