Apache Shiro 身份验证

身份验证是身份验证的过程-也就是说,证明用户实际上就是他们所说的真实身份。为了使用户证明自己的身份,他们需要提供一些标识信息以及系统可以理解和信任的那种身份证明。

这是通过向 Shiro 提交用户的* principals credentials *来查看它们是否与应用程序期望的匹配的。

  • 校长 是受试者的“识别属性”。校长可以是任何可以识别主题的东西,例如名字(姓氏),姓氏(姓氏或姓氏),用户名,社会保险号等。当然,诸如姓氏之类的东西并不擅长唯一地标识Subject,因此用于身份验证的最佳主体对于应用程序是唯一的-通常是用户名或电子邮件地址。

Primary Principal

虽然 Shiro 可以代表任意数量的主体,但 Shiro 希望应用程序具有一个“主要”主体-一个唯一标识应用程序中“主题”的值。在大多数应用程序中,这通常是用户名,电子邮件地址或全局唯一的用户 ID。

  • 凭据 通常是只有Subject知道的 Secret 值,这些 Secret 值被用作证明它们实际上“拥有”所声明身份的证据。凭据的一些常见示例是密码,bioFeature 数据(例如指纹和视网膜扫描)以及 X.509 证书。

主体/凭证配对的最常见示例是用户名和密码。用户名是要求保护的身份,密码是与要求保护的身份匹配的证明。如果提交的密码与应用程序期望的密码匹配,则该应用程序可以在很大程度上假定用户确实是他们所说的那个人,因为没有其他人应该知道相同的密码。

Authenticating Subjects

验证Subject的过程可以有效地分为三个不同的步骤:

  • 收集主题提交的主体和证书

  • 提交主体和凭据以进行身份验证。

  • 如果提交成功,则允许访问,否则重试身份验证或阻止访问。

以下代码演示了 Shiro 的 API 如何反映这些步骤:

步骤 1:收集主题的主体和证书

//Example using most common scenario of username/password pair:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);

//"Remember Me" built-in: 
token.setRememberMe(true);

在这种情况下,我们使用的是UsernamePasswordToken,它支持最常用的用户名/密码身份验证方法。这是 Shiro 的org.apache.shiro.authc.AuthenticationToken接口的实现,该接口是 Shiro 的身份验证系统用来表示提交的主体和凭据的基本接口。

在这里需要注意的重要一点是 Shiro 不在乎您如何获取此信息:数据可能是由提交 HTML 表单的用户获取的,或者是从 HTTPHeaders 中检索的,或者是从 Swing 或 Flex 中读取的 GUI 密码形式,或通过命令行参数。从应用程序最终用户收集信息的过程与 Shiro 的AuthenticationToken概念完全脱钩。

您可以随意构建并表示AuthenticationToken个实例-它与协议无关。

此示例还表明,我们已表明我们希望 Shiro 为身份验证尝试执行“记住我”服务。这样可以确保 Shiro 能够在以后返回应用程序时记住用户身份。我们将在下一章介绍“记住我”服务。

步骤 2:提交主体和凭据

在收集了主体和凭据并将其表示为AuthenticationToken实例之后,我们需要将令牌提交给 Shiro 来执行实际的身份验证尝试:

Subject currentUser = SecurityUtils.getSubject();

currentUser.login(token);

获取当前执行的Subject之后,我们进行一次login调用,并传入我们先前创建的AuthenticationToken实例。

login方法的调用有效地表示身份验证尝试。

步骤 3:处理成功或失败

如果login方法悄悄返回,就可以了-我们完成了! Subject已通过身份验证。应用程序线程可以不间断地 continue,对SecurityUtils.getSubject()的所有其他调用将返回已认证的Subject实例,对subject. isAuthenticated()的任何调用都将返回true

但是,如果登录尝试失败怎么办?例如,如果最终用户提供了错误的密码,或者访问了系统太多次而可能他们的帐户被锁定了怎么办?

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来表示特定的故障情况。

Login Failure Tip

尽管您的代码可以对特定的异常做出反应并在必要时执行逻辑,但是安全性最佳做法是仅在发生失败时向最终用户显示通用失败消息,例如“用户名或密码不正确”。这样可确保没有任何特定信息可供尝试攻击媒介的黑客使用。

记住与已认证

如上例所示,除了常规的登录过程之外,Shiro 还支持“记住我”的概念。值得指出的是,此时,Shiro 在“记住的”主题和实际的“已认证的”主题之间做出了非常精确的区分:

  • 记住 :记住的Subject不是匿名的,并且具有已知的身份(即subject. getPrincipals()是非空的)。但是在“先前”会话期间,通过先前的身份验证会记住此身份。如果subject. isRemembered()返回true,则认为该主题已被记住。

  • Authenticated :已认证的Subject是已经成功认证的对象(即login方法被调用而没有引发异常)在主体当前会话期间。如果subject. isAuthenticated()返回true,则认为该主题已认证。

Mutually Exclusive

记住和已认证状态是互斥的-一个的true值表示另一个的false值,反之亦然。

为什么要区分?

“认证”一词具有* proof *的强烈含义。也就是说,可以预期的“保证”是Subject已经证明自己是他们所说的人。

当仅通过与应用程序的先前交互来记住用户时,证明状态将不复存在:记住的身份使系统了解该用户可能是谁,但实际上,如果记住的主题代表预期的用户。主题通过身份验证后,就不再认为它们仅被记住,因为它们的身份在当前会话中已经过验证。

因此,尽管应用程序的许 Multipart 仍可以基于记住的原则(例如自定义视图)执行特定于用户的逻辑,但通常不应执行高度敏感的操作,直到用户通过执行成功的身份验证尝试合法地验证了其身份。

例如,检查Subject是否可以访问财务信息的检查应几乎始终取决于isAuthenticated()而不是isRemembered(),以保证期望的身份和经过验证的身份。

一个示例

以下是一个相当常见的场景,有助于说明为什么记住和已认证之间的区别很重要。

假设您使用的是Amazon.com。您已成功登录,并已在购物车中添加了几本书。但是您必须参加会议,但忘记注销。会议结束时,该回家了,您离开办公室了。

第二天上班时,您发现自己还没有完成购买,因此回到 amazon.com。这次,亚马逊“记住”您的身份,以名字向您打招呼,并仍然为您提供一些个性化的书本推荐。对于亚马逊,subject.isRemembered()将返回true

但是,如果您尝试访问帐户以更新信用卡信息以购买图书,会发生什么情况?当亚马逊“记住”您(isRemembered() == true)时,它不能保证您实际上就是您(例如,某个同事正在使用您的计算机)。

因此,在您执行敏感操作(如更新信用卡信息)之前,亚马逊会强迫您登录,以便他们保证您的身份。登录后,您的身份已通过验证,并且到亚马逊的isAuthenticated()现在为true

这种情况在许多类型的应用程序中经常发生,因此该功能是 Shiro 内置的,因此您可以将其用于自己的应用程序。现在,是否使用isRemembered()isAuthenticated()来定制视图和工作流已由您决定,但是 Shiro 将保留此基本状态,以备不时之需。

Logging Out

认证的相反是释放所有已知的标识状态。完成Subject与应用程序的交互后,您可以调用subject. logout()放弃所有标识信息:

currentUser.logout(); //removes all identifying information and invalidates their session too.

当您呼叫logout时,任何现有的Session都将失效并且任何身份都将被取消关联(例如,在网络应用中,RememberMe cookie 也将被删除)。

Subject注销后,该Subject实例再次被视为匿名,并且除 Web 应用程序外,如果需要,可以再次用于login

Web Application Notice

由于 Web 应用程序中记住的身份通常与 cookie 保持在一起,并且 cookie 只能在提交响应正文之前删除,因此强烈建议在调用subject.logout()之后立即将最终用户重定向到新视图或页面。这样可以保证所有与安全性有关的 cookie 都可以按预期删除。这是 HTTP cookie 的功能限制,而不是 Shiro 的限制。

Authentication Sequence

到目前为止,我们仅研究了如何从应用程序代码中对Subject进行身份验证。现在,我们将介绍发生身份验证尝试时 Shiro 内部发生的情况。

我们从Architecture章中获取了之前的 Schema,仅突出显示了与身份验证相关的组件。每个数字代表身份验证尝试中的步骤:

步骤 1 :应用程序代码调用Subject.login方法,并传入表示最终用户的主体和凭据的AuthenticationToken实例。

步骤 2Subject实例(通常是DelegatingSubject(或子类))通过调用securityManager.login(token)委托应用程序的SecurityManager,实际的身份验证工作在此开始。

步骤 3SecurityManager是基本的“伞”组件,接收令牌并通过调用authenticator.authenticate(token)来简单地委派给其内部的Authenticator实例。这几乎总是一个ModularRealmAuthenticator实例,它支持在身份验证期间协调一个或多个Realm实例。 ModularRealmAuthenticator本质上为 Apache Shiro 提供了PAM风格的范例(其中每个Realm是 PAM 术语中的“模块”)。

步骤 4 :如果为该应用程序配置了多个Realm,则ModularRealmAuthenticator实例将使用其已配置的AuthenticationStrategy发起多次Realm身份验证尝试。在调用Realms进行身份验证之前,期间和之后,将调用AuthenticationStrategy以使其对每个 Realm 的结果做出反应。我们将尽快涵盖AuthenticationStrategies

Single-Realm Application

如果仅配置一个 Realm,则将直接调用它-在单个 Realm 应用程序中不需要AuthenticationStrategy

步骤 5 :咨询每个已配置的Realm,以查看其是否supports提交的AuthenticationToken。如果是这样,将使用提交的token调用支持的 Realm 的getAuthenticationInfo方法。 getAuthenticationInfo方法有效地表示对该特定Realm的单个身份验证尝试。我们将很快介绍Realm身份验证行为。

Authenticator

如前所述,Shiro SecurityManager实现默认使用ModularRealmAuthenticator实例。 ModularRealmAuthenticator同样支持具有单个领域以及具有多个领域的应用程序。

在单领域应用程序中,ModularRealmAuthenticator将直接调用单个Realm。如果配置了两个或更多领域,它将使用AuthenticationStrategy实例来协调尝试的发生方式。我们将在下面介绍 AuthenticationStrategies。

如果您希望使用自定义Authenticator实现来配置SecurityManager,则可以在shiro.ini中进行配置,例如:

[main]
...
authenticator = com.foo.bar.CustomAuthenticator

securityManager.authenticator = $authenticator

尽管实际上ModularRealmAuthenticator可能适合大多数需求。

AuthenticationStrategy

当为一个应用程序配置了两个或多个领域时,ModularRealmAuthenticator依靠内部AuthenticationStrategy组件来确定认证尝试成功或失败的条件。

例如,如果只有一个 Realm 成功验证了AuthenticationToken,而其他所有都失败了,那么该验证尝试是否被视为成功?还是所有领域都必须成功进行身份验证才能将整体尝试视为成功?或者,如果某个领域成功通过身份验证,是否有必要进一步咨询其他领域? AuthenticationStrategy根据应用程序的需求做出适当的决定。

AuthenticationStrategy 是 Stateless 组件,在尝试进行身份验证时会被查询 4 次(这 4 种交互所需的任何必要状态都将作为方法参数给出):

  • 在任何领域被调用之前

  • 在调用单个 Realm 的getAuthenticationInfo方法之前

  • 在调用单个 Realm 的getAuthenticationInfo方法之后

  • 在所有领域都被调用之后

另外,AuthenticationStrategy负责汇总每个成功 Realm 的结果,并将它们“Binding”成单个AuthenticationInfo表示形式。最终的聚合AuthenticationInfo实例是Authenticator实例返回的结果,也是 Shiro 用来表示Subject的最终身份(也称为 Principals)的东西。

Subject Identity 'View'

如果您在应用程序中使用多个 Realm 从多个数据源获取帐户数据,则AuthenticationStrategy最终负责应用程序看到的主体身份的最终“合并”视图。

Shiro 有 3 个具体的AuthenticationStrategy实现:

AuthenticationStrategyDescription
AtLeastOneSuccessfulStrategy如果一个(或多个)领域成功认证,则整个尝试都被视为成功。如果没有成功进行身份验证,则尝试将失败。
FirstSuccessfulStrategy仅使用从第一个成功通过身份验证的领域返回的信息。所有其他领域将被忽略。如果没有成功通过身份验证,则尝试将失败。
AllSuccessfulStrategy所有配置的领域都必须成功进行身份验证,才能将整体尝试视为成功。如果任何人未成功通过身份验证,则尝试将失败。

ModularRealmAuthenticator默认为 AtLeastOneSuccessfulStrategy 实现,因为这是最常用的策略。但是,您可以根据需要配置其他策略:

[main]
...
authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategy

securityManager.authenticator.authenticationStrategy = $authcStrategy

...

Custom AuthenticationStrategy

如果您想自己创建自己的AuthenticationStrategy实现,则可以使用org.apache.shiro.authc.pam.AbstractAuthenticationStrategy作为起点。 AbstractAuthenticationStrategy类自动实现将每个 Realm 的结果合并到单个AuthenticationInfo实例中的“Binding” /聚合行为。

领域身份验证 Sequences

指出ModularRealmAuthenticator将以* iteration *Sequences 与 Realm 实例进行交互非常重要。

ModularRealmAuthenticator有权访问SecurityManager上配置的Realm实例。在执行身份验证尝试时,它将遍历该集合,对于支持提交的AuthenticationToken的每个Realm,调用 Realm 的getAuthenticationInfo方法。

Implicit Ordering

使用 Shiro 的 INI 配置格式时,应按希望它们处理AuthenticationToken *的 Sequences 配置 Realms *。例如,在shiro.ini中,将按照在 INI 文件中定义的 Sequences 查询领域。也就是说,对于以下shiro.ini示例:

blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm

SecurityManager将配置有这三个领域,并且在身份验证尝试期间,将按该 Sequences*调用blahRealmfooRealmbarRealm

与定义以下行基本上具有相同的效果:

securityManager.realms = $blahRealm, $fooRealm, $barRealm

使用这种方法,您无需设置securityManager's realms属性-定义的每个领域都会自动添加到realms属性。

Explicit Ordering

如果要显式定义与领域交互的 Sequences,无论它们如何定义,都可以将 securityManager 的realms属性设置为显式集合属性。例如,如果使用上面的定义,但您希望最后查询blahRealm而不是首先查询:

blahRealm = com.company.blah.Realm
...
fooRealm = com.company.foo.Realm
...
barRealm = com.company.another.Realm

securityManager.realms = $fooRealm, $barRealm, $blahRealm
...

Explicit Realm Inclusion

当您显式配置securityManager.realms属性时,* only *所引用的领域将在SecurityManager上进行配置。这意味着您可以在 INI 中定义 5 个领域,但是如果realms属性引用了 3 个,则只能实际使用 3 个领域。这与将使用所有可用领域的隐式领域排序不同。

Realm Authentication

本章介绍了 Shiro 的主工作流程,解释了如何进行身份验证尝试。在Realm章的Realm Authentication部分中介绍了在身份验证期间参考的单个领域中发生的内部工作流(即上面的“第 5 步”)。

协助处理文档

尽管我们希望该文档对您使用 Apache Shiro 所做的工作有所帮助,但社区一直在不断改进和扩展文档。如果您想为 Shiro 项目提供帮助,请考虑在需要的地方更正,扩展或添加文档。您提供的每一点帮助都会扩大社区,进而改善 Shiro。

提交文档的最简单方法是通过单击下面的Edit链接提交请求请求,然后将其发送到User Forum用户邮件列表