PHP,DDD,CQRS,Event Sourcing,Kubernetes,Docker,Golang

0%

领域驱动设计中的认证和授权

认证(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
// Request
class AddPostRequest()
{
public function authorId() {…}
public function blogId() {…}
public function title() {…}
public function text() {…}
}
// Application Service
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();
}
// executes AddPostService use case
}
}

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();
}
// execute AddPostService use case
}
}

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()
{
// gets values from console parameters
// execute AddPostService use case
}
}

总结

  • Authentication/Authorization检查是在Application Service之外的Controller或框架的其他hook中进行。
  • Authentication/Authorization必须在Application Service用例执行前进行。
  • 根据应用的入口(如Web、REST、Console)来使用authentication服务,实例中使用的是OauthServiceSessionService

版本2:在Application Service内进行

我们可以在用例内使用通用语言,来隐式的执行authentication/authorization。

Application Service

它将使用一个名为CollaboratorServiceDomain 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);
}
// Request
class AddPostRequest
{
public function authorId() {…}
public function blogId() {…}
public function title() {…}
public function text() {…}
}
// Application Service
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();
// executes ApplicationService use case
}
}

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();
// executes ApplicationService use case
}
}

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()
{
// gets values from console parameters
// executes AddPostService use case
}
}

总结

  • 所有对authentication/authorization的检查都在Application Service中的CollaboratorService中隐式的完成。
  • Application ServiceauthorId参数的输入某种程度上是多态的,因为它接收来自Web的直接的id或来自REST的token。也许这也是允许我们将authentication/auuthorization放进Application Service之内的关键点。
  • CollaboratorService根据认证机制需要几种不同的实现。

原文:https://medium.com/@martinezdelariva/authentication-and-authorization-in-ddd-671f7a5596ac