编辑推荐: |
此文主要定义了用户管理微服务的要求,并设计了它的初始领域模型以及详细介绍如何实现领域模型,在代码之外做了哪些决定。
本文来自于DaoCloud,由火龙果软件Anna编辑、推荐。 |
|
定义领域模型和 REST API
我们会定义应用的需求,初始的领域模型和供前端使用的 REST API。
我们首先定义用户注册和管理用户的故事。
用户故事
在设计新系统时,值得考虑的是用户希望实现的结果。 下面您可以找到用户注册系统应具有的基本功能的列表。
作为用户,我想注册,以便我可以访问需要注册的内容
作为用户,我想在注册后确认我的电子邮件地址
作为用户,我想登录并注销
作为用户,我想更改我的密码
作为用户,我想更改我的电子邮件地址
作为用户,我想要重置我的密码,以便我忘记密码后可以登录
作为用户,我想更新我的个人资料,以便我可以提供我正确的联络资料
作为用户,我想关闭我的帐户,以便我可以关闭我与我注册的服务的关系
作为管理员,我想手动管理(创建/删除/更新)用户,以便工作人员不必重新进行注册过程
作为管理员,我想手动创建用户,这样工作人员就不用再过注册过程了
作为管理员,我想列出所有用户,即使是那些曾经关闭帐户的用户
作为管理员,我希望能够看到用户的活动(登录,注销,密码重置,确认,个人资料更新),以便我可以遵守外部审计要求
工作流程
我们来看看系统将要支持什么样的工作流程。首先,人们应该能够注册和登录,这些是相当明显的功能。
但是,处理确认令牌时需要谨慎。 由于它们可用于执行特权操作,因此我们使用一次性随机令牌来处理密码重置和电子邮件确认。
当一个新的令牌由用户生成,无论什么原因,所有以前的都是无效的。 当有人记住他们的密码时,以前发出的和有效的密码重置令牌必须过期。
非功能性需求
用户故事通常不会定义非功能性要求,例如安全性,开发原理,技术栈等。所以我们在这里单独列出。
领域模型是使用域驱动的设计原则在纯 Java 中实现的,并且独立于要使用的底层技术栈
当用户登录时,将为他们生成一个 JWT 令牌,有效期是 24 小时。在后续请求中包含此令牌,用户可以执行需要身份验证的操作
密码重置令牌有效期为 10 分钟,电子邮件地址确认令牌为一天
密码用加密算法(Bcrypt)加密,并且每用户加盐
提供了 RESTful API,用于与用户注册服务进行交互
应用程序将具有模块化设计,以便能够为各种场景提供单独的部署工件(例如,针对 Google App Engine
的 2.5 servlet 兼容 WAR 和其他用例的基于 Spring Boot 的自包含可执行
JAR)
实体标识符以数据库无关的方式生成,也就是说,不会使用数据库特定机制(AUTO_INCREMENT 或序列)来获取下一个
ID 值。解决方案将类似于 Instagram genetes ID。
领域模型
对于第一轮实现中,我们只关注三个实体,即用户,确认令牌和用户事件。
rest api
访问下面的大多数 API 都需要认证,否则返回一个 UNAUTHORIZED 状态码。 如果用户尝试查询属于某个其他用户的实体,则他们还会返回客户端错误(FORBIDDEN),除非他具有管理权限。
如果指定的实体不存在,则调用的端点返回 NOT_FOUND。
创建会话(POST /sessions)和注册新用户(POST / users)是公开的,它们不需要身份验证。
Session management
GET /session/{session_id} |
如果没有给定 ID 的会话或者会话已经过期,则返回给定会话的详细信息或
NOT_FOUND。
创建新会话,前提是指定的电子邮件和密码对属于一个有效的用户。
DELETE /session/{session_id} |
删除给定的会话(注销)。
User management
根据一个指定的 ID 查找用户。
列举系统中所有的用户。
注册一个新的用户。
删除指定的用户。
更新指定用户的个人信息。
PUT /users/{user_id}/tokens/{token_id} |
使用给定用户的令牌执行与令牌类型相关的操作。
实现领域模型
使用领域驱动设计
在上边,作者提到了将使用领域驱动设计原则,这意味着,该模型可以不依赖于任何框架或基础设施类。在多次应用实现过程中,作者把领域模型和框架的具体注释(如
JPA 或 Hibernate )混在一起,就如同和 Java POJO 一起工作(贫血模型)。在设计领域模型中,唯一使用的库是Lombok,用于减少定义的
getter 和 setter 方法以避免冗余。
当设计 DDD 的模型,第一步是对类进行分类。在埃里克·埃文斯书中的第二部分专注于模型驱动设计的构建模块。考虑到这一点,我们的模型分为以下几类。
实体类
实体有明确的标识和生命周期需要被管理。从这个角度来看,用户肯定是一个实体。
ConfirmationToken 就是一个边缘的例子,因为在没有用户上下文的情况下,逻辑上它就不存在,而另一方面,它可以通过令牌的值来标识并且它有自己的生命周期。
同样的方法也适用于 Session ,这也可能是一个值对象,由于其不可改变的性质,但它仍然有一个
ID 和一个生命周期(会话过期)。
值对象
相对于实体类,值对象没有一个明确的 ID ,那就是,他们只是将一系列属性组合,并且,如果这些属性和另外一个相同类型的值对象的属性相同,那么我们就可以认为这两个值对象是相同的。
当设计领域模型,值对象提供了一种方便的方式来描述携带有一定的信息片段属性的集合。 AddressData,AuditData,ContactData
和 Password 因此可以认为是值对象。
虽然将所有这些属性实现为不可改变的是不切实际的,他们的某些属性可以单独被修改, Password
是一个很好的例子。当我们创建 Password 的实例,它的盐和哈希创建只有一次。在改变密码时,一个全新的实例与新的盐和散列将会被创建。
聚合
聚合代表一组结合在一起,并通过访问所谓的聚合根的对象。
这儿有两个聚合对象:用户和会话。前者包含了所有与用户相关的实体和值对象,而后者只包含一个单一的实体
Session 。
显然,用户聚合根是用户实体。通过一个实例用户实体,我们可以管理确认令牌,用户事件和用户的密码。
聚合 Session 成为一个独立的实体——尽管被捆绑到一个用户的上下文——部分原因是由于其一次性性质,部分是因为当我们查找一个会话时我们不知道用户是谁。
Session 被创建之后,要么过期,要么按需删除。
领域事件
当需要由系统的另外组件处理的事件发生时,领域事件就会被触发。
用户管理应用程序有一个领域事件,这是 UserEvent ,它有以下类型:
DELETED
EMAIL_CHANGED
EMAIL_CHANGE_REQUESTED
EMAIL_CONFIRMED
PASSWORD_CHANGED
PASSWORD_RESET_CONFIRMED
PASSWORD_RESET_REQUESTED
SCREEN_NAME_CHANGED
SIGNIN_SUCCEEDED
SIGNIN_FAILED
SIGNUP_REQUESTED
服务
服务包含了能够操作一组领域模型的类的业务逻辑。在本应用中, UserService 管理用户的生命周期,并发出合适的
UserEvent 。SessionService 是用于创建和销毁用户会话。
存储库
存储库旨在代表一个实体对象的概念集合,但是有时他们只是作为数据访问对象。有两种实现方法,一种方法是列出所有的抽象存储库类或超接口可能的数据访问方法,例如
Spring Data ,或者创建专门存储库接口。
对于用户管理应用程序,作者选择了第二种方法。UserRepository 和 SessionRepository
只列出那些绝对必要的处理他们实体的方法。
项目结构
你可能已经注意到,这里有一个 GitHub 上的库: springuni ,它包含用户管理应用程序的一部分,但它不包含应用程序本身的可执行版本。
究其原因,我为什么不提供单一只包含 Spring Boot 少量 @Enable* 注解的库,是为了可重用性。大多数我碰到的项目第一眼看起来是可以模块化的,但实际上他们只是没有良好分解职责的巨大单体应用。当你试图重用这样一个项目的模块,你很快意识到,它依赖于许多其他模块和/或过多的外部库。
springuni-particles (它可能已被也称为 springuni 模块)提供了多个模块的可重复使用的只为某些明确定义的功能。用户和会话管理是很好的例子。
模块
springuni-auth-model 包含了所有的领域模型类和用于管理用户生命周期的业务逻辑,它是完全与框架无关的。它的存储库,并且可以使用任何数据存储机制,对于手头的实际任务最符合。还有,PasswordChecker
和 PasswordEncryptor 可基于任何强大的密码散列技术实现。
springuni-commons 包含了通用的工具库。有很多著名的第三方库(如 Apache Commons
Lang,Guava 等),这外延了 JDK 的标准库。在另一方面,我发现自己很多时候仅仅只用这些非常可扩展库的少量类。我特别喜欢的
Apache Commons Lang 中的 StringUtils 的和 Apache 共同集合的
CollectionUtils 类,但是,我宁愿为当前项目提供一个高度定制化的 StringUtils
和 CollectionUtils,这样就不需要添加外部依赖。
sprinuni-crm-model 定义了通用的值对象,用于处理联系人数据,如地址,国家等。虽然微服务架构的倡导者将投票反对使用共享库,但我认为这个特定点可能需要不时修订手头的任务。我最近参与了一些
CRM 集成项目,不得不重新实现了几乎同样的领域模型在不同的限界上下文(即用户,客户,联系人),这样一遍又一遍的操作是乏味的。也就是说,我认为使用联系人数据领域模型的小型通用库是值得尝试的。 |