Authentication(身份认证)是Shiro权限控制的第一步,用来告诉系统你就是你。
在提交认证的时候,我们需要给系统提交两个信息:
Principals:是一个表示用户的唯一属性,可以是用户名,邮箱之类的。
Credentials:是证明用户身份的证书,可以是密码或者指纹之类的。
认证主要分为三步:
1、收集认证信息
2、提交认证信息
3、如果认证成功,则允许访问,否则就拒绝访问或者重试。
收集认证信息:
在入门的例子中,使用了一个UsernamePasswordToken来收集用户的用户名和密码,用来登陆。
这个类支持最简单的用户名和密码登陆。实现了org.apache.shiro.authc.AuthenticationToken接口。
AuthenticationToken接口是认证系统的基础,只有getCredentials(),getPrincipal()两个方法用来获取基本的认证信息。
HostAuthenticationToken和RememberMeAuthenticationToken是它的两个子接口。
HostAuthenticationToken只有一个getHost()方法用来获取请求的地址信息。
RememberMeAuthenticationToken只有一个isRememberMe()方法用来标记用户是否需要记住我。
然后还有两个子类UsernamePasswordToken和CasToken提供了基本的实现。其中CasToken已经被废弃了。
例:
//Example using most common scenario of username/password pair:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//"Remember Me" built-in:
token.setRememberMe(true);
提交认证信息:
收集好认证信息之后,保存为AuthenticationToken的一个实例,我们需要提交这个认证信息来进行认证。
进行认证前,我们首先需要获取当前用户(Subject)。然后调用login方法来进行登陆。
例:
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
处理结果:
如果登陆成功的话,则不会有什么异常,此时如果调用isAuthenticated()方法,则会返回true。
如果登陆失败的话,则被抛出异常,在SHiro中,提供了很多异常类,可以用来捕捉具体的异常。
例子:
try {
currentUser.login(token);
} catch ( UnknownAccountException uae ) { ...
} catch ( IncorrectCredentialsException ice ) { ...
} catch ( LockedAccountException lae ) { ...
} catch ( ExcessiveAttemptsException eae ) { ...
} ... catch your own ...
} catch ( AuthenticationException ae ) {
//unexpected error?
} //No problems, continue on as expected...
可以看到,前面出现了Remembered和Authenticated。那这两种有什么区别呢?
需要注意的是,这是两种互斥的情况,当我们在登陆的时候,登陆成功之后,我们Authenticated返回的是true。
选择Remeber me的时候,我们下次可以不登陆直接访问,而我们下次登陆之后,Remembered返回的是true,但是Authenticated返回的是false。
在登陆之后,我们可以可以用logout()方法来注销。这时候用户的信息都会被清空,包括保存在Cookie中的RemeberMe信息还有session也会被无效。
上面我们就简单的讲述了一下在代码中实现登陆验证的流程。具体实例可以参考入门的例子。
那么,在Shrio内部是怎么样的一个认证流程呢?大概可以用下图来概括:
第一步:在代码中调用login方法,传递构造好的AuthenticationToken实例。
第二步:通过一个DelegatingSubject来分发认证请求给SecurityManager
第三步:SecurityManager容器会把接收到的token简单的转发给它内部的认证器实例通过调用认证器的authenticate(token)方法。
这通常是一个ModularRealmAuthenticator实例,用来支持多个Realm。
第四步:如果有定义多个Realm则ModularRealmAuthenticator会初始化一个支持多个Realm的认证器,通过配置的AuthenticationStrategy。
第五步:每一个配置的Realm都会被检测是否支持提交的AuthenticationToken,如果支持的话就会调用getAuthenticationInfo方法,从Realm中获取数据来跟提交的token进行验证。
验证器
在SecurityManager中,默认使用ModularRealmAuthenticator实例,它不仅仅支持单Realm还支持多个Realm。
如果实在单个Realm的情况下,ModualrRealmAuthenticator会直接调用这个Realm来尝试验证。
如果我们需要定义自己的验证器的话,可以通过在配置文件的[main]中如下定义:
[main]
...
authenticator = com.foo.bar.CustomAuthenticator
securityManager.authenticator = $authenticator
验证策略
如果是只有一个Realms的时候,则不需要验证策略。
如果是有两个以上的Realm的时候,ModularRealmAuthenticator依赖于AuthenticationStrategy组件来决定认证成功或者失败的条件,
比如是一个成功就成功还是要都成功才是成功之类的。。。。
在Shiro里面已经有三个认证策略的实现
AtLeastOneSuccessfulStrategy:只要有一个或一个以上的认证成功就表示认证成功
FirstSuccessfulStrategy;只有第一个认证成功的时候才表示认证成功
AllSuccessfulStrategy:只有所有的都认证成功的时候才表示认证成功。
在ModularRealmAuthenticator默认使用AtLeastOneSuccessfulStrategy的验证策略。当然也可以通过下面的方式定义其他的验证策略。
[main]
...
authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $authcStrategy
...
Realm认证顺序
在定义了多个Realm的时候,如果有多个Realm支持当前的AuthenticationToken,则会依次调用Realm的getAuthenticationInfo方法。
调用的顺序分为两种:
隐式的:
如果在SecurityManager中定义了如下的几个Realm,则会按照他们定义的顺序去调用。
blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm
此时的效果就等于下面的语句。
securityManager.realms = $blahRealm, $fooRealm, $barRealm、
显示的:
就如上面的一样,如果在securityManager.realms中配置的时候,改变realm的配置顺序,则会按照这个配置顺序来调用,这就是显示的配置。
blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm
securityManager.realms = $fooRealm, $barRealm, $blahRealm
...
域认证
前面说了下Realm的认证策略和认证顺序,那么,在Realm认证的时候,究竟发生什么事情了呢?
supporting AuthenticationTokens
在Realm被用来尝试认证登陆的时候首先会调用supports方法,来检测是否能解析这个AuthenticationToken,决定是否进行认证。
Handing supported AuthenticationTokens
如果这个realm支持提交的AuthenticationTokens的话,解析器会调用Realm的getAuthenticationINfo(token)方法,来尝试认证,
这个方法大概做了下面这些事情:
1、检测token中的唯一用户标识
2、基于唯一标识 去数据源中查找
3、确保提供的证书跟数据源中保存的一样
4、如果证书一样,就封装一个AuthenticationINfo实例返回
5、如果证书不匹配,则抛出一个AuthenticationException异常
下面,我们来看看Shrio提供的Realm的类结构:
org.apache.shiro.realm.Realm(I):base
org.apache.shiro.realm.CachingRealm(Abstract):提供缓存支持
org.apache.shiro.realm.AuthenticatingRealm(Abstract):提供认证支持
org.apache.shiro.realm.AuthorizingRealm(Abstract):提供授权支持
org.apache.shiro.realm.SimpleAccountRealm(C):简单的用户名密码支持
org.apache.shiro.realm.text.TextConfigurationRealm(C):支持Text文件的简单用户名密码支持
org.apache.shiro.realm.text.IniRealm(C):通过INI文件的简单用户名密码支持(默认使用这个)
org.apache.shiro.realm.text.PropertiesRealm(C):支持属性文件的简单用户名密码支持
org.apache.shiro.realm.jdbc.JdbcRealm(C):支持通过JDBC认证
在系统默认的情况下,系统使用的是IniRealm这个实现,可以从INI配置文件的[users]和[roles]两个节中读取用户信息和权限信息。
如果我们需要定义自己的Realm实现的话,一般都是继承AuthorizingRealm。
稍候,我们将简单介绍下如果从db中来实现认证。
证书匹配:
前面我们说过,Realm需要去匹配用户提交的证书跟数据源中存储的证书是否匹配,如果匹配的话就认为是认证成功。
在获得用户唯一标识后,系统回去Realm会去检索用户的证书,然后通过CredentialsMatcher来检测证书是否匹配。
Shrio中也提供了一些证书的匹配器可以直接拿来使用,使用下面的方法变更默认的匹配器:
[main]
...
customMatcher = com.company.shiro.realm.CustomCredentialsMatcher
myRealm = com.company.shiro.realm.MyRealm
myRealm.credentialsMatcher = $customMatcher
...
Simple Equality Check
默认情况下所有提供的Realm实现都是使用SimpleCredentialsMatcher来检测证书是否匹配。
不过一般情况下都不需要变更,因为默认的就足够了。
Hashing Credentials
相比使用原始的数据存储起来,拿来匹配,我们更愿意将证书加密进行存储来进行匹配。那么如何使用呢?
在Shiro中提供了几个HashedCredentialsMatcher的子类,用来实现这个功能。包括MD5、SHA-256等等的加密方式。
我们可以通过下面的配置方式:
[main]
...
credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
# base64 encoding, not hex in this example:
credentialsMatcher.storedCredentialsHexEncoded = false
credentialsMatcher.hashIterations = 1024
# This next property is only needed in Shiro 1.0\. Remove it in 1.1 and later:
credentialsMatcher.hashSalted = true ...
myRealm = com.company.....
myRealm.credentialsMatcher = $credentialsMatcher
...
需要注意的是,这种情况下在Realm的实现中需要返回一个SaltedAuthenticationInfo,而不是普通的AuthenticationInfo,因为在用户提交认证的时候,需要获取相同的salt来进行加密,进行匹配认证。
那这个salt(盐)是用来干嘛的呢?
这是因为在进行MD5之类加密的时候,还是可以进行破解的,但是如果加入一个变量来进行加密之后,就基本上是无法破解了(不知道这个SALT的情况下)。
下面我们就做一个通过JDBC来认证的登陆认证程序。点此看源码
首先我们需要一个用户表,脚本如下:
CREATE DATABASE `db_shiro` USE `db_shiro`; DROP TABLE IF EXISTS `users`; CREATE TABLE `users` (
`id` int(4) NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `users`(`id`,`username`,`password`) values (1,'fuwh','');
我们需要定义一个shiro_jdbc.ini文件如下
[main]
dataSource=com.mchange.v2.c3p0.ComboPooledDataSource
dataSource.driverClass=com.mysql.jdbc.Driver
dataSource.jdbcUrl=jdbc:mysql://localhost:3306/db_shiro
dataSource.user=root
dataSource.password=rootadmin ;this is comment read from mysql
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource=$dataSource
securityManager.realms=$jdbcRealm
编写登陆认证代码:
package com.fuwh.demo; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; public class ShiroDemo02 { private static Logger log=LoggerFactory.getLogger(ShiroDemo02.class);
public static void main(String[] args) {
//取得SecurityManager工厂
Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:shiro_jdbc.ini");
//取得SecurityManager实例
SecurityManager securityManager=factory.getInstance();
//将securityManager绑定到SecurityUtil
SecurityUtils.setSecurityManager(securityManager); /* 至此为止,简单的从mysql数据库读取realm信息的shiro环境就配置好了 */ //取得当前用户
Subject currentUser=SecurityUtils.getSubject(); //使用shiro来进行登陆验证
if(!currentUser.isAuthenticated()) {
UsernamePasswordToken token=new UsernamePasswordToken("fuwh","123456");
try {
currentUser.login(token);
log.info("登陆成功!!!");
} catch (Exception e) {
e.printStackTrace();
log.error("认证失败...");
}
} currentUser.logout();
}
}
执行结果:
2017-08-26 15:16:03,357 [main] INFO [com.mchange.v2.log.MLog] - MLog clients using log4j logging.
2017-08-26 15:16:04,107 [main] INFO [com.mchange.v2.c3p0.C3P0Registry] - Initializing c3p0-0.9.1.2 [built 21-May-2007 15:04:56; debug? true; trace: 10]
2017-08-26 15:16:04,436 [main] INFO [org.apache.shiro.config.IniSecurityManagerFactory] - Realms have been explicitly set on the SecurityManager instance - auto-setting of realms will not occur.
2017-08-26 15:16:04,560 [main] INFO [com.mchange.v2.c3p0.impl.AbstractPoolBackedDataSource] - Initializing c3p0 pool... com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> 1hgetj59q83i1dm1es8ork|67f89fa3, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> 1hgetj59q83i1dm1es8ork|67f89fa3, idleConnectionTestPeriod -> 0, initialPoolSize -> 3, jdbcUrl -> jdbc:mysql://localhost:3306/db_shiro, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 0, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 15, maxStatements -> 0, maxStatementsPerConnection -> 0, minPoolSize -> 3, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ]
2017-08-26 15:16:05,040 [main] INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler...
2017-08-26 15:16:05,056 [main] INFO [com.fuwh.demo.ShiroDemo02] - 登陆成功!!!
此时,已经可以从表中去验证登陆了。那处理流程又是什么样的呢?
首先我们使用shiro_jdbc.ini来初始化了SecurityManager,在配置文件中,我们定义了连接池信息,还有jdbcRealm,同时在SecurityManager中指定了realm为定义的JjdbcRealm,这时候,其实shiro使用的SecurityManager是一个RealmSecurityManager的实例。而当我们登陆的时候,则会通过配置的jdbcRealm来从数据库中取得用户信息来进行认证。那是怎么取得呢?我们明明没有写sql什么的。
其实,看org.apache.shiro.realm.jdbc.JdbcRealm的源码可以看到,在这个类里面定义了很多的静态sql变量,点此查看,点此查看sql内容
其中比较重要的是AuthenticationQuery这个字段,默认情况下它的值是等于Default_Authentication_query。
Default_Authentication_Query="select password from users where username=?";
所以在默认情况下,它会从users这个表中通过username这个key来查找password。从而拿来跟我们提交的密码来进行匹配验证。
如果我们不想使用默认的数据库,默认的表名,默认的列名的话,也可以通过在配置文件中重写AuthenticationQuery的值来个性化sql文。
首先修改新建一个表members:
USE `db_shiro`; DROP TABLE IF EXISTS `members`; CREATE TABLE `members` (
`id` INT(4) NOT NULL AUTO_INCREMENT,
`userName` VARCHAR(20) DEFAULT NULL,
`pass` VARCHAR(100) DEFAULT NULL,
UNIQUE KEY `id` (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO `members`(`id`,`userName`,`pass`) VALUES (1,'fuwh','');
然后修改shiro_jdbc_sql.ini配置文件:
[main]
dataSource=com.mchange.v2.c3p0.ComboPooledDataSource
dataSource.driverClass=com.mysql.jdbc.Driver
dataSource.jdbcUrl=jdbc:mysql://localhost:3306/db_shiro
dataSource.user=root
dataSource.password=rootadmin ;this is comment read from mysql
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource=$dataSource
jdbcRealm.authenticationQuery=select pass from members where userName=?
securityManager.realms=$jdbcRealm
修改认证程序的配置文件:
package com.fuwh.demo; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; public class ShiroDemoSql02 { private static Logger log=LoggerFactory.getLogger(ShiroDemoSql02.class);
public static void main(String[] args) {
//取得SecurityManager工厂
Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:shiro_jdbc_sql.ini");
//取得SecurityManager实例
SecurityManager securityManager=factory.getInstance();
//将securityManager绑定到SecurityUtil
SecurityUtils.setSecurityManager(securityManager); /* 至此为止,简单的从mysql数据库读取realm信息的shiro环境就配置好了 */ //取得当前用户
Subject currentUser=SecurityUtils.getSubject(); //使用shiro来进行登陆验证
if(!currentUser.isAuthenticated()) {
UsernamePasswordToken token=new UsernamePasswordToken("fuwh","123");
try {
currentUser.login(token);
log.info("登陆成功!!!");
} catch (Exception e) {
e.printStackTrace();
log.error("认证失败...");
}
} currentUser.logout();
}
}
后面我们会讲到角色认证也是同样的道理。
自定义Realm
上面我们使用的是Shiro提供的默认的Realm,下面我们自定义一个从数据库中读取信息的Realm,通过继承AuthorizingRealm。
package com.fuwh.realm; import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException; 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; import com.fuwh.util.DbUtil; public class MyJdbcRealm extends AuthorizingRealm{ @Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
} @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// TODO Auto-generated method stub
Connection conn=DbUtil.getConnection();
String sql="select * from members2 where username=?";
try {
PreparedStatement ps=conn.prepareStatement(sql);
ps.setString(1, token.getPrincipal().toString());
ResultSet rs=ps.executeQuery();
while(rs.next()) {
AuthenticationInfo info=new SimpleAuthenticationInfo(rs.getString("username"),rs.getString("password"),"salt");
return info;
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} return null;
} }
修改配置文件
[main]
myJdbcRealm=com.fuwh.realm.MyJdbcRealm
securityManager.realms=$myJdbcRealm
编写登陆类:
package com.fuwh.demo; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; public class ShiroDemoMySql02 { private static Logger log=LoggerFactory.getLogger(ShiroDemoMySql02.class);
public static void main(String[] args) {
//取得SecurityManager工厂
Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:shiro_jdbc_my_sql.ini");
//取得SecurityManager实例
SecurityManager securityManager=factory.getInstance();
//将securityManager绑定到SecurityUtil
SecurityUtils.setSecurityManager(securityManager); /* 至此为止,简单的从mysql数据库读取realm信息的shiro环境就配置好了 */ //取得当前用户
Subject currentUser=SecurityUtils.getSubject(); //使用shiro来进行登陆验证
if(!currentUser.isAuthenticated()) {
UsernamePasswordToken token=new UsernamePasswordToken("fuwh","123");
try {
currentUser.login(token);
log.info("登陆成功!!!");
} catch (Exception e) {
e.printStackTrace();
log.error("认证失败...");
}
} currentUser.logout();
}
}
那如果我们定义了多个Realm呢?
在编写一个Realm类:
package com.fuwh.realm; 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; public class MyJdbcRealm2 extends AuthorizingRealm{
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
} @Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// TODO Auto-generated method stub
return new SimpleAuthenticationInfo(token.getPrincipal(),"1234","salt");
}
}
修改shiro_jdbc_my_sql_2.ini配置文件:
[main]
myJdbcRealm=com.fuwh.realm.MyJdbcRealm
myJdbcRealm2=com.fuwh.realm.MyJdbcRealm2
authStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy
securityManager.realms=$myJdbcRealm,$myJdbcRealm2
securityManager.authenticator.authenticationStrategy=$authStrategy
修改登陆类:
package com.fuwh.demo; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; public class ShiroDemoMySql02_2 { private static Logger log=LoggerFactory.getLogger(ShiroDemoMySql02_2.class);
public static void main(String[] args) {
//取得SecurityManager工厂
Factory<SecurityManager> factory=new IniSecurityManagerFactory("classpath:shiro_jdbc_my_sql_2.ini");
//取得SecurityManager实例
SecurityManager securityManager=factory.getInstance();
//将securityManager绑定到SecurityUtil
SecurityUtils.setSecurityManager(securityManager); /* 至此为止,简单的从mysql数据库读取realm信息的shiro环境就配置好了 */ //取得当前用户
Subject currentUser=SecurityUtils.getSubject(); //使用shiro来进行登陆验证
if(!currentUser.isAuthenticated()) {
UsernamePasswordToken token=new UsernamePasswordToken("fuwh","123");
try {
currentUser.login(token);
log.info("登陆成功!!!");
} catch (Exception e) {
e.printStackTrace();
log.error("认证失败...");
}
} currentUser.logout();
}
}
上面的例子中使用的认证策略是只有一个成功就认为是成功。
在Shiro中还提供了下面几个策略:
FirstSuccessfulStrategy:只有第一个认证成功的信息会被用
AllSuccessfulStrategy:只有当所有的都成功的时候才认为是认证成功。
源码地址:https://github.com/oukafu/shiro