Apache Shiro 验证
验证(Authentication)
验证(Authentication):身份验证的过程--也就是证明一个用户的真实身份。为了证明用户身份,需要提供系统理解和相信的身份信息和证据。
需要通过向shiro提供用户的身份(Principals)和证明(credentials)来判定是否和系统所要求的匹配。
身份(Principals)是Subject的“身份属性”,可以是任何与Subject相关的标识,比如说名称(给定名称)、名字(姓或者昵称)、用户名、安全号码等等,当然像昵称这样的内容不能很好的对Subject进行独特标识,所以最好的身份信息(Principals)是使用在程序中唯一的标识--典型的使用用户名或邮件地址。
最主要的身份
虽然shiro可以使用任何数量的身份,Shiro还是希望一个程序精确地使用一个主要的身份--一个仅有的唯一标识Subject值。在多数程序中经常会是一个用户名、邮件地址或者全局唯一的用户ID。
证明(Credentials)通常是只有Subject知道的机密内容,用来证明他们真正拥有所需的身份,一些简单的证书例子如密码、指纹、眼底扫描和X.509证书等。
最常见的身份/证明是用户名和密码,用户名是所需的身份说明,密码是证明身份的证据。如果一个提交的密码和系统要求的一致,程序就认为该用户身份正确因为其他人不应该知道同样的密码。
Authenticating对象
Subject验证的过程可以有效地划分分以下三个步骤:
1.收集Subject提交的身份和证明;
2.向Authenticating提交身份和证明;
3.如果提交的内容正确,允许访问,否则重新尝试验证或阻止访问。
下面的代码示范了Shiro API如何实现这些步骤:
第一步:收集用户身份和证明
//Example using mostcommon scenario of username/password pair:
UsernamePasswordTokentoken = new UsernamePasswordToken(username, password);
//”Remember Me” built-in:
token.setRememberMe(true);
在这里我们使用UsernamePasswordToken,支持所有常用的用户名/密码验证途径,这是一个org.apache.shiro.authc.AuthenticationToken接口的实现,这个接口被shiro认证系统用来提交身份和证明。
注意shiro并不关心你如何获取这些住处:也许是用户从一个HTML表单中提交的,或者可能从一个HTTP请求字串中解析的,也可能来自于Swing或者Flex GUI的密码表单,或者通过命令行参数得到。从程序终端用户获取信息的过程与shiro的AuthenticationToken完全无关。
你可以随自己喜欢构造和引用AuthenticationToken--它是协议不可知论者。
这个例子同样显示我们希望shiro在尝试验证时执行“Remember Me”服务,这确保shiro在用户今后返回系统时能记住他们的身份,我们会在以后的章节讨论“Remember Me”服务。
第二步:提交身份和证明
当身份和证明住处被收集并实例化为一个认证令牌后,我们需要向shiro提交令牌以执行真正的验证尝试:
Subject currentUser =SecurityUtils.getSubject();
currentUser.login(token);
在获取当前执行的Subject后,我们执行一个单独的login命令,将之前创建的认证令牌实例传给它。
使用login方法将有效地执行身份验证。
第三步:处理成功或失败
当login函数没有返回信息时表明验证通过了。程序可以继续运行,此时执行SecurityUtils.getSubject()将返回验证后的Subject实例,subject.isAuthenticated()将返回true。
但是如果login失败了呢?例如,用户提供了一个错误的密码或者因访问系统次数过多而被锁定将会怎样呢?
shiro拥有丰富的运行期异常(AuthenticationException)可以精确标明为何验证失败,你可以将login放入到try/catch块中并捕获所有你想捕获的异常并对它们做出处理。例如:
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...
如果原有的异常不能满足你的需求,可以创建自定义的AuthenticationExceptions来表示特定的失败场景。
登录失败小贴士
虽然你的代码可以对指定的异常做出处理并执行某些所需的逻辑,但有经验的安全做法是仅向终端用户输出一般的失败信息,例如“错误的用户名和密码”。这确保不向尝试攻击你的黑客提供有用的信息。
已记住(Remembered) vs 已验证(Authenticated)
如上例所示,shiro支持在登录过程中执行"remember me",在此值得指出,一个已记住的Subject(remembered Subject)和一个正常通过认证的Subject(authenticated Subject)在shiro是完全不同的。
记住的(Remembered):一个被记住的Subject没有已知身份(也就是说subject.getPrincipals()返回空),但是它的身份被先前的认证过程记住,并存于先前session中,一个被认为记住的对象在执行subject.isRemembered()返回真。
已验证(Authenticated):一个被验证的Subject是成功验证后(如登录成功)并存于当前session中,一个被认为验证过的对象调用subject.isAuthenticated()将返回真。
互斥的
已记住(Remembered)和已验证(Authenticated)是互斥的--一个标识值为真另一个就为假,反过来也一样。
为什么区分?
单词验证(authentication)有明显的证明含义,也就是说,需要担保Subject已经被证明他们是谁。
当一个用户仅仅在上一次与程序交互时被记住,证明的状态已经不存在了:被记住的身份只是给系统一个信息这个用户可能是谁,但不确定,没有办法担保这个被记住的Subject是所要求的用户,一旦这个subject被验证通过,他们将不再被认为是记住的因为他们的身份已经被验证并存于当前的session中。
所以尽管程序大部分情况下仍可以针对记住的身份执行用户特定的逻辑,比如说自定义的视图,但不要执行敏感的操作直到用户成功执行身份验证使其身份得到确定。
例如,检查一个Subject是否可以访问金融信息应该取决于是否被验证(isAuthenticated())而不是被记住(isRemembered()),要确保该Subject是所需的和通过身份验证的。
一个例子说明
下面是一个非常常见的场景帮助说明被记住和被验证之间差别为何重要。
假设你使用卓越网,你已经成功登录并且在购物蓝中添加了一些书籍,但你由于临时要参加一个会议,匆忙中你忘记退出登录,当会议结束,回家的时间到了,于是你离开了办公室。
第二天当你回到工作,你意识到你没有完成你的购买动作,于是你回到卓越网,这时,卓越网“记得”你是认证,通过你的名字向你打招呼,仍旧给你提供个性化的图书推荐,对于卓越,subject.isRemembered()将返回真。
但是当你想访问你帐号的信用卡信息完成图书购买的时候会怎样呢?虽然卓越“记住”了你(isRemembered() == true),它不能担保你就是你(也许是正在使用你计算机的同事)。
于是,在你执行像使用信用卡信息之类的敏感操作之前,卓越强制你登录以使他们担保你的身份,在你登录之后,你的身份已经被验证,对于卓越,isAuthenticated()将返回真。
这类情景经常发生,所以shiro加入了该功能,你可以在你的程序中使用。现在是使用isRemembered()还是使用isAuthenticated()来定制你的视图和工作流完全取决于你自己,但shiro维护这种状态基础以防你可能会需要。
退出登录
与验证相对的是释放所有已知的身份信息,当Subject与程序不再交互了,你可以调用subject.logout()丢掉所有身份信息。
currentUser.logout();//removes all identifying information and invalidates their session too.
当你调用logout,任何现存的session将变为不可用并且所有的身份信息将消失(如:在web程序中,RememberMe的Cookie信息同样被删除)。
当一个Subject退出登录,Subject被重新认定为匿名的,对于web程序,如果需要可以重新登录。
Web程序需注意
因为在Web程序中记住身份信息往往使用Cookies,而Cookies只能在Response提交时才能被删除,所以强烈要求在为最终用户调用subject.logout()之后立即将用户引导到一个新页面,确保任何与安全相关的Cookies如期删除,这是Http本身Cookies功能的限制而不是Shiro的限制。
认证序列
直到现在,我们只看到如何在程序代码中验证一个Suject,现在我们看一下当一个验证发生时shiro内部发生了什么。
我们仍使用之前在架构(architecture)章节里见到过的架构图,仅将左侧跟认证相关的组件高亮,每一个数字代表认证中的一个步骤:
第1步:程序代码调用Subject.login方法,向AuthenticationToken(公证处)实例的构造方法传递最终用户的身份和证明。
第2步:Subject实例,通常是一个 DelegatingSubject(或其子类)通过调用securityManager.login(token)将这个令牌转交给程序的SecurityManager。
第3步:SecurityManager,基本的“安全伞”组件,得到令牌并通过调用authenticator.authenticate(token)简单地将其转交它内部的Authenticator实例,大部分情况下是一个ModularRealmAuthenticator实例,用来支持在认证过程中协调一个或多个Realm实例。ModularRealmAuthenticator本质上为Apache Shiro(在PAM术语中每一个Realm称为一个“模块”)提供一个PAM类型的范例。
第4步:如程序配置了多个Realm,ModularRealmAuthenticator实例将使用其配置的 AuthenticationStrategy开始一个多Realm身份验证的尝试。在此之前,之中及之后Realm被验证调用,AuthenticationStrategy被调用以允许其同每一个Realm结果交互,我们将稍后讨论AuthenticationStrategies。
注意:单Realm程序
如果仅有一个Realm被配置,它直接被调用--在单Realm程序中不需要AuthenticationStrategy。
第5步:每一个配置的Realm都被检验看其是否支持提交的AuthenticationToken,如果支持,则该Realm的 getAuthenticationInfo方法将使用提交的牌作调用,getAuthenticationInfo方法为特定的Realm有效提供一次独立的验证尝试,我们将简短地讨论Realm验证行为。
Authenticator
就像以前提到过的,Shiro SecurityManager implementations默认使用一个 ModularRealmAuthenticator实例, ModularRealmAuthenticator同样支持单Realm和多Realm。
在一个单Realm程序中,ModularRealmAuthenticator将直接执行单独的Realm,如果配置有两个或以上Realm,将会使用AuthenticationStrategy实例来协调如何进行验证,我们将在下面讨论AuthenticationStrategy。
如果你希望用自定义的Authenticator实现配置SecurityManager,你可以在shiro.ini中做这件事,如:
[main]
...
authenticator =com.foo.bar.CustomAuthenticator
securityManager.authenticator= $authenticator
尽管在实际操作中, ModularRealmAuthenticator适用于大部分需求。
AuthenticationStrategy
当一个程序中定义了两个或多个realm时,ModularRealmAuthenticator信任一个内部的AuthenticationStrategy组件来决定一个验证是否成功的条件。
例如,如果一个Realm验证一个AuthenticationToken成功,但其他的都失败了,那这次尝试是否被认为是成功的呢?不是所有Realm验证都成功了才认为是成功?又或者一个Realm验证成功,是否还有必要讨论其他Realm?AuthenticationStrategy根据程序需求做出恰当的决定。
AuthenticationStrategy是一个stateless的组件,在整个验证过程中在以下四个时候会被用到(当在这四个活动中需要必要的state时,state将作为方法参数传递)
1.在任何Realms被执行之前;
2.紧接在单独的Realm的getAuthenticationInfo方法调用之前;
3.紧接着单独的Realm的getAuthenticationInfo方法调用之后;
4.在所有的Realm被执行之后。
AuthenticationStrategy还有责任从每一个成功的Realm中收集结果并将它们“绑定”到一个单独的AuthenticationInfo,这个AuthenticationInfo实例是被Authenticator实例返回的,并且shiro用它来展现一个Subject的最终身份(也就是Principals)。
Subject身份“展示(view)”
如果你在程序中使用多于一个的Realm从多个数据源中获取帐户数据,程序可看到的Subject身份最终“融合(merged)”的展示是AuthenticationStrategy最终的责任。
shiro有3个具体的AuthenticationStrategy实现:
AtLeastOneSuccessfulStrategy:如果一个或多个Realm验证成功,所有的尝试都被认为是成功的,如果没有一个验证成功,则该次尝试失败;
FirstSuccessfulStrategy:只有从第一个成功验证的Realm返回的信息会被使用,以后的Realm将被忽略,如果没有一个验证成功,则该次尝试失败。
SuccessfulStrategy:所有配置的Realm在全部尝试中都成功验证才被认为是成功,如果有一个验证不成功,则该次尝试失败。
ModularRealmAuthenticator默认使用AtLeastOneSuccessfulStrategy实现,这也是最常用的策略,然而你也可以配置你希望的不同的策略。
shiro.ini
[main]
...
authcStrategy =org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy= $authcStrategy
...
自定义的AuthenticationStrategy
如果你希望创建你自己的AuthenticationStrategy
实现,你可以使用org.apache.shiro.authc.pam.AbstractAuthenticationStrategy作为起始点。AbstractAuthenticationStrategy类自动实现 '绑定(bundling)'/聚集(aggregation)行为将自于每个Realm的结果收集到一个AuthenticationInfo实例中。
Realm验证的顺序
非常重要的一点是,将和Realm交互的ModularRealmAuthenticator按iteration顺序执行。
ModularRealmAuthenticator可以访问为SecurityManager配置的Realm实例,当尝试一次验证时,它将在集合中遍历,支持对提交的AuthenticationToken处理的每个Realm都将执行Realm的getAuthenticationInfo方法。
隐含的顺序
在使用ShiroINI配置文件形式时,你可以按你希望其处理AuthenticationToken的顺序来配置Realm,例如,在shiro.ni中,Realm将按照他们在INI文件中定义的顺序执行。
blahRealm =com.company.blah.Realm
...
fooRealm =com.company.foo.Realm
...
barRealm =com.company.another.Realm
SecurityManager上配置了这三个Realm,在一个验证过程中,blahRealm, fooRealm, 和 barRealm将被顺序执行。
这基本上与定义下面这一行语句的效果相同:
securityManager.realms= $blahRealm, $fooRealm, $barRealm
使用这种方法,你不需要设置securityManager的realm忏悔,每一个被定义的realm将自动加入到realms属性中。
明确的顺序
如果你希望明确定义realm执行的顺序,不管他们如何被定义,你可以设置SecurityManager的realms属性,例如,使用上面定义的realm,但你希望blahRealm最后执行而不是第一个:
blahRealm =com.company.blah.Realm
...
fooRealm =com.company.foo.Realm
...
barRealm =com.company.another.Realm
securityManager.realms= $fooRealm, $barRealm, $blahRealm
...
明确Realm包含
当你明确的配置securityManager.realms属性时,只有被引用的realm将为SecurityManager配置,也就是说你可能在INI中定义了5个realm,但实际上只使用了3个,如果在realm属性中只引用了3个,这和隐含的realm顺序不同,在那种情况下,所有有效的realm都会用到。
Realm验证
本章阐述了当一个验证尝试发生时shiro主要的工作流程,而在验证过程中,用到的realm内产生的工作流程(如上面提到的第5步)将在Realm章中Realm Authentication节讨论。
为文档加把手
我们希望这篇文档可以帮助你使用Apache Shiro进行工作,社区一直在不断地完善和扩展文档,如果你希望帮助shiro项目,请在你认为需要的地方考虑更正、扩展或添加文档,你提供的任何点滴帮助都将扩充社区并且提升Shiro。
提供你的文档的最简单的途径是将它发送到用户论坛(http://shiro-user.582556.n2.nabble.com/)或邮件列表(http://shiro.apache.org/mailing-lists.html)