计算机系统应用教程网站

网站首页 > 技术文章 正文

理解 AuthenticationProvider 理解当代中国

btikc 2024-09-29 09:56:31 技术文章 17 ℃ 0 评论

在本 spring security 系列的前面文章中,我们介绍了在身份验证流程中起作用的一些组件。 我们讨论了 UserDetails 以及如何定义原型来描述 Spring Security 中的用户。 然后,我们在示例中使用了 UserDetails,在其中您了解了 UserDetailsService 和 UserDetailsManager 合同的工作方式以及如何实现这些合同。 我们还在示例中讨论并使用了这些接口的主要实现。 最后,您学习了 PasswordEncoder 如何管理密码以及如何使用密码,以及 Spring Security 加密模块(SSCM)及其加密器和密钥生成器。

决定是否对请求进行身份验证的条件和指令。将此责任委托给 AuthenticationProvider 的组件是 AuthenticationManager,它接收来自 HTTP 过滤器层的请求。我们将在过滤器系列文章中详细讨论过滤器层。在本系列中,让我们看看身份验证过程,它只有两个可能的结果:

  • 发出请求的实体未经身份验证。 无法识别用户,并且应用程序拒绝该请求而不委托授权过程。 通常,在这种情况下,发送回客户端的响应状态为 HTTP 401 未经授权。
  • 发出请求的实体已通过身份验证。 存储有关请求者的详细信息,以便应用程序可以将其用于授权。 您将在本章中找到,SecurityContext 接口是一个实例,用于存储有关当前已验证请求的详细信息。

为了提醒您这些参与者以及它们之间的连接,图 1 提供了您在前面文章中也看到的图。


Spring Security 中的认证流程。 该过程定义了应用程序如何识别发出请求的人。 在图中,本文讨论的组件以阴影表示。 此处,AuthenticationProvider 在此过程中实现了身份验证逻辑,而 SecurityContext 存储了有关已身份验证请求的详细信息。


本文将介绍身份验证流程的其余部分 (图 1 中的阴影框)。然后,在后面,您将学习授权是如何工作的,这是 HTTP 请求中身份验证之后的过程。首先,我们需要讨论如何实现 AuthenticationProvider 接口。您需要知道 Spring Security 如何理解身份验证过程中的请求。

为了让您清楚地描述如何展示请求,我们将从 Authentication 接口开始。讨论完这一点后,我们可以进一步观察成功身份验证后请求的细节发生了什么。成功的身份验证之后,我们可以讨论 SecurityContext 接口以及 Spring Security 管理它的方式。在后面,您将学习如何自定义 HTTP Basic 身份验证方法。我们还将讨论另一个可以在应用程序中使用的身份验证选项—基于表单的登录。

理解 AuthenticationProvider

在企业应用程序中,您可能会发现自己处于一种情况,即基于用户名和密码的默认身份验证实现不适用。此外,当涉及到身份验证时,您的应用程序可能需要实现几个场景(图2)。例如,您可能希望用户能够通过使用SMS消息中接收的代码或由特定应用程序显示的代码来证明他们是谁。或者,您可能需要实现身份验证场景,其中用户必须提供存储在文件中的某种密钥。您甚至可能需要使用用户指纹的表示来实现身份验证逻辑。框架的目的是要足够灵活,允许您实现这些所需的任何场景。


对于应用程序,您可能需要以不同的方式实现身份验证。 尽管在大多数情况下,用户名和密码已足够,但在某些情况下,用户身份验证方案可能更复杂。

框架通常提供一组最常用的实现,但是,它当然不能涵盖所有可能的选择。 在 Spring Security 方面,您可以使用 AuthenticationProvider 约定来定义任何自定义身份验证逻辑。 在本节中,您将通过实现 Authentication 接口,然后使用 AuthenticationProvider 创建自定义身份验证逻辑来学习表示身份验证事件。 为了实现我们的目标

  • 首先,我们分析 Spring Security 如何表示身份验证事件。
  • 然后,我们讨论 AuthenticationProvider 约定,该约定负责身份验证逻辑。
  • 再然后,您可以通过在示例中实现 AuthenticationProvider 约定来编写自定义身份验证逻辑

表示身份验证期间的请求

在本节中,我们将讨论 Spring Security 如何在身份验证过程中表示请求。在深入实现自定义身份验证逻辑之前,有必要先讨论这个问题。正如您将在下一节中学到的,要实现自定义 AuthenticationProvider,您首先需要了解如何表示身份验证事件本身。在本节中,我们将查看表示身份验证的契约,并讨论您需要知道的方法。

身份验证是流程中涉及到的具有相同名称的基本接口之一。Authentication 接口表示身份验证请求事件,并保存请求访问应用程序的实体的详细信息。您可以在身份验证过程期间和之后使用与身份验证请求事件相关的信息。请求访问应用程序的用户称为主体。如果您曾经在任何应用程序中使用过 Java Security API,您就会知道在Java Security API 中,一个名为 Principal 的接口表示相同的概念。Spring Security 的 Authentication 接口扩展了这个约定(图 3)。

Authentication 约定继承了 Principal 约定,Authentication 约定增加了需求,例如需要密码或可以指定有关身份验证请求的更多详细信息。 其中的一些详细信息(例如权限列表)是特定于 Spring Security 的。

Spring Security 中的 Authentication 约定不仅代表一个 Principal,还添加了关于身份验证过程是否完成的信息,以及一个权限集合。事实上,这个约定是为了从 Java Security API 扩展Principal 约定而设计的,这在与其他框架和应用程序实现的兼容性方面是一个加分项。这种灵活性允许从以另一种方式实现身份验证的应用程序更方便地迁移到 Spring Security。

在下面的清单中,让我们进一步了解身份验证接口的设计。

清单 1 在Spring Security中声明的身份验证接口

public interface Authentication extends Principal, Serializable {

  Collection<? extends GrantedAuthority> getAuthorities();
  Object getCredentials();
  Object getDetails();
  Object getPrincipal();
  boolean isAuthenticated();
  void setAuthenticated(boolean isAuthenticated) 
     throws IllegalArgumentException;
}

目前,你需要学习的约定方法只有以下几种:

  • isAuthenticated()— 如果身份验证过程结束,则返回 true;如果身份验证过程仍在进行,则返回 false。
  • getCredentials()—返回在身份验证过程中使用的密码或任何密钥。
  • getAuthorities()—为经过身份验证的请求返回已授予的权限的集合。

我们将在后面的章节中讨论用于 Authentication 约定的其他方法,这些方法适合我们接下来讨论的实现。

实现自定义身份验证逻辑

在本节中,我们将讨论如何实现自定义身份验证逻辑。我们将分析与此职责相关的 Spring Security 约定,以理解其定义。通过这些细节,您可以使用下一节中的代码示例实现自定义身份验证逻辑。

Spring Security 中的 AuthenticationProvider 负责验证逻辑。AuthenticationProvider 接口的默认实现将查找系统用户的职责委托给 UserDetailsService。在认证过程中,还使用了 PasswordEncoder 进行密码管理。下面的清单给出了 AuthenticationProvider 的定义,您需要实现它来为您的应用程序定义一个自定义身份验证提供者。

清单 2 AuthenticationProvider 接口

public interface AuthenticationProvider {

  Authentication authenticate(Authentication authentication) 
    throws AuthenticationException;

  boolean supports(Class<?> authentication);
}

AuthenticationProvider 职责与 Authentication 约定强耦合。authenticate() 方法接收一个 Authentication 对象作为参数并返回一个 Authentication 对象。我们实现 authenticate() 方法来定义身份验证逻辑。我们可以用三个要点快速总结一下应该如何实现 authenticate() 方法:

  • 如果身份验证失败,则该方法应引发 AuthenticationException。
  • 如果该方法接收到 AuthenticationProvider 的实现不支持的身份验证对象,则该方法应返回null。通过这种方式,我们可以使用在定 HTTP 过滤器级别分离的多个身份验证类型。
  • 该方法应该返回一个表示完全身份验证对象的 Authentication 实例。对于这个实例,isAuthenticated() 方法返回 true,它包含有关已验证实体的所有必要细节。通常,应用程序还会从该实例中删除密码等敏感数据。实现之后,就不再需要密码了,保留这些细节可能会暴露给不必要的人。

AuthenticationProvider 接口中的第二个方法是 supports(Class<?> authentication)。如果当前的 AuthenticationProvider 支持作为 Authentication 对象提供的类型,则可以实现此方法返回 true。注意,即使该方法为对象返回 true, authenticate() 方法仍然有可能通过返回 null拒绝请求。 Spring Security 这样设计是为了更加灵活,并允许您实现一个 AuthenticationProvider,该 provider 可以根据请求的详细信息拒绝身份验证请求,而不仅仅是根据其类型。

身份验证管理器和身份验证提供者如何一起验证或使身份验证请求无效的类似方法是为您的门设置更复杂的锁。你可以使用卡片或老式的物理钥匙来打开这个锁 ( 图 4 )。锁本身是决定是否开门的身份验证管理器。为了做出这个决定,它委托给两个身份验证提供者:一个知道如何验证卡片,另一个知道如何验证物理密匙。如果您提供一张卡来开门,只使用物理密钥的身份验证提供者会抱怨它不知道这种身份验证。但另一个提供者支持这种身份验证,并验证卡是否对门有效。这实际上是 supports() 方法的目的。


AuthenticationManager 委托给可用的身份验证提供者之一。AuthenticationProvider 可能不支持所提供的身份验证类型。另一方面,如果它支持对象类型,它可能不知道如何验证特定的对象。对身份验证进行评估,身份验证提供者可以判断请求是否正确,并向 AuthenticationManager 作出响应。

除了测试身份验证类型外,Spring Security 还增加了一层灵活性。门锁可以识别多种卡片。在这种情况下,当您提供一张卡片时,其中一个身份验证提供者可能会说,“我认为这是一张卡片。但这不是我能验证的那种卡片!”当 supports() 返回 true 而 authenticate() 返回 null 时,会发生这种情况。

应用自定义身份验证逻辑

在本节中,我们将实现自定义身份验证逻辑。您可以在项目yyit-ch5-ex1中找到这个例子。通过这个例子,您可以应用前面中所学到的Authentication 和AuthenticationProvider接口。在清单 3 和 4中,我们逐步构建了一个如何实现自定义AuthenticationProvider的示例。如图 5所示,这些步骤如下:

  • 声明一个实现 AuthenticationProvider 约定的类。
  • 决定新的 AuthenticationProvider 支持哪种身份验证对象:

--重写 supports(Class<?> c) 方法,以指定我们定义的 AuthenticationProvider 支持的身份验证类型。

--重写 authenticate(Authentication a) 方法以实现身份验证逻辑。

  • 用 Spring Security 注册一个新的 AuthenticationProvider 实现的实例。

清单 3 重写 AuthenticationProvider 的 supports() 方法

@Component
public class CustomAuthenticationProvider 
  implements AuthenticationProvider {

  // Omitted code

  @Override
  public boolean supports(Class<?> authenticationType) {
    return authenticationType
            .equals(UsernamePasswordAuthenticationToken.class);
  }
}

在清单 3 中,我们定义了一个实现 AuthenticationProvider 接口的新类。我们用 @Component 注解这个类,让它在 Spring 管理的上下文中有一个该类型的实例。然后,我们必须决定 AuthenticationProvider 支持哪种身份验证接口实现。这取决于我们希望作为参数提供给 authenticate() 方法的类型。如果我们没有在身份验证过滤器级别上定制任何东西(这就是我们的情况,但是我们将在过滤器系列文章中进行定制),那么类 UsernamePasswordAuthenticationToken 将定义类型。该类是 Authentication 接口的实现,表示一个带有用户名和密码的标准身份验证请求。

通过这个定义,我们让 AuthenticationProvider 支持一种特定的密钥。一旦我们指定了 AuthenticationProvider 的范围,我们就通过重写 authenticate() 方法来实现身份验证逻辑,如下面的清单所示。

清单 4 实现身份认证逻辑

@Component
public class CustomAuthenticationProvider 
  implements AuthenticationProvider {

  @Autowired
  private UserDetailsService userDetailsService;

  @Autowired
  private PasswordEncoder passwordEncoder;

  @Override
  public Authentication authenticate(Authentication authentication) {
    String username = authentication.getName();
    String password = authentication.getCredentials().toString();

    UserDetails u = userDetailsService.loadUserByUsername(username);

    if (passwordEncoder.matches(password, u.getPassword())) {
      return new UsernamePasswordAuthenticationToken(
            username, 
            password, 
            u.getAuthorities());
      // 如果密码匹配,则返回带有必要详细信息的 Authentication 约定的实现
    } else {
      throw new BadCredentialsException
                  ("Something went wrong!");
      //如果密码不匹配,则抛出一个类型为 AuthenticationException 的异常。BadCredentialsException 继承自 AuthenticationException。
    }
  }

  // Omitted code
}

清单 4 中的逻辑很简单,图 5 直观地显示了该逻辑。 我们利用 UserDetailsService实现来获取 UserDetails。 如果用户不存在,则 loadUserByUsername() 方法应引发 AuthenticationException。 在这种情况下,身份验证过程将停止,并且 HTTP 过滤器将响应状态设置为 HTTP 401未经授权。 如果用户名存在,我们可以从上下文中使用 PasswordEncoder 的 matchs() 方法进一步检查用户的密码。 如果密码不匹配,则再次抛出 AuthenticationException。 如果密码正确,则 AuthenticationProvider 返回一个 Authentication 实例,标记为 “ authenticated”,其中包含有关请求的详细信息。


由 AuthenticationProvider 实现的自定义身份验证流程。 为了验证身份验证请求,AuthenticationProvider 使用提供的 UserDetailsService 实现加载用户详细信息,如果密码匹配,则使用 PasswordEncoder 验证密码。 如果用户不存在或密码不正确,则 AuthenticationProvider 抛出 AuthenticationException。

要插入 AuthenticationProvider 的新实现,请在项目的配置类中重写 WebSecurityConfigurerAdapter 类的 configure(AuthenticationManagerBuilder auth) 方法。 在下面的清单中对此进行了演示。

清单 5 在配置类中注册 AuthenticationProvider

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private AuthenticationProvider authenticationProvider;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) {
      auth.authenticationProvider(authenticationProvider);
  }

  // Omitted code
}

在清单 5 中,我在声明为 AuthenticationProvider 的字段上使用 @Autowired 注解。Spring 将 AuthenticationProvider 识别为接口(一种抽象)。但是 Spring 知道它需要在其上下文中找到特定接口的实现实例。在我们的例子中,实现是 CustomAuthenticationProvider 的实例,这是我们使用 @Component 注解声明并添加到 Spring 上下文中的唯一这种类型的实例。

就是这样! 您已成功自定义 AuthenticationProvider 的实现。 现在,您可以在需要的地方自定义应用程序的身份验证逻辑

如何在应用程序设计中失败


不正确地应用框架会导致应用程序的可维护性降低。更糟糕的是,有时那些使用框架失败的人认为这是框架的错误。让我给你讲个故事。


有一年冬天,我工作的一家公司的开发主管以顾问的身份打电话给我,让我帮助他们实现一个新功能。他们需要在早期用 Spring 开发的系统组件中应用自定义身份验证方法。不幸的是,在实现应用程序的类设计时,开发人员没有正确地依赖 Spring Security 的主架构。他们仅依靠过滤器链,将Spring Security的全部功能重新实现为自定义代码。


开发人员发现,随着时间的推移,自定义变得越来越困难。但是没有人采取行动重新设计组件,以正确地使用 Spring Security 中的约定。很多困难来自于不知道 Spring 的能力。一个主要的开发人员说,“这只是这个 Spring Security 的错误!这个框架很难应用,而且很难与任何定制一起使用。”我对他的评论感到有点震惊。我知道 Spring Security 有时很难理解,该框架以没有软学习曲线而闻名。但是,我从来没有遇到过无法用 Spring Security 设计易于自定义的类的情况!


我们一起研究了一下,我意识到应用程序开发人员仅使用了 Spring Security 提供的内容的10%。 然后,我举办了为期两天的有关 Spring Security 的研讨会,重点讨论了我们可以为他们需要更改的特定系统组件做什么(以及如何做)。


一切都以完全重写大量自定义代码以正确地依赖 Spring Security 的决定而告终,从而使应用程序更易于扩展,从而满足他们对安全性实现的关注。 我们还发现了一些其他与 Spring Security 不相关的问题,但这是另一回事了。


您可以从这个故事中学到一些教训:

1. 在许多聪明人的参与下,编写了一种框架,尤其是广泛用于应用程序的框架。 即使这样,也很难相信它会被错误地实现。 在断定任何问题都是框架的问题之前,请务必分析您的应用程序。

2. 在决定使用框架时,请确保至少了解其基础知识。

– 注意用于学习框架的资源。 有时,您在网络上找到的文章向您展示了如何快速解决问题,而不一定显示如何正确实现类设计。

– 在你的研究中使用多个来源。为了澄清你的误解,当你不确定如何使用某个东西时,写一个概念证明。

3. 如果决定使用框架,请尽可能将其用于其预期目的。 例如,假设您使用 Spring Security,并且发现对于安全性实现,您倾向于编写更多的自定义代码,而不是依赖于框架提供的内容。 您应该对为什么会发生这样的问题提出疑问。


当我们依靠框架实现的功能时,我们会享受到很多好处。 我们知道它们已经过测试,并且包含漏洞的更改很少。 同样,一个好的框架依赖于抽象,它可以帮助您创建可维护的应用程序。 请记住,在编写自己的实现时,您更容易受到漏洞的影响。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表