Spring Security in Action 第三章 SpringSecurity管理用户

本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获,也请大家多多支持。
专栏地址:SpringSecurity专栏
本文涉及的代码都已放在gitee上:gitee地址
如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
专栏汇总:专栏汇总


本章涵盖了

  • 用UserDetails接口描述一个用户
  • 在认证流程中使用UserDetailsService
  • 创建一个自定义的UserDetailsService的实现
  • 创建UserDetailsManager的自定义实现?在认证流程中使用JdbcUserDetailsManager

我的一位大学同事的厨艺很好。他不是高级餐厅的厨师,但他对烹饪相当有热情。有一天,在讨论中分享想法时,我问他如何设法记住这么多食谱。他告诉我,这很容易。“你不必记住整个食谱,但要记住基本成分之间的搭配方式。这就像一些现实世界的合约,告诉你什么可以混合或不应该混合。然后对于每个配方,你只记得一些技巧”。

这个比喻类似于架构的工作方式。对于任何强大的框架,我们都会使用契约来将框架的实现与建立在其上的应用解耦。在Java中,我们使用接口来定义合同。程序员类似于厨师,知道各种成分是如何 "运作 "的,从而选择合适的 “实现”。程序员知道框架的抽象,并使用这些抽象来与之整合。

本章是关于详细了解你在第2章的第一个例子中遇到的基本角色之一–UserDetailsService。与UserDetailsService一起,我们将讨论:

  • UserDetails,它为Spring Security描述用户。
  • GrantedAuthority,它允许我们定义用户可以执行的动作。
  • UserDetailsManager,它扩展了UserDetailsService合约。除了继承的行为,它还描述了创建用户和修改或删除用户密码等动作。

通过第二章,你已经对UserDetailsService和PasswordEncoder在认证过程中的作用有了一个概念。但我们只讨论了如何插入一个由你定义的实例,而不是使用Spring Boot配置的默认实例。我们还有更多细节要讨论:

  • Spring Security提供了哪些实现以及如何使用它们
  • 如何为合同定义一个自定义的实现,以及何时这样做
  • 实现你在现实世界应用中发现的接口的方法
  • 使用这些接口的最佳实践

计划从Spring Security如何理解用户定义开始。为此,我们将讨论UserDetails和GrantedAuthority合约。然后,我们将详细介绍UserDetailsService以及UserDetailsManager如何扩展这个契约。你将应用这些接口的实现(比如InMemoryUserDetailsManager,JdbcUserDetailsManager,以及LdapUserDetailsManager)。当这些实现不适合你的系统时,你会写一个自定义实现。

3.1 在Spring Security中实现认证

在上一章中,我们开始了Spring Security的学习。在第一个例子中,我们讨论了Spring Boot是如何定义一些默认值的,这些默认值定义了一个新的应用程序最初的工作方式。你还学习了如何使用我们经常在应用程序中发现的各种替代方法来覆盖这些默认值。但我们只考虑了这些的表面情况,以便你对我们要做的事情有一个概念。在这一章,以及第四章和第五章中,我们将更详细地讨论这些接口,以及不同的实现和你可能在现实世界的应用中找到它们。

图3.1展示了Spring Security中的认证流程。这个架构是Spring Security实现的认证过程的骨干。了解它真的很重要,因为你将在任何Spring Security的实现中依赖它。你会发现,我们几乎在本书的所有章节中都讨论了这个架构的一部分。你会经常看到它,以至于你可能会把它背下来,这很好。如果你知道这个架构,你就像一个知道自己的成分的厨师,可以把任何食谱放在一起。

在图3.1中,阴影框代表我们开始使用的组件:UserDetailsService和PasswordEncoder。这两个组件集中在流程的一部分,我经常把它称为 “用户管理部分”。在本章中,UserDetailsService和PasswordEncoder是直接处理用户细节和他们的证书的组件。我们将在第四章详细讨论PasswordEncoder。我还将在本书中详细介绍你可以在认证流程中定制的其他组件:在第5章中,我们将看看AuthenticationProvider和SecurityContext,在第9章中,我们将看看过滤器。

image-20230201225235150

图3.1 Spring Security的认证流程。AuthenticationFilter拦截请求并将认证责任委托给AuthenticationManager。为了实现认证逻辑,AuthenticationManager使用一个认证提供者。为了检查用户名和密码,AuthenticationProvider使用UserDetailsService和PasswordEncoder。

作为用户管理的一部分,我们使用UserDetailsService和UserDetailsManager接口。UserDetailsService只负责按用户名检索用户。这个动作是框架完成认证所需要的唯一动作。UserDetailsManager增加了关于添加、修改或删除用户的行为,这在大多数应用程序中都是必需的功能。这两个契约之间的分离是接口隔离原则的一个很好的例子。分离接口可以获得更好的灵活性,因为如果你的应用程序不需要,框架不会强迫你实现行为。如果应用程序只需要验证用户,那么实现UserDetailsService合同就足以涵盖所需的功能。为了管理用户,UserDetailsService和UserDetailsManager组件需要一种方法来表示它们。

Spring Security提供了UserDetails契约,你必须实现它来以框架理解的方式描述用户。正如你在本章中所了解的,在Spring Security中,用户有一组权限,也就是用户被允许做的动作。我们将在第7章和第8章讨论授权问题时,大量使用这些权限。但现在,Spring Security用GrantedAuthority接口表示用户可以做的动作。我们通常称这些权限,一个用户有一个或多个权限。在图3.2中,你可以看到认证流程中的用户管理部分的组件之间的关系表示。

image-20230201225645087

图3.2 参与用户管理的组件之间的依赖关系。UserDetailsService返回一个用户的详细信息,通过其名字找到用户。UserDetails合约描述了用户。一个用户有一个或多个权限,由GrantedAuthority接口表示。为了给用户添加诸如创建、删除或更改密码等操作,UserDetailsManager契约扩展了UserDetailsService来添加操作。

了解Spring Security架构中这些对象之间的联系以及实现它们的方法,可以让你在处理应用程序时有多种选择。这些选项中的任何一个都可能是你正在开发的应用程序中的正确拼图,你需要明智地做出选择。但为了能够选择,你首先需要知道你可以选择什么。

3.2 描述用户

在本节中,你将学习如何描述你的应用程序的用户,以便Spring Security能够理解他们。学习如何表示用户并使框架了解他们是构建认证流程的一个重要步骤。基于用户,应用程序会做出一个决定–对某一功能的调用是否被允许。为了与用户打交道,你首先需要了解如何在你的应用程序中定义用户的原型。在这一节中,我将通过实例描述如何在Spring Security应用程序中为用户建立一个蓝图。

对于Spring Security来说,用户定义应该尊重UserDetails合约。UserDetails合约代表了Spring Security所理解的用户。你的应用程序中描述用户的类必须实现这个接口,通过这种方式,框架可以理解它。

3.2.1 解读UserDetails合同的定义

在本节中,你将学习如何实现UserDetails接口来描述你的应用程序中的用户。我们将讨论UserDetails合约所声明的方法,以了解我们如何以及为什么要实现每一个方法。首先,让我们看看下面列表中介绍的接口。

清单3.1 UserDetails 接口

image-20230201230944571

getUsername()和getPassword()方法返回,正如你所期望的,用户名和密码。应用程序在认证过程中使用这些值,这些是该合同中唯一与认证有关的细节。其他五个方法都与授权用户访问应用程序的资源有关。

一般来说,应用程序应该允许用户做一些在应用程序的上下文中有意义的动作。例如,用户应该能够读取数据、写入数据或删除数据。我们说一个用户有或没有执行某个动作的权限,而一个权限代表一个用户拥有的权限。我们实现getAuthorities()方法来返回授予用户的权限组。

注意 正如你将在第7章中学习的那样,Spring Security使用权限来指代细粒度的权限或角色,后者是权限的组。为了使你的阅读更加轻松,在本书中,我把细粒度的权限称为权限。

此外,正如在UserDetails合同中所看到的,用户可以:

  • 让账户过期
  • 锁定账户
  • 让凭证过期
  • 禁用该帐户

如果你选择在你的应用程序的逻辑中实现这些用户限制,你需要覆盖以下方法:isAccountNonExpired(), isAccountNonLocked(), isCredentialsNonExpired(), isEnabled(),使那些需要启用的方法返回true。并非所有的应用程序都有过期或在某些条件下被锁定的账户。如果你不需要在你的应用程序中实现这些功能,你可以简单地让这四个方法返回真。

注意 UserDetails接口中最后四个方法的名字可能听起来很奇怪。可以说,从简洁的编码和可维护性的角度来看,这些方法的选择是不明智的。例如,isAccountNonExpired()这个名字看起来像一个双重否定,乍一看,可能会产生混淆。但是,要注意分析所有四个方法的名称。这些方法的命名是这样的:在授权失败的情况下,它们都返回false,否则返回true。这是正确的方法,因为人类的思维倾向于将 "假 "字与消极性联系起来,将 "真 "字与积极的情况联系起来。

3.2.2 关于GrantedAuthority合同的详细说明

正如你在第3.2.1节UserDetails接口的定义中所观察到的,授予一个用户的行动被称为权限。在第7章和第8章中,我们将基于这些用户权限来编写授权配置。因此,知道如何定义它们是很有必要的。

授权代表了用户在你的应用程序中可以做什么。没有权限,所有的用户都是平等的。虽然有一些简单的应用程序中的用户是平等的,但在大多数实际情况下,一个应用程序会定义多种类型的用户。一个应用程序可能有只能阅读特定信息的用户,而其他人也可以修改数据。而你需要根据应用的功能需求,使你的应用对他们进行区分,这就是用户需要的权限。为了描述Spring Security中的权限,你可以使用GrantedAuthority接口。

在我们讨论实现UserDetails之前,让我们先了解一下GrantedAuthority接口。我们在定义用户详细信息时使用这个接口。它代表了授予用户的特权。一个用户可以没有任何数量的权限,通常,他们至少有一个。下面是GrantedAuthority定义的实现。

public interface GrantedAuthority extends Serializable {
    
     
    String getAuthority(); 
}

要创建一个权限,你只需要为该权限找到一个名称,这样你就可以在以后编写授权规则时参考它。例如,一个用户可以读取应用程序所管理的记录或删除它们。你可以根据你给这些动作起的名字来编写授权规则。在第7章和第8章,你将学习如何根据用户的权限来编写授权规则。

在本章中,我们将实现getAuthority()方法,以字符串形式返回权限名称。GrantedAuthority接口只有一个抽象方法,在本书中,你经常会发现一些例子,我们使用lambda表达式来实现它。另一种可能性是使用SimpleGranted- Authority类来创建权限实例。

SimpleGrantedAuthority类提供了一种方法来创建GrantedAuthority类型的不可变实例。你在建立实例时提供了权限名称。在接下来的代码片段中,你会发现两个实现GrantedAuthority的例子。在这里,我们利用一个lambda表达式,然后使用SimpleGrantedAuthority类。

注意 在用lambda表达式实现接口之前,用@FunctionalInterface注解验证该接口是否被标记为功能性的,这是一个好的做法。这种做法的原因是,如果接口没有被标记为功能性,就意味着其开发者保留了在未来版本中为其添加更多抽象方法的权利。在Spring Security中,GrantedAuthority接口没有被标记为功能性的。然而,我们将在本书中使用lambda表达式来实现该接口,以使代码更短、更容易阅读,即使这不是我推荐你在真实世界的项目中做的事情。

3.2.3 编写UserDetails的最小实现

在这一节中,你将编写UserDetails合约的第一个实现。我们从一个基本的实现开始,其中每个方法返回一个静态值。然后我们把它改成一个你更有可能在实际场景中找到的版本,一个允许你有多个不同用户实例的版本。现在你知道了如何实现UserDetails和GrantedAuthority接口,我们可以为一个应用程序编写最简单的用户定义。

通过一个名为DummyUser的类,我们来实现列表3.2中对用户的最小描述。我使用这个类主要是为了演示实现UserDetails契约的方法。这个类的实例总是只提到一个用户,“bill”,他有一个密码 "12345 "和一个名为 "READ "的权限。

清单3.2 DummyUser类

public class DummyUser implements UserDetails {
    
    
    @Override
    public String getUsername() {
    
    
        return "bill";
    }
    @Override
    public String getPassword() {
    
    
        return "12345";
    }
// Omitted code
}

列表3.2中的类实现了UserDetails接口,需要实现它的所有方法。你可以在这里找到 getUsername() 和 getPassword() 的实现。在这个例子中,这些方法只为每个属性返回一个固定的值。

接下来,我们为权限列表添加一个定义。清单3.3显示了getAuthorities()方法的实现。这个方法返回一个只有一个GrantedAuthority接口实现的集合。

清单3.3 getAuthorities()方法的实现

public class DummyUser implements UserDetails {
    
    
    // Omitted code
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        return List.of(() -> "READ");
    }
// Omitted code
}

最后,你必须为UserDetails接口的最后四个方法添加一个实现。对于DummyUser类,这些方法总是返回true,这意味着用户永远是活跃的、可用的。你可以在下面的列表中找到这些例子。

清单3.4 最后四个UserDetails接口方法的实现

public class DummyUser implements UserDetails {
    
    
    // Omitted code
    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
    
    
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }
    @Override
    public boolean isEnabled() {
    
    
        return true;
    }
// Omitted code
}

当然,这种最小的实现意味着该类的所有实例都代表同一个用户。这是理解契约的一个良好开端,但不是你在实际应用中会做的事情。对于一个真实的应用,你应该创建一个可以用来生成代表不同用户的实例的类。在这种情况下,你的定义至少要把用户名和密码作为类中的属性,如下面的列表所示。

清单3.5 一个更实用的UserDetails接口的实现

public class SimpleUser implements UserDetails {
    
    
    private final String username;
    private final String password;
    public SimpleUser(String username, String password) {
    
    
        this.username = username;
        this.password = password;
    }
    @Override
    public String getUsername() {
    
    
        return this.username;
    }
    @Override
    public String getPassword() {
    
    
        return this.password;
    }
// Omitted code
}

3.2.4 使用构建器来创建UserDetails类型的实例

有些应用程序很简单,不需要自定义实现User- Details接口。在这一节中,我们看一下如何使用Spring Security提供的构建器类来创建简单的用户实例。你不用在你的应用程序中再声明一个类,而是用User builder类快速获得一个代表你的用户的实例。

org.springframework.security.core.userdetails包中的User类是构建UserDetails类型实例的一种简单方法。使用这个类,你可以创建UserDetails的不可变的实例。你需要至少提供一个用户名和一个密码,而且用户名不应该是一个空字符串。清单3.6演示了如何使用这个构建器。以这种方式构建用户,你不需要有UserDetails契约的实现。

清单3.6 用用户构建器类构建一个用户

UserDetails u = User.withUsername("bill") 
    .password("12345") 
    .authorities("read", "write") 
    .accountExpired(false)
    .disabled(true) .build();

以前面的列表为例,让我们更深入地了解User构建器类的结构。User.withUsername(String username)方法返回一个嵌套在 User 类中的构建器类 UserBuilder 的实例。另一种创建构建器的方法是从另一个 UserDetails 的实例开始。在列表3.7中,第一行构建了一个UserBuilder,从给定的字符串的用户名开始。之后,我们演示了如何从一个已经存在的 UserDetails 实例开始创建一个构建器。

清单3.7 创建User.UserBuilder实例

//用他们的用户名建立一个用户。
User.UserBuilder builder1 = User.withUsername("bill");
      UserDetails u1 = builder1
      .password("12345")
      .authorities("read", "write")
          //密码编码器只是一个做编码的函数。
      .passwordEncoder(p -> encode(p))
      .accountExpired(false)
      .disabled(true)
          //在构建管道的末端,调用build()方法
      .build();
//你也可以从一个现有的UserDetails实例建立一个用户。
User.UserBuilder builder2 = User.withUserDetails(u);
UserDetails u2 = builder2.build();

你可以看到,通过清单 3.7 中定义的任何一个构建器,你可以使用构建器来获得由 UserDetails 合同代表的用户。在构建管道的末端,你调用 build() 方法。如果你提供了密码,它将应用定义的函数对密码进行编码,构建 UserDetails 的实例,并返回它。

注意 密码编码器与我们在第2章讨论的bean不同。这个名字可能让人困惑,但在这里我们只有一个函数<String, String>。这个函数的唯一职责是在给定的编码中转换一个密码字。在下一节中,我们将详细讨论我们在第二章中使用的来自Spring Security的PasswordEncoder合约。

3.2.5 结合与用户有关的多种责任

在上一节中,你学到了如何实现UserDetails接口。在现实世界的场景中,它往往更复杂。在大多数情况下,你会发现一个用户与多个职责相关。而如果你把用户存储在数据库中,然后在应用程序中,你也需要一个类来表示持久化实体。或者,如果你通过网络服务从另一个系统检索用户,那么你可能需要一个数据传输对象来表示用户实例。假设第一种情况很简单,但也很典型,让我们考虑一下,我们在一个SQL数据库中有一个表,我们在其中存储用户。为了让这个例子更简短,我们只给每个用户一个权限。下面的列表显示了映射该表的实体类。

清单3.8 定义JPA用户实体类

@Entity
public class User {
    
    
  @Id
  private Long id;
  private String username;
  private String password;
  private String authority;
// Omitted getters and setters
}

如果你让同一个类也为用户实现Spring Security合约的 细节,这个类就会变得更加复杂。你对下一个列表中的代码有什么看法?看起来如何?从我的角度来看,它是一团糟。我会迷失在其中。

清单3.9 用户类有两个职责

image-20230202205108497

该类包含JPA注释、getters和setters,其中getUsername()和getPassword()都覆盖了UserDetails合同中的方法。它有一个返回字符串的getAuthority()方法,以及一个返回集合的getAuthorities()方法。getAuthority()方法只是类中的一个getter,而getAuthorities()实现了UserDetails接口中的方法。而在添加与其他实体的关系时,事情就变得更加复杂了。再说一遍,这段代码一点也不友好!

我们怎样才能把这段代码写得更干净呢?前面的代码例子的泥泞方面的根源是两个责任的混合。虽然在应用程序中确实需要这两种职责,但在这种情况下,没有人说你必须把它们放在同一个类中。让我们试着通过定义一个单独的名为SecurityUser的类来分离这些职责,该类装饰User类。如清单3.10所示,SecurityUser类实现了UserDetails契约,并使用它将我们的用户插入到Spring的安全架构中。User类只剩下它的JPA实体责任。

清单3.10 将用户类仅作为JPA实体来实现

@Entity
public class User {
    
    
  @Id
  private int id;
  private String username;
  private String password;
  private String authority;
// Omitted getters and setters
}

列表3.10中的User类只剩下了它的JPA实体责任,因此,变得更加可读。如果你阅读这段代码,你现在可以只关注与持久化有关的细节 与持久化相关的细节,从Spring Security的角度来看,这些细节并不重要。在 下一个列表中,我们实现了SecurityUser类来包装用户实体。

清单3.11 SecurityUser类实现了UserDetails合同。

public class SecurityUser implements UserDetails {
    
    
  private final User user;
  public SecurityUser(User user) {
    
    
    this.user = user;
  }
  @Override
  public String getUsername() {
    
    
    return user.getUsername();
  }
  @Override
  public String getPassword() {
    
    
    return user.getPassword();
  }
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
    return List.of(() -> user.getAuthority());
  }
// Omitted code
}

正如你所看到的,我们使用SecurityUser类只是为了将系统中的用户细节映射到Spring Security所理解的UserDetails契约中。为了表明SecurityUser在没有用户实体的情况下是没有意义的,我们把这个字段变成了最终的。你必须通过构造函数来提供用户。SecurityUser类对User实体类进行了分级,并添加了与Spring Security合同相关的所需代码,而没有将代码混入JPA实体,从而实现了多个不同的任务。

注意 你可以找到不同的方法来分离这两项职责。我不想说我在本节中介绍的方法是最好的或唯一的。通常情况下,你选择的实现类设计的方式在不同的情况下有很大的不同。但主要的想法是一样的:避免混合责任,尽量写出解耦的代码,以提高应用程序的可维护性。

3.3 指导Spring Security如何管理用户

在上一节中,你实现了UserDetails契约来描述用户,以便Spring Security能够理解他们。但Spring Security是如何管理用户的呢?在比较凭证时,他们是从哪里来的,以及你如何添加新的用户或改变现有的用户?在第2章中,你了解到框架定义了一个特定的组件,认证过程将用户管理委托给它:UserDetailsService实例。我们甚至定义了一个UserDetailsService来覆盖Spring Boot提供的默认实现。

在这一节中,我们尝试了实现UserDetailsService类的各种方法。通过在我们的例子中实现UserDetailsService合约所描述的责任,你将了解用户管理是如何工作的。之后,你会发现UserDetailsManager接口如何为UserDetailsService定义的契约增加更多的行为。在本节的最后,我们将使用Spring Security提供的UserDetailsManager接口的实现。我们将写一个示例项目,其中我们将使用Spring Security提供的最著名的实现之一–JdbcUserDetailsManager。通过学习,你将知道如何告诉Spring Security在哪里找到用户,这在认证流程中是至关重要的。

3.3.1 了解UserDetailsService合同

在本节中,你将了解UserDetailsService接口的定义。在理解如何以及为什么要实现它之前,你必须首先理解契约。现在是时候详细介绍UserDetailsService以及如何与这个组件的实现一起工作了。UserDetailsService接口只包含一个方法,如下所示。

image-20230202205914317

认证的实现会调用loadUserByUsername(String username)方法来获取具有给定用户名的用户的详细信息(图3.3)。当然,该用户名被认为是唯一的。这个方法返回的用户是UserDetails合约的一个实现。如果该用户名不存在,该方法会抛出一个 UsernameNotFoundException。

image-20230202205955662

图3.3 AuthenticationProvider是实现认证逻辑的组件,它使用UserDetailsService来加载用户的详细信息。为了按用户名查找用户,它调用loadUserByUsername(String username)方法。

注意 UsernameNotFoundException是一个RuntimeException。UserDetailsService接口中的throws子句仅仅是为了记录的目的。UsernameNotFoundException直接继承自AuthenticationException类型,它是所有与认证过程相关的异常的父类。AuthenticationException进一步继承了RuntimeException类。

3.3.2 实现UserDetailsService合同

在本节中,我们通过一个实际的例子来演示UserDetailsService的实现。你的应用程序管理着关于证书和其他用户方面的细节。这些信息可能存储在数据库中,也可能由你通过Web服务或其他方式访问的另一个系统处理(图3.3)。不管这在你的系统中是如何发生的,Spring Security需要你做的唯一一件事就是实现按用户名检索用户。

在下一个例子中,我们写一个UserDetailsService,它有一个内存中的用户列表。在第2章中,你使用了一个提供的实现来做同样的事情,即InMemoryUserDetailsManager。因为你已经熟悉了这个实现的工作方式,所以我选择了一个类似的功能,但这次是由我们自己实现的。当我们创建UserDetails- Service类的实例时,我们提供一个用户列表。你可以在项目sia-ch3-ex1中找到这个例子。在名为model的包中,我们定义了UserDetails,如下表所示。

清单3.12 UserDetails接口的实现

package com.laurentiuspilca.ssia.model;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class User implements UserDetails {
    
    
	//用户类是不可改变的。当你建立实例时,你给出了三个属性的值,而这些值在之后不能被改变。
    private final String username;
    private final String password;
    private final String authority;

    //为了使例子简单,一个用户只有一个权限。
    public User(String username, String password, String authority) {
    
    
        this.username = username;
        this.password = password;
        this.authority = authority;
    }

    //返回一个只包含GrantedAuthority对象的列表,该对象的名称是在你建立实例时提供的。
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        return List.of(() -> authority);
    }

    @Override
    public String getPassword() {
    
    
        return password;
    }

    @Override
    public String getUsername() {
    
    
        return username;
    }

    //该账户不会过期或被锁定。
    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
    
    
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }

    @Override
    public boolean isEnabled() {
    
    
        return true;
    }
}

在名为services的包中,我们创建了一个名为InMemoryUserDetailsService的类。下面的列表显示了我们如何实现这个类。

清单3.13 UserDetailsService接口的实现

package com.laurentiuspilca.ssia.services;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.List;

public class InMemoryUserDetailsService implements UserDetailsService {
    
    

    //UserDetailsService在内存中管理用户的列表。
    private final List<UserDetails> users;

    public InMemoryUserDetailsService(List<UserDetails> users) {
    
    
        this.users = users;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        return users.stream()
            //从用户列表中,筛选出具有所要求的用户名的用户
                .filter(u -> u.getUsername().equals(username))
            //如果有这样一个用户,则将其返回
                .findFirst()
            //如果一个具有此用户名的用户不存在,则抛出一个异常。
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }
}

loadUserByUsername(String username)方法在用户列表中搜索给定的用户名并返回想要的UserDetails实例。如果没有该用户名的实例,它会抛出一个UsernameNotFoundException。我们现在可以使用这个实现作为我们的UserDetailsService。下一个列表显示了我们如何在配置类中把它作为一个Bean添加,并在其中注册一个用户。

清单3.14 UserDetailsService在配置类中被注册为一个Bean。

package com.laurentiuspilca.ssia.config;

import com.laurentiuspilca.ssia.model.User;
import com.laurentiuspilca.ssia.services.InMemoryUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.List;

@Configuration
public class ProjectConfig {
    
    

    @Bean
    public UserDetailsService userDetailsService() {
    
    
        UserDetails u = new User("john", "12345", "read");
        List<UserDetails> users = List.of(u);
        return new InMemoryUserDetailsService(users);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }
}

最后,我们创建一个简单的端点并测试其实现。下面的列表定义了这个端点。

清单3.15 用于测试实现的端点的定义

package com.laurentiuspilca.ssia.controllers;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    
    

    @GetMapping("/hello")
    public String hello() {
    
    
        return "Hello";
    }
}

当使用cURL调用端点时,我们观察到,对于密码为12345的用户John,我们得到的是HTTP 200 OK。如果我们使用其他东西,应用程序会返回401未授权。

curl -u john:12345 http://localhost:8080/hello

3.3.3 实现UserDetailsManager合同

在这一节中,我们讨论使用和实现UserDetailsManager接口。这个接口扩展了UserDetailsService合约,并为其增加了更多方法。Spring Security需要UserDetailsService合约来进行授权。但一般来说,在应用程序中,也需要对用户进行管理。大多数时候,一个应用程序应该能够添加新的用户或删除现有的用户。在这种情况下,我们实现了一个由Spring Security定义的更特殊的接口,即UserDetailsManager。它扩展了UserDetailsService,增加了更多我们需要实现的操作。

public interface UserDetailsManager extends UserDetailsService {
    
    
    void createUser(UserDetails user);
    void updateUser(UserDetails user);
    void deleteUser(String username);
    void changePassword(String oldPassword, String newPassword);
    boolean userExists(String username);
}

我们在第二章中使用的InMemoryUserDetailsManager对象实际上是一个UserDetailsManager。当时,我们只考虑到它的UserDetailsService特性,但现在你更明白为什么我们能够在实例上调用createUser()方法。

3.3.4 使用jdbcuserdetailsmanager进行用户管理

在InMemoryUserDetailsManager之外,我们经常使用另一个UserDetailManager,即JdbcUserDetailsManager。JdbcUserDetailsManager在SQL数据库中管理用户。它通过JDBC直接连接到数据库。这样,JdbcUserDetailsManager独立于任何其他与数据库连接有关的框架或规范。

为了理解JdbcUserDetailsManager是如何工作的,最好是通过一个例子将其付诸实施。在下面的例子中,你将实现一个应用程序,使用JdbcUserDetailsManager来管理MySQL数据库中的用户。图3.4概述了JdbcUserDetailsManager的实现在认证流程中的位置。

你将通过创建一个数据库和两个表来开始我们关于如何使用JdbcUserDetailsManager的演示应用程序。在我们的例子中,我们将数据库命名为spring,并将其中一个表命名为users,另一个命名为authorities。这些名字是JdbcUserDetailsManager所知道的默认表名。正如你将在本节末尾学到的,JdbcUserDetailsManager的实现很灵活,如果你想的话,可以覆盖这些默认的名字。users表的目的是为了保存用户记录。JdbcUserDetails Manager的实现希望在用户表中有三列:一个用户名、一个密码和启用,你可以用它来停用用户。

image-20230202225613252

图3.4 Spring Security的认证流程。这里我们使用一个JDBCUserDetailsManager作为我们的UserDetailsService组件。JdbcUserDetailsManager使用一个数据库来管理用户。

你可以选择使用你的数据库管理系统(DBMS)的命令行工具或客户端应用程序来自己创建数据库及其结构。例如,对于MySQL,你可以选择使用MySQL Workbench来做这件事。但最简单的是让Spring Boot自己为你运行脚本。要做到这一点,只需在项目的资源文件夹中再添加两个文件:schema.sql 和 data.sql。在schema.sql文件中,你可以添加与数据库结构有关的查询,如创建、更改或删除表。在data.sql文件中,你添加与表内数据有关的查询,如INSERT、UPDATE或DELETE。当你启动应用程序时,Spring Boot会自动为你运行这些文件。对于构建需要数据库的例子,一个更简单的解决方案是使用H2内存数据库。这样,你就不需要安装一个单独的DBMS解决方案。

注意 如果你愿意,在开发本书所介绍的应用程序时,你也可以用H2。我选择用一个外部DBMS来实现这些例子,以明确它是系统的一个外部组件,这样可以避免混淆。

你使用下一个列表中的代码,用MySQL服务器创建用户表。你可以把这个脚本添加到Spring Boot项目的schema.sql文件中。

清单3.16 创建用户表的SQL查询

CREATE TABLE IF NOT EXISTS `spring`.`users` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL,
`password` VARCHAR(45) NOT NULL,
`enabled` INT NOT NULL,
PRIMARY KEY (`id`));

权限表存储每个用户的权限。每条记录存储一个用户名和为该用户名的用户授予的权限。

清单3.17 创建权限表的SQL查询

CREATE TABLE IF NOT EXISTS `spring`.`authorities` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(45) NOT NULL,
`authority` VARCHAR(45) NOT NULL,
PRIMARY KEY (`id`));

注意 为了简单起见,在本书提供的例子中,我跳过了对索引或外键的定义。

为了确保你有一个用于测试的用户,在每个表中插入一条记录。你可以在Spring Boot项目的资源文件夹中的data.sql文件中添加这些查询:

INSERT IGNORE INTO `spring`.`authorities` VALUES (NULL, 'john', 'write'); 
INSERT IGNORE INTO `spring`.`users` VALUES (NULL, 'john', '12345', '1');

对于你的项目,你需要至少添加以下列表中的依赖项。检查你的pom.xml文件,确保你添加了这些依赖项。

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>


        

注意 在你的例子中,你可以使用任何SQL数据库技术,只要你把正确的JDBC驱动添加到依赖关系中。

你可以在项目的application.properties文件中配置数据源,或者作为一个单独的Bean。如果你选择使用application.properties文件,你需要在该文件中添加以下几行。

spring.datasource.url=jdbc:mysql://localhost/spring
spring.datasource.username=<your user>
spring.datasource.password=<your password>

在项目的配置类中,你定义了UserDetailsService和 PasswordEncoder。JdbcUserDetailsManager需要DataSource来连接数据库。连接到数据库。数据源可以通过方法的一个参数自动连接。方法的一个参数(如下面的列表所示)或通过类的一个属性来自动连接数据源。

清单3.19 在配置类中注册JdbcUserDetailsManager

package com.laurentiuspilca.ssia.config;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

  @Bean
  public UserDetailsService userDetailsService(DataSource dataSource) {
    
    
   	return new JdbcUserDetailsManager(dataSource);

  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    
    
    return NoOpPasswordEncoder.getInstance();
  }

}

要访问应用程序的任何端点,你现在需要使用HTTP Basic认证,并使用存储在数据库中的一个用户。为了证明这一点,我们创建一个新的端点,如下表所示,然后用cURL调用它。

package com.laurentiuspilca.ssia.controllers;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    
    

    @GetMapping("/hello")
    public String hello() {
    
    
        return "Hello!";
    }
}

在下一个代码片断中,你会发现用正确的用户名和密码调用端点时的结果。

curl -u john:12345 http://localhost:8080/hello
Hello!

JdbcUserDetailsManager还允许你配置使用的查询。在前面的例子中,我们确保为表和列使用了准确的名称,因为JdbcUserDetailsManager的实现期待这些名称。但是对于你的应用程序来说,这些名字可能不是最好的选择。接下来的列表显示了如何覆盖JdbcUserDetailsManager的查询。

清单3.21 改变JdbcUserDetailsManager的查询以找到用户

package com.laurentiuspilca.ssia.config;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

  @Bean
  public UserDetailsService userDetailsService(DataSource dataSource) {
    
    
    String usersByUsernameQuery = "select username, password, enabled from spring.users where username = ?";
    String authsByUserQuery = "select username, authority from spring.authorities where username = ?";
    var userDetailsManager = new JdbcUserDetailsManager(dataSource);
    userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
    userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
    return userDetailsManager;

  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    
    
    return NoOpPasswordEncoder.getInstance();
  }

}

以同样的方式,我们可以改变JdbcUserDetailsManager实现所使用的所有查询。

练习。编写一个类似的应用程序,在数据库中以不同的方式命名表和列。覆盖JdbcUserDetailsManager实现的查询

使用ldapuserdetailsmanager进行用户管理
Spring Security还为LDAP提供了一个UserDetailsManager的实现。尽管它没有JdbcUserDetailsManager那么流行,但如果你需要与LDAP系统集成进行用户管理,你可以信赖它。在项目sia- ch3-ex3中,你可以找到一个使用LdapUserDetailsManager的简单演示。因为我不能在这个演示中使用真正的LDAP服务器,我在我的Spring Boot应用程序中设置了一个嵌入式的LDAP服务器。为了设置嵌入式LDAP服务器,我定义了一个简单的LDAP数据交换格式(LDIF)文件。下面的列表显示了我的LDIF文件的内容。

#定义了基础实体
dn: dc=springframework,dc=org
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: springframework

#定义了一个group实体
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

#定义一个用户
dn: uid=john,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: John
sn: John
uid: john
userPassword: 12345

在LDIF文件中,我只添加了一个用户,在这个例子的最后,我们需要测试应用程序的行为。我们可以直接将LDIF文件添加到资源文件夹中。这样,它就自动在classpath中,所以我们以后可以很容易地引用它。我把这个LDIF文件命名为server.ldif。为了与LDAP一起工作,并允许Spring Boot启动嵌入式LDAP服务器,你需要将pom.xml添加到依赖项中,如下面的代码片段。

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-ldap</artifactId>
</dependency>

<dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
</dependency>

在application.properties文件中,你还需要添加嵌入式LDAP服务器的配置,如下图代码片段所示。应用程序需要启动嵌入式LDAP服务器的值包括LDIF文件的位置、LDAP服务器的端口和基础域组件(DN)标签值。

spring.ldap.embedded.ldif=classpath:server.ldif
spring.ldap.embedded.base-dn=dc=springframework,dc=org
spring.ldap.embedded.port=33389

一旦你有一个用于认证的LDAP服务器,你就可以配置你的应用程序来使用它。下一个列表显示了如何配置LdapUserDetailsManager,使你的应用程序能够通过LDAP服务器认证用户。

清单3.23 配置文件中LdapUserDetailsManager的定义

package com.laurentiuspilca.ssia.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.ldap.DefaultLdapUsernameToDnMapper;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.security.ldap.userdetails.LdapUserDetailsManager;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
    
    

    //在Spring上下文中添加一个UserDetailsService实现。
    @Override
    @Bean
    public UserDetailsService userDetailsService() {
    
    
        //创建一个上下文源,指定LDAP服务器的地址。
        var cs = new DefaultSpringSecurityContextSource("ldap://127.0.0.1:33389/dc=springframework,dc=org");
        cs.afterPropertiesSet();

        //创建LdapUserDetailsManager实例。
        LdapUserDetailsManager manager = new LdapUserDetailsManager(cs);
        
        //设置一个用户名映射器,指示LdapUserDetailsManager如何搜索用户。
        manager.setUsernameMapper(
                new DefaultLdapUsernameToDnMapper("ou=groups", "uid"));
        manager.setGroupSearchBase("ou=groups");
        return manager;
    }

    //设置应用程序需要搜索用户的群体搜索基础
    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        return NoOpPasswordEncoder.getInstance();
    }

}

让我们也创建一个简单的端点来测试安全配置。我添加了一个controller类,如下面的代码片断中所示。

package com.laurentiuspilca.ssia.controllers;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    
    

    @GetMapping("/hello")
    public String hello() {
    
    
        return "Hello!";
    }
}

现在启动应用程序并调用/hello端点。如果你想让应用程序允许你调用端点,你需要用用户John进行认证。接下来的代码片段向你展示了用cURL调用端点的结果。

curl -u john:12345 http://localhost:8080/hello
Hello!

总结

  • UserDetails接口是你用来描述Spring Security中用户的契约。
  • UserDetailsService接口是Spring Security期望你在认证架构中实现的契约,以描述应用程序获取用户详细信息的方式。
  • UserDetailsManager接口扩展了UserDetailsService,并增加了与创建、改变或删除用户有关的行为。
  • Spring Security提供了UserDetailsManager合约的一些实现。其中包括InMemoryUserDetailsManager、JdbcUserDetailsManager和LdapUserDetailsManager。
  • JdbcUserDetailsManager的优点是直接使用JDBC,不会将应用程序锁定在其他框架中。

猜你喜欢

转载自blog.csdn.net/Learning_xzj/article/details/128998522