Spring Security in Action 第三章 SpringSecurity管理用户
本专栏将从基础开始,循序渐进,以实战为线索,逐步深入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如何扩展这个契约。你将应用这些接口的实现(比如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章中,我们将看看过滤器。
图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 { @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