做网站自己申请域名还是对方,网站建设需要的客户资料,门户网站建设要点,品牌建设综述本专栏将从基础开始#xff0c;循序渐进#xff0c;以实战为线索#xff0c;逐步深入SpringSecurity相关知识相关知识#xff0c;打造完整的SpringSecurity学习步骤#xff0c;提升工程化编码能力和思维能力#xff0c;写出高质量代码。希望大家都能够从中有所收获#… 本专栏将从基础开始循序渐进以实战为线索逐步深入SpringSecurity相关知识相关知识打造完整的SpringSecurity学习步骤提升工程化编码能力和思维能力写出高质量代码。希望大家都能够从中有所收获也请大家多多支持。 专栏地址:SpringSecurity专栏 本文涉及的代码都已放在gitee上gitee地址 如果文章知识点有错误的地方请指正大家一起学习一起进步。 专栏汇总专栏汇总 文章目录3.1 在Spring Security中实现认证3.2 描述用户3.2.1 解读UserDetails合同的定义3.2.2 关于GrantedAuthority合同的详细说明3.2.3 编写UserDetails的最小实现3.2.4 使用构建器来创建UserDetails类型的实例3.2.5 结合与用户有关的多种责任3.3 指导Spring Security如何管理用户3.3.1 了解UserDetailsService合同3.3.2 实现UserDetailsService合同3.3.3 实现UserDetailsManager合同3.3.4 使用jdbcuserdetailsmanager进行用户管理本章涵盖了用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如何扩展这个契约。你将应用这些接口的实现比如InMemoryUserDetailsManagerJdbcUserDetailsManager以及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章中我们将看看过滤器。 图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中你可以看到认证流程中的用户管理部分的组件之间的关系表示。 图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 接口 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 {Overridepublic String getUsername() {return bill;}Overridepublic String getPassword() {return 12345;}
// Omitted code
}列表3.2中的类实现了UserDetails接口需要实现它的所有方法。你可以在这里找到 getUsername() 和 getPassword() 的实现。在这个例子中这些方法只为每个属性返回一个固定的值。
接下来我们为权限列表添加一个定义。清单3.3显示了getAuthorities()方法的实现。这个方法返回一个只有一个GrantedAuthority接口实现的集合。
清单3.3 getAuthorities()方法的实现
public class DummyUser implements UserDetails {// Omitted codeOverridepublic Collection? extends GrantedAuthority getAuthorities() {return List.of(() - READ);}
// Omitted code
}最后你必须为UserDetails接口的最后四个方法添加一个实现。对于DummyUser类这些方法总是返回true这意味着用户永远是活跃的、可用的。你可以在下面的列表中找到这些例子。
清单3.4 最后四个UserDetails接口方法的实现
public class DummyUser implements UserDetails {// Omitted codeOverridepublic boolean isAccountNonExpired() {return true;}Overridepublic boolean isAccountNonLocked() {return true;}Overridepublic boolean isCredentialsNonExpired() {return true;}Overridepublic 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;}Overridepublic String getUsername() {return this.username;}Overridepublic 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 {Idprivate Long id;private String username;private String password;private String authority;
// Omitted getters and setters
}如果你让同一个类也为用户实现Spring Security合约的 细节这个类就会变得更加复杂。你对下一个列表中的代码有什么看法看起来如何从我的角度来看它是一团糟。我会迷失在其中。
清单3.9 用户类有两个职责 该类包含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 {Idprivate 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;}Overridepublic String getUsername() {return user.getUsername();}Overridepublic String getPassword() {return user.getPassword();}Overridepublic 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接口只包含一个方法如下所示。 认证的实现会调用loadUserByUsername(String username)方法来获取具有给定用户名的用户的详细信息图3.3。当然该用户名被认为是唯一的。这个方法返回的用户是UserDetails合约的一个实现。如果该用户名不存在该方法会抛出一个 UsernameNotFoundException。 图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对象的列表该对象的名称是在你建立实例时提供的。Overridepublic Collection? extends GrantedAuthority getAuthorities() {return List.of(() - authority);}Overridepublic String getPassword() {return password;}Overridepublic String getUsername() {return username;}//该账户不会过期或被锁定。Overridepublic boolean isAccountNonExpired() {return true;}Overridepublic boolean isAccountNonLocked() {return true;}Overridepublic boolean isCredentialsNonExpired() {return true;}Overridepublic 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 ListUserDetails users;public InMemoryUserDetailsService(ListUserDetails users) {this.users users;}Overridepublic 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 {Beanpublic UserDetailsService userDetailsService() {UserDetails u new User(john, 12345, read);ListUserDetails users List.of(u);return new InMemoryUserDetailsService(users);}Beanpublic 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的实现希望在用户表中有三列一个用户名、一个密码和启用你可以用它来停用用户。 图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文件确保你添加了这些依赖项。
dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-jdbc/artifactId/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdscoperuntime/scope/dependency 注意 在你的例子中你可以使用任何SQL数据库技术只要你把正确的JDBC驱动添加到依赖关系中。 你可以在项目的application.properties文件中配置数据源或者作为一个单独的Bean。如果你选择使用application.properties文件你需要在该文件中添加以下几行。
spring.datasource.urljdbc:mysql://localhost/spring
spring.datasource.usernameyour user
spring.datasource.passwordyour 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 {Beanpublic UserDetailsService userDetailsService(DataSource dataSource) {return new JdbcUserDetailsManager(dataSource);}Beanpublic 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 {Beanpublic 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;}Beanpublic 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: dcspringframework,dcorg
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: springframework#定义了一个group实体
dn: ougroups,dcspringframework,dcorg
objectclass: top
objectclass: organizationalUnit
ou: groups#定义一个用户
dn: uidjohn,ougroups,dcspringframework,dcorg
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添加到依赖项中如下面的代码片段。
dependencygroupIdorg.springframework.security/groupIdartifactIdspring-security-ldap/artifactId
/dependencydependencygroupIdcom.unboundid/groupIdartifactIdunboundid-ldapsdk/artifactId
/dependency在application.properties文件中你还需要添加嵌入式LDAP服务器的配置如下图代码片段所示。应用程序需要启动嵌入式LDAP服务器的值包括LDIF文件的位置、LDAP服务器的端口和基础域组件DN标签值。
spring.ldap.embedded.ldifclasspath:server.ldif
spring.ldap.embedded.base-dndcspringframework,dcorg
spring.ldap.embedded.port33389一旦你有一个用于认证的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实现。OverrideBeanpublic UserDetailsService userDetailsService() {//创建一个上下文源指定LDAP服务器的地址。var cs new DefaultSpringSecurityContextSource(ldap://127.0.0.1:33389/dcspringframework,dcorg);cs.afterPropertiesSet();//创建LdapUserDetailsManager实例。LdapUserDetailsManager manager new LdapUserDetailsManager(cs);//设置一个用户名映射器指示LdapUserDetailsManager如何搜索用户。manager.setUsernameMapper(new DefaultLdapUsernameToDnMapper(ougroups, uid));manager.setGroupSearchBase(ougroups);return manager;}//设置应用程序需要搜索用户的群体搜索基础Beanpublic 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不会将应用程序锁定在其他框架中。