【Shiro】 验证

Metadata

title: 【Shiro】 验证
date: 2023-01-19 13:42
tags:
  - 行动阶段/完成
  - 主题场景/组件
  - 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
  - 细化主题/Module/Shiro/基础
categories:
  - Shiro
keywords:
  - Shiro
description: 【Shiro】 验证

【Shiro】 验证

身份验证是验证用户身份的过程。也就是说,当用户通过应用程序进行身份验证时,他们就是在证明他们确实是他们所说的那个人。这有时也称为“登录”。这通常是一个三步过程。

  1. 收集用户的身份信息,称为principals(身份),并支持身份证明,称为credentials (凭证)。
  2. 将身份和凭据提交到系统。
  3. 如果提交的凭据与系统对该用户身份(主体)的预期匹配,则认为该用户已通过身份验证。如果它们不匹配,则认为用户未通过身份验证。
    每个人都熟悉的此过程的一个常见示例是用户名/密码组合。当大多数用户登录软件应用程序时,他们通常会提供用户名(主体)和支持密码(凭证)。如果存储在系统中的密码(或其表示形式)与用户指定的相匹配,则认为他们已通过身份验证。

每个人都熟悉的此过程的一个常见示例是用户名/密码组合。当大多数用户登录软件应用程序时,他们通常会提供用户名(主体)和支持密码(凭证)。如果存储在系统中的密码(或其表示形式)与用户指定的相匹配,则认为他们已通过身份验证。

Shiro 以简单直观的方式支持相同的工作流程。正如我们所说,Shiro 有一个以 Subject 为中心的 API——几乎所有你在运行时使用 Shiro 做的事情都是通过与当前正在执行的 Subject 交互来实现的。因此,要登录一个 Subject,您只需调用它的登录方法,传递一个 AuthenticationToken 实例,该实例代表提交的主体和凭据(在本例中为用户名和密码)。下面的清单 5 中显示了这个示例。

清单5: 主题登录

//1. 获取提交的主体和凭证:
AuthenticationToken token = 
new UsernamePasswordToken(username, password);
//2。获取当前Subject:
Subject currentUser = SecurityUtils.getSubject();

//3。登录:
currentUser.login(token);

如您所见,Shiro 的 API 很容易反映常见的工作流程。您将继续将这种简单性视为 Subject 的所有操作的主题。当调用登录方法时,SecurityManager 将接收 AuthenticationToken 并将其分派到一个或多个已配置的 Realms,以允许每个 Realms 根据需要执行身份验证检查。每个领域都能够根据需要对提交的 AuthenticationTokens 做出反应。但是如果登录尝试失败会发生什么?如果用户指定了错误的密码怎么办?您可以通过响应 Shiro 的运行时 AuthenticationException 来处理失败,如清单 6 所示。

清单6: 处理登录失败

//3。登录:
try { 
    currentUser.login(token); 
} catch (IncorrectCredentialsException ice) { ... 
} catch (LockedAccountException lae) { ... 
} 
... 
catch (AuthenticationException ae) { ... 
}

您可以选择捕获其中一个 AuthenticationException 子类并做出具体反应,或者一般地处理任何 AuthenticationException(例如,向用户显示一条通用的“用户名或密码不正确”消息)。您可以根据自己的应用要求进行选择。

Subject 成功登录后,他们被认为已通过身份验证,通常您允许他们使用您的应用程序。但是仅仅因为用户证明了他们的身份并不意味着他们可以在您的应用程序中为所欲为。这就引出了下一个问题,“我如何控制用户可以做什么或不可以做什么?” 决定允许用户做什么称为授权。接下来我们将介绍 Shiro 如何启用授权。

Authenticating Subjects

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

  1. 收集受试者提交的主体和证书
  2. 提交主体和凭据进行身份验证。
  3. 如果提交成功,则允许访问,否则重试认证或阻止访问。

下面的代码演示了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表单的用户获取的,也可能是从HTTP报头中检索的,也可能是从Swing或Flex GUI密码表单中读取的,也可能是通过命令行参数获取的。从应用程序最终用户收集信息的过程与Shiro的AuthenticationToken概念完全分离。
您可以按照自己的喜好构造和表示AuthenticationToken实例——它与协议无关。

这个示例还表明,我们已经表明希望Shiro为身份验证尝试执行“Remember Me”服务。这可以确保Shiro在用户稍后返回应用程序时记住用户身份。我们将在后面的章节介绍Remember Me服务。

步骤2:提交主体和凭证

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

Subject currentUser = SecurityUtils.getSubject();

currentUser.login(token);

在获取当前正在执行的Subject之后,我们进行一次登录调用,传入前面创建的AuthenticationToken实例。

对登录方法的调用有效地表示一次身份验证尝试。

步骤3:处理成功或失败

如果login方法安静地返回,那么我们就完成了!主题已通过认证。应用程序线程可以不间断地继续,所有对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 ) {
    //unexpected error?
}

//No problems, continue on as expected...

如果现有的某个异常类不能满足您的需求,可以创建自定义Authenticationexception来表示特定的失败场景。

登录失败提示
虽然您的代码可以根据需要对特定的异常做出反应并执行逻辑,但安全最佳实践是在发生故障时仅向最终用户显示通用的失败消息,例如“用户名或密码不正确”。这确保了可能试图进行攻击的黑客无法获得特定信息。

Remembered vs. Authenticated

如上面的例子所示,Shiro除了支持正常的登录过程外,还支持“记住我”的概念。值得指出的是,Shiro在记忆的Subject和实际验证的Subject之间做出了非常精确的区分:

  • Remembered: 记住的Subject不是匿名的,它有一个已知的身份(即Subject . getprincipals()是非空的)。但是这个标识是在以前的会话中从以前的身份验证中记住的。如果subject. isremembered()返回true,则该Subject被认为是已记住的。
  • Authenticated: 一个Authenticated Subject是一个在Subject当前会话期间成功通过身份验证的Subject(即登录方法被调用而没有抛出异常)。如果subject. isauthenticated()返回true,则认为主题已验证。

互相排斥的
记住的状态和已验证的状态是互斥的——一个为真值表示另一个为假值,反之亦然。

Why the distinction?

“认证”这个词有很强的证明的内涵。也就是说,有一个预期的保证,即主体已经证明了他们是他们所说的那个人。

当一个用户只在与应用程序的前一次交互中被记住时,证明状态就不再存在:被记住的身份给了系统一个用户可能是谁的想法,但在现实中,没有办法绝对保证被记住的Subject是否代表预期的用户。一旦主题经过身份验证,就不再认为它们只是被记住了,因为它们的身份将在当前会话期间被验证。

因此,尽管应用程序的许多部分仍然可以基于记住的主体执行特定于用户的逻辑,例如自定义视图,但在用户通过执行成功的身份验证尝试合法地验证了自己的身份之前,它通常不应该执行高度敏感的操作。

例如,检查Subject是否可以访问财务信息几乎总是依赖于isAuthenticated(),而不是isRemembered(),以保证预期的和已验证的身份。

举例说明

下面是一个相当常见的场景,它有助于说明为什么记忆和验证之间的区别是重要的。

假设你正在使用亚马逊网站。您已经成功登录,并向购物车中添加了几本书。但是你要赶去开会,却忘了登出。当会议结束的时候,是时候回家了,你离开了办公室。

第二天当你来上班时,你意识到你没有完成购买,所以你回到亚马逊网站。这一次,亚马逊会“记住”你是谁,叫出你的名字问候你,还会给你一些个性化的图书推荐。对于Amazon, subject.isRemembered()将返回true。

但是,如果你试图访问你的账户来更新你的信用卡信息来购买书籍,会发生什么呢?虽然亚马逊“记得”你(isRemembered() == true),但它不能保证你就是你自己(例如,可能某个同事正在使用你的电脑)。

因此,在你执行像更新信用卡信息这样的敏感操作之前,亚马逊会强迫你登录,以确保你的身份。登录后,您的身份已经验证,对于Amazon, isAuthenticated()现在将为真。

对于许多类型的应用程序,这种情况经常发生,因此Shiro内置了该功能,以便您可以在自己的应用程序中利用它。现在,您是使用isRemembered()还是isAuthenticated()来定制视图和工作流取决于您,但是Shiro将维护这个基本状态以备您需要。

Logging Out

与身份验证相反的是释放所有已知的标识状态。当Subject完成与应用程序的交互时,你可以调用Subject .logout()来放弃所有标识信息:

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

当你调用logout时,任何现有的Session都将失效,任何身份都将被解除关联(例如,在web应用程序中,RememberMe cookie也将被删除)。

在Subject注销后,Subject实例再次被认为是匿名的,除了web应用程序,如果需要,可以重新用于登录。

Web Application Notice
因为在web应用程序中记住的身份通常是通过cookie持久化的,并且cookie只能在提交响应体之前删除,所以强烈建议在调用subject.logout()后立即将最终用户重定向到新的视图或页面。这保证了任何与安全相关的cookie都可以按照预期删除。这是HTTP cookie功能的限制,而不是Shiro的限制。

身份验证序列

到目前为止,我们只讨论了如何在应用程序代码中验证Subject。现在,我们将讨论当尝试进行身份验证时Shiro内部会发生什么。

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

  • 步骤1:应用程序代码调用Subject。方法,传入表示最终用户的主体和凭证的构造的AuthenticationToken实例。
  • 步骤2:Subject实例,通常是一个DelegatingSubject(或子类),通过调用SecurityManager .login(令牌)委托给应用程序的SecurityManager,从那里开始实际的身份验证工作。
  • 步骤3:作为一个基本的“保护伞”组件,SecurityManager接收令牌,并通过调用Authenticator .authenticate(token)简单地委托给它内部的Authenticator实例。这几乎总是一个ModularRealmAuthenticator实例,它支持在身份验证期间协调一个或多个Realm实例。ModularRealmAuthenticator本质上为Apache Shiro提供了PAM风格的范例(其中每个Realm在PAM术语中都是一个“模块”)。
  • 步骤4:如果为应用程序配置了多个域,ModularRealmAuthenticator实例将利用其配置的AuthenticationStrategy发起多域身份验证尝试。在调用域进行身份验证之前、期间和之后,将调用AuthenticationStrategy以允许它对每个域的结果做出反应。我们将很快讨论身份验证策略。
  • 步骤5:查看每个配置的Realm是否支持提交的AuthenticationToken。如果是,支持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

AuthenticationStrategy | 认证策略

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

例如,如果只有一个Realm对AuthenticationToken进行了成功的身份验证,但其他所有域都失败了,是否认为该身份验证尝试成功?或者所有的领域都必须成功验证,才能认为整个尝试是成功的?或者,如果一个领域验证成功,是否有必要进一步咨询其他领域?AuthenticationStrategy根据应用程序的需要做出适当的决策。

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

  • 在任何领域被调用之前
  • 在每个Realm的getAuthenticationInfo方法调用之前
  • 在单个Realm的getAuthenticationInfo方法调用之后立即调用
  • 在所有的领域都被调用之后

AuthenticationStrategy还负责聚合来自每个成功Realm的结果,并将它们“捆绑”到单个AuthenticationInfo表示中。这个最终的聚合AuthenticationInfo实例是Authenticator实例返回的,也是Shiro用来表示Subject的最终身份(又名主体)的实例。

Shiro有3个具体的AuthenticationStrategy实现:

认证策略类 描述
AtLeastOneSuccessfulStrategy 如果一个(或多个)Realms认证成功,则认为整个尝试成功。如果没有认证成功,则尝试失败。
FirstSuccessfulStrategy 只有从第一个成功认证的Realm返回的信息将被使用。所有其他领域将被忽略。如果没有验证成功,则尝试失败。
AllSuccessfulStrategy 所有配置的域必须成功验证,才能认为整体尝试成功。如果任何一方没有验证成功,则尝试失败。

ModularRealmAuthenticator默认为atleastonessuccessfulstrategy实现,因为这是最常用的策略。然而,如果你想,你可以配置一个不同的策略:

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

securityManager.authenticator.authenticationStrategy = $authcStrategy

...

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

Realm Authentication Order | 验证顺序

很重要的一点是,ModularRealmAuthenticator将以迭代顺序与Realm实例交互。

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

Implicit Ordering | 隐式顺序

当使用Shiro的INI配置格式时,你应该按照你希望它们处理AuthenticationToken的顺序来配置realm。例如,在’ shiro.ini中,Realms将按照它们在INI文件中定义的顺序进行查询。对于下面的shiro.ini示例:

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

SecurityManager将配置这三个域,在尝试身份验证时,blahRealm、fooRealm和barRealm将按此顺序调用。

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

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

使用这种方法,您不需要设置securityManager的realms属性—定义的每个域都将自动添加到realms属性。

Explicit Ordering | 显示顺序

如果您想显式地定义与领域交互的顺序,而不管它们是如何定义的,您可以将securityManager的realms属性设置为一个显式的集合属性。例如,如果使用上面的定义,但你希望blahRealm是最后而不是第一个被查询:

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

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