认证就是身份验证的过程,具体来说就是证明一个用户的真实身份和用户所描述的身份一致的过程。当验证一个用户身份的时候,需要用户提供一些身份信息就比如系统可以理解的结构化的数据。
这个过程通过提交一个用户的principal和credential给shiro判断是否匹配应用内部期望的信息来完成的。
- principal是一个用户的身份属性,principal可以有多个,可以是任何可证明用户身份的东西,比如系统名(由系统给定的名称),昵称(用户自定义名),用户手机号,社保号码,等等.当然有些内容并不适合作为principal,比如用户实际的姓名,因为现实中重名的概率极大,最好的principal必须是唯一的,比如用户的身份证号或者邮箱地址.
主principal
Shiro中可以存在任意数量的principal,但推荐一个应用程序只接受一个主要的principal,一个在应用中唯一存在的值对应一个principal.这也是在大多数应用中经常使用的用户名,邮箱地址或者全局唯一用户id等单一principal认证方式.
- credential通常是加密后的数据,只和principal相关的内容,总是被用来校验身份是否属实.通常的实例是密码,生物数据数据比如指纹信息,视网膜扫描,和X.509格式的证书等.
比较常见的principal/credential对是用户名和密码.用户名就是声明的信息,密码就是声明的信息和系统内部是否匹配的证明.如果提交的密码与内部存储的期望的信息匹配就足以说明此用户为真.
用户证明
用户认证过程可以被分为三个步骤:
- 收集用户提交的principal和credential
- 提交收集到的信息
-
如果后台验证成功,则允许使用系统其它功能,否则需要重新认证或者禁止认证
接下来的代码演示了如何使用Shiro的API实现上述步骤:
第一步: 收集用户的principal和credential
// 通常情况下使用用户名密码进行安全认证的例子
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 自带记住我的功能
token.setRememberMe(true);
在此案例中,我们使用了UsernamePasswordToken
类,支持最常见的用户名/密码的身份认证方式.这个类实现了Shiro里面的org.apache.shiro.authc.AuthenticationToken
接口,AuthenticationToken接口的任意实现都可以被Shiro的认证系统获取此系统的使用者提交的principal和credential.
有一点很重要,Shiro根本不关心你是如何获取这些信息的,或许这些数据信息是一个用户通过html表单里面提交的,或者它检索自一个HTTP头信息,又或者它来源于一个Swing/Flex GUI密码表单,又或者仅仅是命令行的参数.从终端用户那里收集信息的过程与认证用户身份的过程是完全分离的.
你可以随意构造和创建AuthenticationToken对象实例-它与协议无关
此示例还表明,我们已经表示希望Shiro为身份验证尝试执行'Remember Me'服务.这就保证了Shiro记住此用户的登录信息,同时可以在用户一段时间后再次使用此应用时无需重复登录.我们将在之后的章节介绍Remember Me(记住我)服务.
第二步:提交principal和credential
在principal和credential收集成功并创建了一个AuthenticationToken
对象实例后,我们需要提交此令牌给Shiro平台执行真正的认证测试:
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
获取到当前正在访问的用户
后,用其调用一次login
(登录)方法,将之前创建的AuthenticationToken
实例作为登录方法的参数传过去.
每次login
(登录)方法的调用都意味着需要重新尝试认证一次.
第三步:处理成功和失败
如果login
(登录)方法平稳的执行成功了,这就意味着认证结束了!Subject
(用户)已经被认证了.此程序线程可以在没有中断的情况下继续执行,并且随后任意次数的执行SecurityUtils.getSubject()
方法都将返回已经授权的Subject
用户实例,同时随后任意次数的执行subject.isAuthenticated()
方法都将返回true
(真).
但是如果登录失败会怎么样?比如,假如终端用户提供了一个错误的密码,或者访问系统次数太多又或者他们的账户被锁定?
Shiro有一个丰富的运行时AuthenticationException
(身份验证异常)层次结构,可以准确指出尝试失败的原因.您可以将登录封装在try/catch块中,捕捉任何希望的异常,并相应地对它们作出处理.例如:
try {
currentUser.login(token);
} catch ( UnknownAccountException uae ) { // 未知账户异常
...
} catch ( IncorrectCredentialsException ice ) { // 密码错误异常
...
} catch ( LockedAccountException lae ) { // 账户被锁定异常
...
} catch ( ExcessiveAttemptsException eae ) { // 过度尝试异常
...
} ... catch your own ... // 捕捉你自定义的异常
} catch ( AuthenticationException ae ) {
//没有预料到的异常错误?
}
// 没问题,向预期的那样运行...
如果所有已经存在的异常类型都不满足你的需要,你可以自定义新的AuthenticationException
来表示特定的失败场景.
登录失败提示小技巧
虽然代码可以根据需要对特定异常作出反应并执行逻辑,但安全性最佳的方案是仅在发生故障时向最终用户显示一般故障消息,例如“用户名或密码不正确”.这确保没有特定的信息可供黑客使用,黑客可能正在尝试不同的攻击方向.
remeberd(已记住)和authenticated(已授权)
就像上面例子中展示的那样,除了普通登录外,Shiro还提供了"remember me"(记住我)的概念.现在有必要指出Shiro中remeberd Subject(已记住的用户)和实际authenticated Subject(已认证的用户)的细微不同点:
- 已记住: 一个已记住的
Subject
(用户)不是匿名用户,而且此用户有一个已知的身份(比如:subject.getPrincipals()
方法返回的就非空).但是这个身份是基于上一次认证留下的session.subject.isRemembered()
方法返回true
的时候就代表一个subject(用户)被认为是已记住的状态.
-已授权: 一个已授权的Subject
(用户)代表用户在当前session成功认证的状态(比如:login
[登录]方法在不抛出任何异常的情况下被调用).只要subject.isAuthenticated()
方法返回true
就可以认定当前用户已认证.
互斥性
已记住和已认证这两个状态只能存在一个,不能同时存在.
为什么要有区分?
认证这个词有证明的含义.那就是说已经得到保证此用户已经证明了他们是他们说的谁谁谁.
当一个用户只记住了之前的应用信息,证明的过程早已成为历史:已记住身份给出了系统一个当前用户可能是谁的提示,但事实上,没有任何办法保证已记住的用户就是预期的用户.一旦用户已认证,他们不属于已记住的范围,因为他们的身份已经在当前session中得到确认.
因此尽管程序大部分情况下仍可以针对记住的身份执行用户特定的逻辑,比如说自定义的视图,但不要执行敏感的操作直到用户成功执行身份认证使其身份得到确定.
例如,检查一个Subject
(用户)是否可以访问财务信息应该取决于isAuthenticated()
方法被认证为真,而不是isRemembered()
方法被记住为真,要确保该Subject
(用户)是期望用户的同时还要确保用户通过身份认证.
一个例子说明
下面是一个非常常见的场景帮助说明被记住和被认证之间差别为何重要。
假设你使用淘宝进行网上购物,你已经成功登录并且在购物篮中添加了一些书籍,但由于你临时要参加一个会议,匆忙中你忘记退出登录,当会议结束,回家的时间到了,于是你离开了办公室.
第二天当你回到工作,你意识到你没有完成你的购买,于是你回到淘宝网站,这时淘宝网站记得你是谁,并通过你的名字向你打招呼,同时给你提供个性化的图书推荐,对于淘宝网站,subject.isRemembered()
方法将返回true
.
但是当你想访问已买到的宝贝功能完成购买的时候会怎样呢?虽然淘宝网站'记住'了你(isRemembered()
== true
),但它不能担保现在的你就是原来的你(也许是正在使用你计算机的同事).
于是在你执行像查看用户私人信息之类的敏感操作之前,淘宝网站会强制你再次登录以使他们确信你的身份,在你登录之后,你的身份已经被验证,对于淘宝网站来说此时isAuthenticated()
方法将返回true
.
这类情景经常发生,所以Shiro加入了该功能,你可以在你的程序中使用.现在是使用isRemembered()
还是使用 isAuthenticated()
来定制你的视图和工作流完全取决于你自己,但Shiro将继续维护这项功能以防你可能会需要.
退出登录
与已认证相对的是释放所有已知的身份信息,当Subject
(用户)与程序不再交互了,你可以调用subject.logout()
方法删除所有身份信息.
currentUser.logout(); // 删除所有认证信息,使session失效,通俗的说就是退出登录
当你调用logout
,任何现存的session
将变为不可用并且所有的身份信息将丢失(如:在web程序中,位于服务器的记住我的 Cookie信息将同样被删除).
当一个Subject
(用户)退出登录,Subject
(用户)被重新认定为匿名的,对于web程序,如果需要可以重新login
(登录).
Web 程序需注意
因为在 Web 程序中记住身份信息往往使用cookies,而 cookies 只能在 Response 提交时才能被删除,所以强烈要求在为最终用户调用subject.logout()
之后立即将用户引导到一个新页面,确保任何与安全相关的Cookies如期删除,这是HTTP本身cookies功能的限制而不是Shiro的限制.
认证的顺序
直到现在,我们只看到如何在程序代码中验证一个Suject(用户),现在我们看一下当一个身份验证触发时Shiro内部发生了什么.
我们仍使用之前在架构章节里见到过的架构图,仅将左侧跟认证相关的组件高亮,每一个数字代表认证中的一个步骤:
第1步:程序代码调用
Subject.login
方法,向AuthenticationToken
(认证令牌)实例的构造函数传递用户的身份和证明信息.
第2步:Subject
实例,通常是一个DelegatingSubject
(或其子类)通过调用securityManager.login(token)
将这个令牌转交给程序的SecurityManager
.
第3步:SecurityManager
,类似于“伞”的安全管理组件,得到令牌后通过调用authenticator.authenticate(token)
方法简单地将其转交给内部的Authenticator
认证器实例.大部分情况下是一个ModularRealmAuthenticator
实例,ModularRealmAuthenticator
本质上为Apache Shiro提供一个PAM(Pluggable_Authentication_Modules 可插拔认证模块)类型的范例.用来支持在验证过程中协调一个或多个Realm实例(在 PAM 术语中每一个Realm
称为一个“模块”,每个realm代表一个获取安全数据源的方式).
第4步:如程序配置了多个Realm
模块,ModularRealmAuthenticator
实例将使用其配置的 AuthenticationStrategy
(认证策略)开始一个多Realm
身份验证的尝试.在Realm
被验证调用的整个过程中,AuthenticationStrategy
(认证策略)被调用来回应每个Realm的结果.我们将稍后讨论 AuthenticationStrategies
(认证策略).
注意:单 Realm 程序
如果仅有一个 Realm 被配置,它直接被调用--在单 Realm 程序中不需要AuthenticationStrategy
第5步:每一个配置的 Realm 都被检验看其是否支持
提交的AuthenticationToken
,如果支持,则该 Realm 的getAuthenticationInfo
方法将随着令牌的提交而被调用,getAuthenticationInfo
方法有效地表示该特定Realm
(安全领域)的单一身份验证尝试,我们将稍后讨论Realm
验证行为.
认证器
就像之前提到过的,Shiro的SecurityManager
的实现类默认使用一个ModularRealmAuthenticator
(通用安全领域认证器)实例, ModularRealmAuthenticator
(通用安全领域认证器)同样支持单 Realm 和多 Realm
在一个单 Realm 程序中,ModularRealmAuthenticator
(通用安全领域认证器)将直接调用单独的Realm
,如果配置有两个或以上 Realm,将会使用AuthenticationStrategy
(认证策略)实例来协调如何进行验证,我们将在下面的章节中讨论 AuthenticationStrategy.
如果你希望用自定义的Authenticator
(认证器)来配置SecurityManager
,可以在shiro.ini
中这样自定义:
[main]
...
authenticator = com.foo.bar.CustomAuthenticator
securityManager.authenticator = $authenticator
尽管在实际操作中,ModularRealmAuthenticator
(通用安全领域认证器)已经满足常见需求.
认证策略
当一个程序中定义了两个或多个 realm 时,ModularRealmAuthenticator
(通用安全领域认证器)使用一个内部的AuthenticationStrategy
(认证策略)组件来决定一次认证是否成功.
例如,如果一个 Realm 验证一个AuthenticationToken
成功,但其他的都失败了,那这次尝试是否被认为是成功的呢?是不是所有 Realm 验证都成功了才认为是成功?又或者一个 Realm 验证成功后,是否还有必要讨论其他Realm? AuthenticationStrategy
(认证策略)负责帮助解决这些问题.
认证策略是一个无状态的组件,在身份验证尝试期间会被查询4次(这4次交互所需的任何必要状态都将作为方法参数给出):
1.在任何 Realms 被验证之前
2.在某个的 Realm 的 getAuthenticationInfo 方法调用之前
3.在某个的 Realm 的 getAuthenticationInfo 方法调用之后
4.在所有的 Realm 被验证之后
AuthenticationStrategy
还有责任从每一个成功的 Realm 中收集结果并将它们绑定到一个单独的 AuthenticationInfo
,这个AuthenticationInfo
实例是被 Authenticator 实例返回的,并且 Shiro 用它来展现一个 Subject
(用户)的最终身份(也就是 Principals).
Subject(用户)身份view(展示)
如果你在程序中使用多于一个的 Realm 从多个数据源中获取帐户数据,AuthenticationStrategy
最终负责应用程序所看到的 Subject 身份最终合并的视图.
Shiro 有3个具体的
AuthenticationStrategy
实现类:
实现类 : 描述
-
AtLeastOneSuccessfulStrategy
: 如果有一个或多个Realm验证成功,整体认证被认为是成功的,如果没有一个验证成功,则该次认证失败 -
FirstSuccessfulStrategy
: 只有从第一个成功验证的Realm返回的信息会被使用,以后的Realm将被忽略,如果没有一个验证成功,则该次认证失败 -
AllSuccessfulStrategy
: 所有配置的Realm在全部尝试中都成功验证才被认为是成功,如果有一个验证不成功,则该次认证失败
ModularRealmAuthenticator
默认使用AtLeastOneSuccessfulStrategy
实现,这也是最常用的策略,然而你也可以配置成你希望的其他策略:
[main]
...
authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $authcStrategy
...
自定义的认证策略
如果你希望创建你自己的AuthenticationStrategy
实现,你可以使用 org.apache.shiro.authc.pam.AbstractAuthenticationStrategy
作为起点.AbstractAuthenticationStrategy
类自动实现绑定/聚集行为同时将来自于每一个 Realm 的结果收集到一个 AuthenticationInfo
实例中.
Realm 验证的顺序
非常重要的一点是,和 Realm 交互的ModularRealmAuthenticator
按迭代(iteration)顺序执行
ModularRealmAuthenticator
可以访问为SecurityManager
配置的Realm
实例,当尝试一次认证时,它将在集合中遍历,支持对提交的AuthenticationToken
进行处理的每个Realm
都将执行 Realm 的 getAuthenticationInfo
方法
隐含的顺序
在使用 Shiro INI 配置文件时,你可以通过配置 Realm 来使程序按你希望的顺序去处理 AuthenticationToken,例如,在shiro.ini
中,Realm 将按照他们在 INI 文件中定义的顺序执行:
blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm
SecurityManager上配置了这三个 Realm,但没有设置securityManager.realms属性,此时在一个验证过程中blahRealm, fooRealm, 和 barRealm 将被按照他们定义的先后顺序执行.
加不加下面的配置顺序都一样:
securityManager.realms = $blahRealm, $fooRealm, $barRealm
使用这种配置方式,你不需要调用方法来设置
securityManager
的 realms 属性,在程序读取INI文件时每一个被定义的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个, 如果在 realms 属性中只引用了3个, 这和隐含的 realm 顺序不同, 在那种情况下, 所有有效的 realm 都会用到.
Realm认证
本章阐述了当尝试一次认证时 Shiro 主要的工作流程,而在验证过程中,用到的 Realm 内产生的工作流程(如上面提到的第5步)将在 Realm 章中 Realm Authentication 节讨论