1.多Realm驗證
存在這樣一種場景,同一個密碼可能在MqSQL中存儲,也可能在Oracle中存儲,有可能MqSQL中使用的是MD5加密算法,而Oracle使用SHA1加密算法。這就需要有多個Realm以及認證策略的問題。
通過查看源碼可以看到 ModularRealmAuthenticator.class 中的 doAuthenticate
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
即可以看到如果有一個Realm 使用的是 doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
如果有多個Realm 使用的是doMultiRealmAuthentication(realms, authenticationToken);
所以我們可以配置多個Realm 給到 ModularRealmAuthenticator 這個bean,將ModularRealmAuthenticator 單獨配置為一個bean,將這個bean 配置給SecurityManager
1).添加第二個Realm SecondRealm.java
package com.java.shiro.realms; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.crypto.hash.SimpleHash; import org.apache.shiro.realm.AuthenticatingRealm; import org.apache.shiro.util.ByteSource; public class SecondRealm extends AuthenticatingRealm { @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { System.out.println("[SecondRealm] doGetAuthenticationInfo " + token); // 1. 把AuthenticationToken 轉換為UsernamePasswordToken UsernamePasswordToken up = (UsernamePasswordToken) token; // 2. 從UsernamePasswordToken 中來獲取username String username = up.getUsername(); // 3. 調用數據庫的方法,從數據庫中查詢username對應的用戶記錄 System.out.println("從數據庫中獲取userName :" + username + " 所對應的用戶信息."); // 4. 若用戶不存在,則可以拋出 UnknownAccoountException 異常 if ("unknown".equals(username)) { throw new UnknownAccountException("用戶不存在"); } // 5. 根據用戶信息的情況,決定是否需要拋出其他的AuthencationException 異常 假設用戶被鎖定 if ("monster".equals(username)) { throw new LockedAccountException("用戶被鎖定"); } // 6. 根據用戶的情況,來構建AuthenticationInfo 對象並返回,通常使用的是 // SimpleAuthenticationInfo // 以下信息是從數據庫獲取的. Object principal = username; // principal 認證的實體信息. // 可以是username,也可以是數據表對應的用戶的實體類對象 // String credentials = "fc1709d0a95a6be30bc5926fdb7f22f4"; // credentials:密碼 String credentials = null; // credentials:密碼 String realmName = getName(); AuthenticationInfo info = null;/*new SimpleAuthenticationInfo(principal, credentials, realmName);*/ if("admin".equals(username)){ credentials = "ce2f6417c7e1d32c1d81a797ee0b499f87c5de06"; }else if("user".equals(username)){ credentials = "073d4c3ae812935f23cb3f2a71943f49e082a718"; } ByteSource credentialsSalt = ByteSource.Util.bytes(username);//這里的參數要給個唯一的; info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName); return info; } public static void main(String[] args) { String hashAlgorithmName = "SHA1"; String credentials = "123456"; int hashIterations = 1024; ByteSource credentialsSalt = ByteSource.Util.bytes("admin"); Object obj = new SimpleHash(hashAlgorithmName, credentials, credentialsSalt, hashIterations); System.out.println(obj); } }
2). 修改 applicationContext.xml的配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 數據源配置,暫時不考慮數據源,做一些靜態的數據 --> <!-- Sample RDBMS data source that would exist in any application - not Shiro related. --> <!-- <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <property name="url" value="jdbc:hsqldb:mem:shiro-spring"/> <property name="username" value="sa"/> </bean> --> <!-- Populates the sample database with sample users and roles. --> <!-- <bean id="bootstrapDataPopulator" class="org.apache.shiro.samples.spring.BootstrapDataPopulator"> <property name="dataSource" ref="dataSource"/> </bean> --> <!-- Simulated business-tier "Manager", not Shiro related, just an example --> <!-- <bean id="sampleManager" class="org.apache.shiro.samples.spring.DefaultSampleManager"/> --> <!-- ========================================================= Shiro Core Components - Not Spring Specific ========================================================= --> <!-- Shiro's main business-tier object for web-enabled applications (use DefaultSecurityManager instead when there is no web environment)--> <!-- 1.配置SecurityManager! --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="cacheManager" ref="cacheManager"/> <!-- Single realm app. If you have multiple realms, use the 'realms' property instead. --> <!-- 配置session的管理方式 --> <!-- <property name="sessionMode" value="native"/> --> <!-- <property name="realm" ref="jdbcRealm"/> --> <!-- 配置多個Realm --> <property name="authenticator" ref="authenticator"></property> </bean> <!-- 配置多個Realm --> <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> <property name="realms"> <list> <ref bean="jdbcRealm"/> <ref bean="secondRealm"/> </list> </property> </bean> <!-- Let's use some enterprise caching support for better performance. You can replace this with any enterprise caching framework implementation that you like (Terracotta+Ehcache, Coherence, GigaSpaces, etc --> <!-- 2.配置CacheManager,實例上可以用企業的緩存產品來提升性能 2.1需要加入ehcache的jar包及配置文件 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <!-- Set a net.sf.ehcache.CacheManager instance here if you already have one. If not, a new one will be creaed with a default config: <property name="cacheManager" ref="ehCacheManager"/> --> <!-- If you don't have a pre-built net.sf.ehcache.CacheManager instance to inject, but you want a specific Ehcache configuration to be used, specify that here. If you don't, a default will be used.: --> <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> </bean> <!-- Used by the SecurityManager to access security data (users, roles, etc). Many other realm implementations can be used too (PropertiesRealm, LdapRealm, etc. --> <!-- 3.配置Realm 3.1 自己寫一個Realm,需要實現Realm接口 --> <bean id="jdbcRealm" class="com.java.shiro.realms.ShiroRealm"> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="MD5"></property> <!-- 加密算法的名稱 --> <property name="hashIterations" value="1024"></property> <!-- 配置加密的次數 --> </bean> </property> </bean> <bean id="secondRealm" class="com.java.shiro.realms.SecondRealm"> <property name="credentialsMatcher"> <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="SHA1"></property> <!-- 加密算法的名稱 --> <property name="hashIterations" value="1024"></property> <!-- 配置加密的次數 --> </bean> </property> </bean> <!-- ========================================================= Shiro Spring-specific integration ========================================================= --> <!-- Post processor that automatically invokes init() and destroy() methods for Spring-configured Shiro objects so you don't have to 1) specify an init-method and destroy-method attributes for every bean definition and 2) even know which Shiro objects require these methods to be called. --> <!-- 4.配置 LifecycleBeanPostProcessor,可以自動的調用配置在spring IOC容器中Shiro bean的聲明周期方法 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run: --> <!-- 5.啟用 IOC 容器中使用 shiro 注解,但必須在配置了LifecycleBeanPostProcessor 之后才可以使用。 --> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> <!-- Secure Spring remoting: Ensure any Spring Remoting method invocations can be associated with a Subject for security checks. --> <!-- 遠程調用,暫時不需要 --> <!-- <bean id="secureRemoteInvocationExecutor" class="org.apache.shiro.spring.remoting.SecureRemoteInvocationExecutor"> <property name="securityManager" ref="securityManager"/> </bean> --> <!-- Define the Shiro Filter here (as a FactoryBean) instead of directly in web.xml - web.xml uses the DelegatingFilterProxy to access this bean. This allows us to wire things with more control as well utilize nice Spring things such as PropertiesPlaceholderConfigurer and abstract beans or anything else we might need: --> <!-- 6.配置ShiroFilter 6.1 id 必須和web.xml 中配置的 DelegatingFilterProxy 的 <filter-name> 一致 若不一致,則會拋出:NoSuchBeanDefinitionException.因為Shiro會來IOC容器中查找和<filter-name> 名字對應的filter bean. --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/><!-- 登錄頁面 --> <property name="successUrl" value="/list.jsp"/><!-- 登錄成功頁面 --> <property name="unauthorizedUrl" value="/unauthorized.jsp"/><!-- 沒有權限的頁面 --> <!-- The 'filters' property is not necessary since any declared javax.servlet.Filter bean defined will be automatically acquired and available via its beanName in chain definitions, but you can perform overrides or parent/child consolidated configuration here if you like: --> <!-- <property name="filters"> <util:map> <entry key="aName" value-ref="someFilterPojo"/> </util:map> </property> --> <!-- 配置哪些頁面需要受保護 以及訪問這些頁面需要的權限 1). anon(anonymous) 可以被匿名訪問,即不需要登錄就可以訪問 2). authc(authentication) 必須認證之后,即登錄后才可以訪問 3). URL 權限采取第一次匹配優先的方式,即從開頭使用第一個匹配的url模式對應的攔截器鏈。 4). logout 登出 --> <property name="filterChainDefinitions"> <value> /login.jsp= anon /shiro/login= anon /shiro/logout = logout # everything else requires authentication: /** = authc </value> </property> </bean> </beans>
3).在UsernamePasswordToken.class 的
public char[] getPassword() { return password; }
處打斷點,會看到斷點停兩次。
由於兩種都使用的HashedCredentialsMatcher 時的兩種算法:
測試成功:
[FirstRealm] doGetAuthenticationInfo org.apache.shiro.authc.UsernamePasswordToken - admin, rememberMe=true
從數據庫中獲取userName :admin 所對應的用戶信息.
[SecondRealm] doGetAuthenticationInfo org.apache.shiro.authc.UsernamePasswordToken - admin, rememberMe=true
從數據庫中獲取userName :admin 所對應的用戶信息.
這兩個使用順序的,因為我們在配置文件中的配置,
<!-- 配置多個Realm -->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>
</bean>
2. Shiro 認證策略
1).如果有多個Realm,怎樣才算是認證成功,這就需要認證策略。
認證策略主要使用的是 AuthenticationStrategy 接口
這個接口由三個實現類:
-
- AuthenticationStrategy接口的默認實現:
- FirstSuccessfulStrategy:只要有一個Realm 驗證成功即可,只返回第一個Realm 身份驗證成功的認證信息,其他的忽略;
- AtLeastOneSuccessfulStrategy:只要有一個Realm驗證成功即可,和FirstSuccessfulStrategy不同,將返回所有Realm身份驗證成功的認證信息;
- AllSuccessfulStrategy:所有Realm驗證成功才算成功,且返回所有Realm身份驗證成功的認證信息,如果有一個失敗就失敗了。
- ModularRealmAuthenticator默認是AtLeastOneSuccessfulStrategy策略
通過debug 可以看出,默認使用的是AtLeastOneSuccessfulStrategy
為了便於觀看,修改SecondRealm中的
info = new SimpleAuthenticationInfo("seconde", credentials, credentialsSalt, realmName);
2). 如何切換認證策略
切換成 AllSuccessfulStrategy 即所有認證策略都通過了,才算認證成功。
可以看出 認證策略是ModularRealmAuthenticator 類的一個屬性 authenticationStrategy
即在applicationContext.xml中添加配置:
<!-- 配置多個Realm --> <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> <property name="realms"> <list> <ref bean="jdbcRealm"/> <ref bean="secondRealm"/> </list> </property> <property name="authenticationStrategy"> <bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean> </property> </bean>
修改其中一個Realm的密碼 為一個錯誤的密碼:
if("admin".equals(username)){ credentials = "ce2f6417c7e1d32c1d81a797ee0b499f87c5de06---"; }else if("user".equals(username)){ credentials = "073d4c3ae812935f23cb3f2a71943f49e082a718---"; }
則可以看到修改后的驗證策略為:
org.apache.shiro.authc.pam.AllSuccessfulStrategy@72f51247
[FirstRealm] doGetAuthenticationInfo org.apache.shiro.authc.UsernamePasswordToken - admin, rememberMe=true
從數據庫中獲取userName :admin 所對應的用戶信息.
[SecondRealm] doGetAuthenticationInfo org.apache.shiro.authc.UsernamePasswordToken - admin, rememberMe=true
從數據庫中獲取userName :admin 所對應的用戶信息.
登錄失敗:Unable to acquire account data from realm [com.java.shiro.realms.SecondRealm@349102eb]. The [org.apache.shiro.authc.pam.AllSuccessfulStrategy implementation requires all configured realm(s) to operate successfully for a successful authentication.