注:该系列所有测试均在之前创建的Shiro3的Web工程的基础上。
上一篇讲解了Shiro的密码MD5加盐校验,下面来讲解Shiro的多Realm验证。
在我们之前的样例中,一直使用的是ShiroRealm来获取数据源中的账号密码封装信息的,而该
Realm配置在securityManager中
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="realm" ref="shiroRealm"/>
</bean>
而在实际的业务场景中,会遇到安全数据存储在不同的数据库中的情况(例如两库用户登录同一系统),
比如一个是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
进行认证。
我们先回顾一下ShiroRealm类的代码:
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>
为了实现多Realm校验,需要把这两个Realm(之前的ShiroRealm和现在的secordRealm)配置到
ModularRealmAuthenticator认证器中:
<!-- 认证器 -->
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<property name="realms">
<list>
<ref bean="shiroRealm"/>
<ref bean="secordRealm"/>
</list>
</property>
</bean>
然后删除之前securityManager中配置的单个Realm,替换为上面的authenticator认证器:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager"/>
<property name="authenticator" ref="authenticator"/>
</bean>
我们分别在不同的Realm的doGetAuthenticationInfo方法前面加一个打印,以此在控制台中可以看到Shiro
使用了哪个Realm进行凭证获取:
System.out.println("[ShiroRealm]");
System.out.println("[SecordRealm]");
然后我们重新启动Shiro3工程,进行测试:
进入登录界面,我们分别输入jack和jack2的账号信息:
发现都会从两个Realm中获取认证信息:
获取Realm认证信息的顺序,就是ModularRealmAuthenticator认证器中realms集合的顺序。
这里我们其实还需要知道,两个Realm时,怎么才能知道认证通过了?是一个Realm通过了就通过,还是全部
Realm通过了才通过,还是其它?这就牵扯到了认证策略,下一篇我们继续讲解。