认证(Authentication)和授权(Authorization)是一个应用中常见的场景。但在DDD中实现认证/授权的最佳方式是什么?
在下面我将基于一个虚拟的用例来展示两种实现方式。
基本要求
这些概念最好了解一下:
用例
假设我们想开发一个博客平台。
我们在下面定义了博客平台上下文(Context)中的通用语言(Ubiquitous Languate):
- Author: 博客平台中的用户,它是一个带有
authorId
唯一标识的Entity。
- Blog: 博客平台中含有博文的特定区域,它是一个带有
blogId
唯一标识的Entity。
- Collaborator: 可以在给定的Blog中创建博文的Author。
- Authentication: 识别一个Author的过程。
- Authorization: 检查在给定的Blog中Author是否是Collaborator。
我们将要开发的用例是Blog的Collaborator可以在Blog中创建博文。为了能够比较两种实现方式,该用例可以在Web、REST和Console中执行。
注意:为了把注意力放在重要概念上,示例代码中对dependencies、getters等概念已经简化了。
版本1:在Application Srvice外进行
这种方式下,一旦发起对Application Service
的请求,则它将被执行,且没有任何Authentication和Authorization的过程。
Application Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| class AddPostRequest() { public function authorId() {…} public function blogId() {…} public function title() {…} public function text() {…} }
class AddPostService() { public function execute($request) { $authorId = $request->authorId(); $blogId = $request->blogId(); $title = $request->title(); $text = $request->text(); if (!$blog = $this->blogRepository->blogOfId($blogId)) { throw new BlogNotFoundException(); } $post = $blog->buildPost( $this->postRepository->nextIdentity(), $authorId, $title, $text ); $this->postRepository->save($post); } }
|
Web
在控制器中(或框架的其他hook中),用例执行之前,对当前用户进行认证检查,同时也检查其是否是Blog的collaborator。
为了检查这些,我们在Controller中使用Domain Service
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class PostController { public newPostAction() { if (!$this->sessionService()->isAuthenticated()) { throw new UnauthenticatedUserException(); } $blogId = $this->request->get(‘blog_id’); $userId = $this->sessionService()->currentUserId(); $isCollaborator = $this->permissionService()->isCollaborator( $userId, $blogId ); if (!$isCollaborator) { throw new UnauthorizedUserException(); } } }
|
REST
REST方式中,会使用header中的Authorization获取token,需要检查这个token是否属于某个用户,并且这用户是否是Blog的collaborator。
为了检查这些,我们在Controller中使用Domain Service
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class PostRestFulController { public newPostAction() { $authorizationValue = $this->authorizationHeader(); $userId = $this->oauthService()->userId($authorizationValue); $blogId = $this->request->get(‘blog_id’); if (!$userId) { throw new UnauthenticatedUserException(); } $isCollaborator = $this->permissionService()->isCollaborator( $userId, $blogId ); if (!$isCollaborator) { throw new UnauthorizedUserException(); } } }
|
Console
Console下,执行用例没有检查authentication和authorization。userId
来自命令行的参数。
1 2 3 4 5 6 7 8 9 10
| > blog add-post — user-id 1 — blog-id 1 — title Foo …
class PostConsole { public newPostCommand() { } }
|
总结
- Authentication/Authorization检查是在Application Service之外的Controller或框架的其他hook中进行。
- Authentication/Authorization必须在Application Service用例执行前进行。
- 根据应用的入口(如Web、REST、Console)来使用authentication服务,实例中使用的是OauthService、SessionService。
版本2:在Application Service内进行
我们可以在用例内使用通用语言,来隐式的执行authentication/authorization。
Application Service
它将使用一个名为CollaboratorService的Domain Service来检查authentication和authorization。CollaboratorService返回Author的值对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| interface CollaboratorService { public function authorFrom($authorId, $blogId); }
class AddPostRequest { public function authorId() {…} public function blogId() {…} public function title() {…} public function text() {…} }
class AddPostService { public function execute($request) { $authorId = $request->authorId(); $blogId = $request->blogId(); $title = $request->title(); $text = $request->text(); $author = $this->collaboratorService->authorFrom( $authorId, $blogId ); if (!$author) { throw new InvalidAuthorException(); } if (!$blog = $this->blogRepository->blogOfId($blogId)) { throw new BlogNotFoundException(); } $post = $blog->buildPost( $this->postRepository->nextIdentity(), $author, $title, $text ); $this->postRepository->save($post); } }
|
Web
Controller不需要检查authentication/authorization,但它需要创建用例请求和传入authorId参数,一个通用场景是从session中读取userId。剩下的参数从http请求中读取到。
随后CollaboratorService的实现直接与userId进行交互。
1 2 3 4 5 6 7 8 9 10 11 12
| class UserIdCollaboratorService implements CollaboratorService { public function authorFrom($authorId, $blogId) { … } } class PostController { public newPostAction() { $authorId = $this->sessionService()->userId(); } }
|
REST
REST下使用http headr头中Authorization获取用户的token,在REST入口中,认证机制可能是OAuth、JTW等。CollaboratorService的实现可能与那个token进行交互。
1 2 3 4 5 6 7 8 9 10 11 12
| class OAuthCollaboratorService implements CollaboratorService { public function authorFrom($authorId, $blogId) { … } } class PostRestFulController { public newPostAction() { $authorId = $this->authorizationHeader(); } }
|
Console
authorId参数来自命令行,因此如果给出的author-id属于这个blog则不会有任何异常抛出:
1
| blog add-post — author-id 1 — blog-id 1 — title Foo
|
CallaboratorService的实现可能与Web或其他实现一样:
1 2 3 4 5 6 7 8 9 10 11 12
| class ConsoleCollaboratorService implements CollaboratorService { public function authorFrom($authorId, $blogId) { … } } class PostConsole { public newPostCommand() { } }
|
总结
- 所有对authentication/authorization的检查都在Application Service中的CollaboratorService中隐式的完成。
- Application Service中authorId参数的输入某种程度上是多态的,因为它接收来自Web的直接的id或来自REST的token。也许这也是允许我们将authentication/auuthorization放进Application Service之内的关键点。
- CollaboratorService根据认证机制需要几种不同的实现。
原文:https://medium.com/@martinezdelariva/authentication-and-authorization-in-ddd-671f7a5596ac