认证策略实际上是AuthenticationStrategy这个接口,它有三个实现:

• AuthenticationStrategy 接口的默认实现:

• FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第 一个 Realm 身份验证成功的认证信息,其他的忽略;

• AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信 息;

• AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有 Realm身份验证成功的认证信息,如果有一个失败就失败了。

• ModularRealmAuthenticator 默认是 AtLeastOneSuccessfulStrategy 策略

在实际的业务场景中,会遇到安全数据存储在不同的数据库中的情况(例如两库用户登录同一系统),比如一个是Mysql数据库中的数据,一个是Oracle数据库中的数据,其中Mysql中使用的加密算法是MD5,而Oracle中使用的加密算法是SHA1或其它与Mysql不同的加密算法。此时我们进行用户登录认证的时候,就需要同时访问这两个数据库,也就需要多个Realm。

我们通过查看源码,来看一下Subject的login方法底层是如何使用Realm校验的。在login的深层,会调用ModularRealmAuthenticator的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);
}
}

可以看到,该方法中通过getRealms()获取Realm集合,如果realm只有一个,走的是doSingleRealmAuthentication方法,如果有多个,走的是doMultiRealmAuthentication方法。所以当我们使用ModularRealmAuthenticator类来配置多个Realm的时候,Shiro会使用我们配置的多个Realm进行认证。

package com.test.shiro.realms;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
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;
import com.test.shiro.po.User;

public class ShiroRealm extends AuthenticatingRealm{

private static Map<String,User> userMap = new HashMap<String,User>();
static{
//使用Map模拟数据库获取User表信息
userMap.put("jack", new User("jack","43e66616f8730a08e4bf1663301327b1",false));//密码明文:aaa123
userMap.put("tom", new User("tom","3abee8ced79e15b9b7ddd43b95f02f95",false));//密码明文:bbb321
userMap.put("jean", new User("jean","1a287acb0d87baded1e79f4b4c0d4f3e",true));//密码明文:ccc213
}


@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
System.out.println("[ShiroRealm]");
//1.把AuthenticationToken转换为UsernamePasswordToken
UsernamePasswordToken userToken = (UsernamePasswordToken) token;

//2.从UsernamePasswordToken中获取username
String username = userToken.getUsername();

//3.调用数据库的方法,从数据库中查询Username对应的用户记录
System.out.println("从数据看中获取UserName为"+username+"所对应的信息。");
//Map模拟数据库取数据
User u = userMap.get(username);

//4.若用户不行存在,可以抛出UnknownAccountException
if(u==null){
throw new UnknownAccountException("用户不存在");
}

//5.若用户被锁定,可以抛出LockedAccountException
if(u.isLocked()){
throw new LockedAccountException("用户被锁定");
}

//7.根据用户的情况,来构建AuthenticationInfo对象,通常使用的实现类为SimpleAuthenticationInfo
//以下信息是从数据库中获取的
//1)principal:认证的实体信息,可以是username,也可以是数据库表对应的用户的实体对象
Object principal = u.getUsername();
//2)credentials:密码
Object credentials = u.getPassword();
//3)realmName:当前realm对象的name,调用父类的getName()方法即可
String realmName = getName();
//4)credentialsSalt盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(principal);//使用账号作为盐值

SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal,credentials,realmName);
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
}

该ShiroRealm使用MD5加盐算法,为密码加密,并且采用MD5策略封装认证信息。


下面我们来进行多Realm认证的编写,首先创建一个名为“SecordRealm”的Realm类,复制之前ShiroRealm的代码,将加密方式改为“SHA1”,新增3个用户名不同的测试账户:

package com.test.shiro.realms;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
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;
import com.test.shiro.po.User;

public class SecordRealm extends AuthenticatingRealm{

private static Map<String,User> userMap = new HashMap<String,User>();
static{
//使用Map模拟数据库获取User表信息
userMap.put("jack2", new User("jack2","837b21a5a86ed8df842a4c2114a8b9f7d7c6d02d",false));//密码明文:aaa123
userMap.put("tom2", new User("tom2","ca578a1c0498fb93b7b0f06e30b2eecd155930db",false));//密码明文:bbb321
userMap.put("jean2", new User("jean2","d523305baa947918891aaa578d7b195d3122d8d0",true));//密码明文:ccc213
}


@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
System.out.println("[SecordRealm]");
//1.把AuthenticationToken转换为UsernamePasswordToken
UsernamePasswordToken userToken = (UsernamePasswordToken) token;

//2.从UsernamePasswordToken中获取username
String username = userToken.getUsername();

//3.调用数据库的方法,从数据库中查询Username对应的用户记录
System.out.println("从数据看中获取UserName为"+username+"所对应的信息。");
//Map模拟数据库取数据
User u = userMap.get(username);

//4.若用户不行存在,可以抛出UnknownAccountException
if(u==null){
throw new UnknownAccountException("用户不存在");
}

//5.若用户被锁定,可以抛出LockedAccountException
if(u.isLocked()){
throw new LockedAccountException("用户被锁定");
}

//7.根据用户的情况,来构建AuthenticationInfo对象,通常使用的实现类为SimpleAuthenticationInfo
//以下信息是从数据库中获取的
//1)principal:认证的实体信息,可以是username,也可以是数据库表对应的用户的实体对象
Object principal = u.getUsername();
//2)credentials:密码
Object credentials = u.getPassword();
//3)realmName:当前realm对象的name,调用父类的getName()方法即可
String realmName = getName();
//4)credentialsSalt盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(principal);//使用账号作为盐值

SimpleAuthenticationInfo info = null;
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
}

然后将该Realm配置到Spring的IOC容器中,即在Spring的applicationContext.xml配置文件中加入SecordRealm类的Bean配置:

<bean id="secordRealm" class="com.test.shiro.realms.SecordRealm">
<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>

(方式一:在ModularRealmAuthenticator中指定相应realms)​为了实现多Realm校验,需要把这两个Realm(之前的ShiroRealm和现在的secordRealm)配置到ModularRealmAuthenticator认证器中:

注意:执行的顺序和list的顺序有关!

<!-- 认证器 -->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="realms">
<list>
<ref bean="shiroRealm"/>
<ref bean="secordRealm"/>
</list>
</property>
</bean>

(方式二:在DefaultWebSecurityManager中指定相应的realms,这样就不用在​ModularRealmAuthenticator指定相应的realms​)

注意:执行的顺序和list的顺序有关

<!-- 
1. 配置 SecurityManager!
-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"></property>

<property name="realms">
<list>
<ref bean="jdbcRealm"/>
<ref bean="secondRealm"/>
</list>
</property>

<property name="rememberMeManager.cookie.maxAge" value="10"></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>

<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>

SecurityManager配置realms

在没有使用认证器之前,我们是这样配置Realm的:  

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="realm" ref="shiroRealm"/>
</bean>
<bean id="shiroRealm" class="com.test.shiro.realms.ShiroRealm"></bean>

后来使用了多Realm验证时,会将多个Realm配置到认证器中,再将认证器配置给securityManager:

<!--1. 配置 SecurityManager-->     
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
</bean>
<!-- 认证器 -->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="realms">
<list>
<ref bean="shiroRealm"/>
<ref bean="secordRealm"/>
</list>
</property>
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>

那么,我们如果将authenticator中的realms删除,而重新在securityManager添加两个Realm配置,

我们的工程还能跑起来吗?可以试验一下,首先将配置改为一下配置:

<!--1. 配置 SecurityManager-->     
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
<property name="realms">
<list>
<ref bean="shiroRealm"/>
<ref bean="secordRealm"/>
</list>
</property>
</bean>
<!-- 认证器 -->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
</property>
</bean>

重启Shiro3项目,然后使用jack登录:

Shiro实现多Realm认证、SecurityManager配置realms_数据库

发现依然可以进行正常的校验:

Shiro实现多Realm认证、SecurityManager配置realms_数据库_02

那么可以证明这种写法是没有问题的,但是我们要想一下,为什么这样可以呢?可以看到,我们是在securityManager中配置了authenticator的,所以在进行校验的时候是通过authenticator来获取认证信息并且校验的(ModularRealmAuthenticator源码):

Shiro实现多Realm认证、SecurityManager配置realms_apache_03

而使用的realms集合是authenticator类(ModularRealmAuthenticator)自己的realms属性,而我们在上面的配置中又没有给ModularRealmAuthenticator配置realms参数,而是给securityManager配置了realms属性,那么我们的程序是如何获取两个realm的认证信息的呢?

观察ModularRealmAuthenticator的源码我们可以看到,它除了包含名为realms的集合变量外,还有相应的get和set方法(当然这是IOC必须的),是不是有人在某个时候给它悄悄set了realms属性呢?

我们在AuthenticationSecurityManager中找到了答案:

Shiro实现多Realm认证、SecurityManager配置realms_数据库_04

发现在上面的方法中,只要校验出authenticator的类型属于ModularRealmAuthenticator,则会将SecurityManager的realms属性赋值给对应的authenticator类。所以确实是SecurityManager将自己的realms集合属性给了authenticator,所以authenticator才有realm可以用。

以后我们将使用这种写法来配置realm,因为后面我们讲解授权的时候,需要使用到SecurityManager的realms属性。