授权
介绍
除了提供内置的身份验证服务外,Laravel还提供了一种简单的方法来授权用户对给定资源的操作。例如,即使用户已通过身份验证,他们可能无权更新或删除由您的应用程序管理的某些Eloquent模型或数据库记录。Laravel的授权功能提供了一种简单、有组织的方式来管理这些类型的授权检查。
Laravel提供了两种主要的授权操作方式:门卫和策略。可以将门卫和策略视为路由和控制器。门卫提供了一种简单的、基于闭包的方法来进行授权,而策略则像控制器一样,将逻辑围绕特定的模型或资源进行分组。在本文档中,我们将首先探讨门卫,然后研究策略。
在构建应用程序时,您不需要在仅使用门卫或仅使用策略之间做出选择。大多数应用程序很可能会包含一些门卫和策略的混合,这完全没问题!门卫最适用于与任何模型或资源无关的操作,例如查看管理员仪表板。相反,当您希望为特定模型或资源授权操作时,应使用策略。
门卫
编写门卫
WARNING
门卫是学习Laravel授权功能基础的好方法;然而,在构建健壮的Laravel应用程序时,您应该考虑使用策略来组织您的授权规则。
门卫只是确定用户是否有权执行给定操作的闭包。通常,门卫是在App\Providers\AppServiceProvider
类的boot
方法中使用Gate
外观定义的。门卫总是接收用户实例作为第一个参数,并且可以选择接收其他参数,例如相关的Eloquent模型。
在此示例中,我们将定义一个门卫,以确定用户是否可以更新给定的App\Models\Post
模型。门卫将通过将用户的id
与创建帖子的用户的user_id
进行比较来实现这一点:
use App\Models\Post;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id;
});
}
2
3
4
5
6
7
8
9
10
11
12
13
像控制器一样,门卫也可以使用类回调数组定义:
use App\Policies\PostPolicy;
use Illuminate\Support\Facades\Gate;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Gate::define('update-post', [PostPolicy::class, 'update']);
}
2
3
4
5
6
7
8
9
10
通过门卫授权操作
要使用门卫授权操作,您应该使用Gate
外观提供的allows
或denies
方法。请注意,您不需要将当前经过身份验证的用户传递给这些方法。Laravel会自动处理将用户传递到门卫闭包中。通常在执行需要授权的操作之前,在应用程序的控制器中调用门卫授权方法:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PostController extends Controller
{
/**
* 更新给定的帖子。
*/
public function update(Request $request, Post $post): RedirectResponse
{
if (! Gate::allows('update-post', $post)) {
abort(403);
}
// 更新帖子...
return redirect('/posts');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
如果您想确定当前经过身份验证的用户以外的用户是否有权执行操作,可以使用Gate
外观的forUser
方法:
if (Gate::forUser($user)->allows('update-post', $post)) {
// 用户可以更新帖子...
}
if (Gate::forUser($user)->denies('update-post', $post)) {
// 用户不能更新帖子...
}
2
3
4
5
6
7
您可以使用any
或none
方法一次授权多个操作:
if (Gate::any(['update-post', 'delete-post'], $post)) {
// 用户可以更新或删除帖子...
}
if (Gate::none(['update-post', 'delete-post'], $post)) {
// 用户不能更新或删除帖子...
}
2
3
4
5
6
7
授权或抛出异常
如果您想尝试授权操作并在用户无权执行给定操作时自动抛出Illuminate\Auth\Access\AuthorizationException
,可以使用Gate
外观的authorize
方法。AuthorizationException
实例会自动转换为Laravel的403 HTTP响应:
Gate::authorize('update-post', $post);
// 操作已授权...
2
3
提供额外的上下文
门卫方法用于授权能力(allows
、denies
、check
、any
、none
、authorize
、can
、cannot
)和授权Blade指令(@can
、@cannot
、@canany
)可以接收一个数组作为第二个参数。这些数组元素作为参数传递给门卫闭包,并可用于在进行授权决策时提供额外的上下文:
use App\Models\Category;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::define('create-post', function (User $user, Category $category, bool $pinned) {
if (! $user->canPublishToGroup($category->group)) {
return false;
} elseif ($pinned && ! $user->canPinPosts()) {
return false;
}
return true;
});
if (Gate::check('create-post', [$category, $pinned])) {
// 用户可以创建帖子...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
门卫响应
到目前为止,我们只检查了返回简单布尔值的门卫。然而,有时您可能希望返回更详细的响应,包括错误消息。为此,您可以从门卫返回一个Illuminate\Auth\Access\Response
:
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
Gate::define('edit-settings', function (User $user) {
return $user->isAdmin
? Response::allow()
: Response::deny('您必须是管理员。');
});
2
3
4
5
6
7
8
9
即使您从门卫返回授权响应,Gate::allows
方法仍将返回一个简单的布尔值;然而,您可以使用Gate::inspect
方法获取门卫返回的完整授权响应:
$response = Gate::inspect('edit-settings');
if ($response->allowed()) {
// 操作已授权...
} else {
echo $response->message();
}
2
3
4
5
6
7
使用Gate::authorize
方法时,如果操作未授权,将抛出AuthorizationException
,授权响应提供的错误消息将传播到HTTP响应:
Gate::authorize('edit-settings');
// 操作已授权...
2
3
自定义HTTP响应状态
当通过门卫拒绝操作时,将返回403
HTTP响应;然而,有时返回替代HTTP状态码可能很有用。您可以使用Illuminate\Auth\Access\Response
类上的denyWithStatus
静态构造函数自定义授权检查失败时返回的HTTP状态码:
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
Gate::define('edit-settings', function (User $user) {
return $user->isAdmin
? Response::allow()
: Response::denyWithStatus(404);
});
2
3
4
5
6
7
8
9
由于通过404
响应隐藏资源是Web应用程序中非常常见的模式,因此提供了denyAsNotFound
方法以方便使用:
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
Gate::define('edit-settings', function (User $user) {
return $user->isAdmin
? Response::allow()
: Response::denyAsNotFound();
});
2
3
4
5
6
7
8
9
拦截门卫检查
有时,您可能希望为特定用户授予所有权限。您可以使用before
方法定义一个在所有其他授权检查之前运行的闭包:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::before(function (User $user, string $ability) {
if ($user->isAdministrator()) {
return true;
}
});
2
3
4
5
6
7
8
如果before
闭包返回非空结果,则该结果将被视为授权检查的结果。
您可以使用after
方法定义一个在所有其他授权检查之后执行的闭包:
use App\Models\User;
Gate::after(function (User $user, string $ability, bool|null $result, mixed $arguments) {
if ($user->isAdministrator()) {
return true;
}
});
2
3
4
5
6
7
after
闭包返回的值不会覆盖授权检查的结果,除非门卫或策略返回null
。
内联授权
有时,您可能希望确定当前经过身份验证的用户是否有权执行给定操作,而无需编写与该操作对应的专用门卫。Laravel允许您通过Gate::allowIf
和Gate::denyIf
方法执行这些类型的“内联”授权检查。内联授权不会执行任何定义的“前”或“后”授权钩子:
use App\Models\User;
use Illuminate\Support\Facades\Gate;
Gate::allowIf(fn (User $user) => $user->isAdministrator());
Gate::denyIf(fn (User $user) => $user->banned());
2
3
4
5
6
如果操作未授权或当前没有用户经过身份验证,Laravel将自动抛出Illuminate\Auth\Access\AuthorizationException
异常。AuthorizationException
实例会自动转换为Laravel异常处理程序的403 HTTP响应。
创建策略
生成策略
策略是围绕特定模型或资源组织授权逻辑的类。例如,如果您的应用程序是一个博客,您可能有一个App\Models\Post
模型和一个相应的App\Policies\PostPolicy
来授权用户操作,例如创建或更新帖子。
您可以使用make:policy
Artisan命令生成策略。生成的策略将放置在app/Policies
目录中。如果您的应用程序中不存在此目录,Laravel将为您创建它:
php artisan make:policy PostPolicy
make:policy
命令将生成一个空的策略类。如果您希望生成一个包含与查看、创建、更新和删除资源相关的示例策略方法的类,可以在执行命令时提供--model
选项:
php artisan make:policy PostPolicy --model=Post
注册策略
策略发现
默认情况下,只要模型和策略遵循标准的Laravel命名约定,Laravel会自动发现策略。具体来说,策略必须位于包含模型的目录或其上方的Policies
目录中。因此,例如,模型可以放置在app/Models
目录中,而策略可以放置在app/Policies
目录中。在这种情况下,Laravel将检查app/Models/Policies
中的策略,然后是app/Policies
。此外,策略名称必须与模型名称匹配,并具有Policy
后缀。因此,User
模型将对应于UserPolicy
策略类。
如果您希望定义自己的策略发现逻辑,可以使用Gate::guessPolicyNamesUsing
方法注册自定义策略发现回调。通常,此方法应从应用程序的AppServiceProvider
的boot
方法中调用:
use Illuminate\Support\Facades\Gate;
Gate::guessPolicyNamesUsing(function (string $modelClass) {
// 返回给定模型的策略类的名称...
});
2
3
4
5
手动注册策略
使用Gate
外观,您可以在应用程序的AppServiceProvider
的boot
方法中手动注册策略及其对应的模型:
use App\Models\Order;
use App\Policies\OrderPolicy;
use Illuminate\Support\Facades\Gate;
/**
* 启动任何应用程序服务。
*/
public function boot(): void
{
Gate::policy(Order::class, OrderPolicy::class);
}
2
3
4
5
6
7
8
9
10
11
编写策略
策略方法
一旦策略类注册完毕,您可以为其授权的每个操作添加方法。例如,让我们在PostPolicy
上定义一个update
方法,以确定给定的App\Models\User
是否可以更新给定的App\Models\Post
实例。
update
方法将接收User
和Post
实例作为其参数,并应返回true
或false
,指示用户是否有权更新给定的Post
。因此,在此示例中,我们将验证用户的id
是否与帖子的user_id
匹配:
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
/**
* 确定给定的帖子是否可以由用户更新。
*/
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
您可以根据需要继续在策略中定义其他方法,以授权其授权的各种操作。例如,您可以定义view
或delete
方法来授权与Post
相关的各种操作,但请记住,您可以自由地为策略方法命名。
如果您在通过Artisan控制台生成策略时使用了--model
选项,它将已经包含与viewAny
、view
、create
、update
、delete
、restore
和forceDelete
操作相关的方法。
NOTE
所有策略都是通过Laravel服务容器解析的,允许您在策略的构造函数中类型提示任何需要的依赖项,以便自动注入。
策略响应
到目前为止,我们只检查了返回简单布尔值的策略方法。然而,有时您可能希望返回更详细的响应,包括错误消息。为此,您可以从策略方法返回一个Illuminate\Auth\Access\Response
实例:
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
/**
* 确定给定的帖子是否可以由用户更新。
*/
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::deny('您不拥有此帖子。');
}
2
3
4
5
6
7
8
9
10
11
12
13
当从策略返回授权响应时,Gate::allows
方法仍将返回一个简单的布尔值;然而,您可以使用Gate::inspect
方法获取门卫返回的完整授权响应:
use Illuminate\Support\Facades\Gate;
$response = Gate::inspect('update', $post);
if ($response->allowed()) {
// 操作已授权...
} else {
echo $response->message();
}
2
3
4
5
6
7
8
9
使用Gate::authorize
方法时,如果操作未授权,将抛出AuthorizationException
,授权响应提供的错误消息将传播到HTTP响应:
Gate::authorize('update', $post);
// 操作已授权...
2
3
自定义HTTP响应状态
当通过策略方法拒绝操作时,将返回403
HTTP响应;然而,有时返回替代HTTP状态码可能很有用。您可以使用Illuminate\Auth\Access\Response
类上的denyWithStatus
静态构造函数自定义授权检查失败时返回的HTTP状态码:
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
/**
* 确定给定的帖子是否可以由用户更新。
*/
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::denyWithStatus(404);
}
2
3
4
5
6
7
8
9
10
11
12
13
由于通过404
响应隐藏资源是Web应用程序中非常常见的模式,因此提供了denyAsNotFound
方法以方便使用:
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\Response;
/**
* 确定给定的帖子是否可以由用户更新。
*/
public function update(User $user, Post $post): Response
{
return $user->id === $post->user_id
? Response::allow()
: Response::denyAsNotFound();
}
2
3
4
5
6
7
8
9
10
11
12
13
无模型的方法
某些策略方法仅接收当前经过身份验证的用户的实例。这种情况在授权create
操作时最为常见。例如,如果您正在创建一个博客,您可能希望确定用户是否有权创建任何帖子。在这些情况下,您的策略方法应仅期望接收一个用户实例:
/**
* 确定给定的用户是否可以创建帖子。
*/
public function create(User $user): bool
{
return $user->role == 'writer';
}
2
3
4
5
6
7
访客用户
默认情况下,如果传入的HTTP请求不是由经过身份验证的用户发起的,所有门卫和策略都会自动返回false
。然而,您可以通过声明“可选”类型提示或为用户参数定义提供null
默认值来允许这些授权检查通过到您的门卫和策略:
<?php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
/**
* 确定给定的帖子是否可以由用户更新。
*/
public function update(?User $user, Post $post): bool
{
return $user?->id === $post->user_id;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
策略过滤器
对于某些用户,您可能希望在给定策略中授权所有操作。为此,请在策略中定义一个before
方法。before
方法将在策略上的任何其他方法之前执行,给您一个机会在实际调用预期的策略方法之前授权操作。此功能最常用于授权应用程序管理员执行任何操作:
use App\Models\User;
/**
* 执行预授权检查。
*/
public function before(User $user, string $ability): bool|null
{
if ($user->isAdministrator()) {
return true;
}
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
如果您希望拒绝对特定类型用户的所有授权检查,则可以从before
方法返回false
。如果返回null
,授权检查将传递到策略方法。
WARNING
如果类不包含与被检查能力名称匹配的方法,则不会调用策略类的before
方法。
使用策略授权操作
通过用户模型
Laravel应用程序中包含的App\Models\User
模型包括两个用于授权操作的有用方法:can
和cannot
。can
和cannot
方法接收您希望授权的操作名称和相关模型。例如,让我们确定用户是否有权更新给定的App\Models\Post
模型。通常,这将在控制器方法中完成:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* 更新给定的帖子。
*/
public function update(Request $request, Post $post): RedirectResponse
{
if ($request->user()->cannot('update', $post)) {
abort(403);
}
// 更新帖子...
return redirect('/posts');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
如果为给定模型注册了策略,can
方法将自动调用相应的策略并返回布尔结果。如果未为模型注册策略,can
方法将尝试调用与给定操作名称匹配的基于闭包的门卫。
不需要模型的操作
请记住,某些操作可能对应于不需要模型实例的策略方法,例如create
。在这些情况下,您可以将类名传递给can
方法。类名将用于确定在授权操作时使用哪个策略:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PostController extends Controller
{
/**
* 创建一个帖子。
*/
public function store(Request $request): RedirectResponse
{
if ($request->user()->cannot('create', Post::class)) {
abort(403);
}
// 创建帖子...
return redirect('/posts');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
通过Gate
外观
除了为App\Models\User
模型提供的有用方法外,您还可以通过Gate
外观的authorize
方法始终授权操作。
与can
方法一样,此方法接受您希望授权的操作名称和相关模型。如果操作未授权,authorize
方法将抛出Illuminate\Auth\Access\AuthorizationException
异常,Laravel异常处理程序将自动将其转换为403状态码的HTTP响应:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class PostController extends Controller
{
/**
* 更新给定的博客帖子。
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update', $post);
// 当前用户可以更新博客帖子...
return redirect('/posts');
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
不需要模型的操作
如前所述,某些策略方法(如create
)不需要模型实例。在这些情况下,您应该将类名传递给authorize
方法。类名将用于确定在授权操作时使用哪个策略:
use App\Models\Post;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
/**
* 创建一个新的博客帖子。
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function create(Request $request): RedirectResponse
{
Gate::authorize('create', Post::class);
// 当前用户可以创建博客帖子...
return redirect('/posts');
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
通过中间件
Laravel包括一个中间件,可以在传入请求到达您的路由或控制器之前授权操作。默认情况下,可以使用can
中间件别名将Illuminate\Auth\Middleware\Authorize
中间件附加到路由上,Laravel会自动注册该别名。让我们探索一个使用can
中间件授权用户可以更新帖子的示例:
use App\Models\Post;
Route::put('/post/{post}', function (Post $post) {
// 当前用户可以更新帖子...
})->middleware('can:update,post');
2
3
4
5
在此示例中,我们将can
中间件传递了两个参数。第一个是我们希望授权的操作名称,第二个是我们希望传递给策略方法的路由参数。在这种情况下,由于我们使用了隐式模型绑定,App\Models\Post
模型将传递给策略方法。如果用户无权执行给定操作,中间件将返回403状态码的HTTP响应。
为了方便起见,您还可以使用can
方法将can
中间件附加到路由上:
use App\Models\Post;
Route::put('/post/{post}', function (Post $post) {
// 当前用户可以更新帖子...
})->can('update', 'post');
2
3
4
5
不需要模型的操作
同样,某些策略方法(如create
)不需要模型实例。在这些情况下,您可以将类名传递给中间件。类名将用于确定在授权操作时使用哪个策略:
Route::post('/post', function () {
// 当前用户可以创建帖子...
})->middleware('can:create,App\Models\Post');
2
3
在字符串中间件定义中指定整个类名可能会变得繁琐。因此,您可以选择使用can
方法将can
中间件附加到路由上:
use App\Models\Post;
Route::post('/post', function () {
// 当前用户可以创建帖子...
})->can('create', Post::class);
2
3
4
5
通过Blade模板
在编写Blade模板时,您可能希望仅在用户有权执行给定操作时显示页面的一部分。例如,您可能希望仅在用户可以实际更新帖子时显示博客帖子的更新表单。在这种情况下,您可以使用@can
和@cannot
指令:
@can('update', $post)
<!-- 当前用户可以更新帖子... -->
@elsecan('create', App\Models\Post::class)
<!-- 当前用户可以创建新帖子... -->
@else
<!-- ... -->
@endcan
@cannot('update', $post)
<!-- 当前用户不能更新帖子... -->
@elsecannot('create', App\Models\Post::class)
<!-- 当前用户不能创建新帖子... -->
@endcannot
2
3
4
5
6
7
8
9
10
11
12
13
这些指令是编写@if
和@unless
语句的便捷快捷方式。上面的@can
和@cannot
语句等效于以下语句:
@if (Auth::user()->can('update', $post))
<!-- 当前用户可以更新帖子... -->
@endif
@unless (Auth::user()->can('update', $post))
<!-- 当前用户不能更新帖子... -->
@endunless
2
3
4
5
6
7
您还可以确定用户是否有权执行给定操作数组中的任何操作。为此,请使用@canany
指令:
@canany(['update', 'view', 'delete'], $post)
<!-- 当前用户可以更新、查看或删除帖子... -->
@elsecanany(['create'], \App\Models\Post::class)
<!-- 当前用户可以创建帖子... -->
@endcanany
2
3
4
5
不需要模型的操作
与大多数其他授权方法一样,如果操作不需要模型实例,您可以将类名传递给@can
和@cannot
指令:
@can('create', App\Models\Post::class)
<!-- 当前用户可以创建帖子... -->
@endcan
@cannot('create', App\Models\Post::class)
<!-- 当前用户不能创建帖子... -->
@endcannot
2
3
4
5
6
7
提供额外的上下文
在使用策略授权操作时,您可以将数组作为第二个参数传递给各种授权函数和助手。数组中的第一个元素将用于确定应调用哪个策略,而数组的其余元素将作为参数传递给策略方法,并可用于在进行授权决策时提供额外的上下文。例如,考虑以下包含额外$category
参数的PostPolicy
方法定义:
/**
* 确定给定的帖子是否可以由用户更新。
*/
public function update(User $user, Post $post, int $category): bool
{
return $user->id === $post->user_id &&
$user->canUpdateCategory($category);
}
2
3
4
5
6
7
8
在尝试确定经过身份验证的用户是否可以更新给定帖子时,我们可以像这样调用此策略方法:
/**
* 更新给定的博客帖子。
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post): RedirectResponse
{
Gate::authorize('update', [$post, $request->category]);
// 当前用户可以更新博客帖子...
return redirect('/posts');
}
2
3
4
5
6
7
8
9
10
11
12
13
授权与Inertia
虽然授权必须始终在服务器上处理,但为前端应用程序提供授权数据以正确呈现应用程序的UI通常很方便。Laravel没有定义将授权信息暴露给Inertia驱动的前端的必需约定。
然而,如果您使用的是Laravel的Inertia基础入门套件之一,您的应用程序已经包含一个HandleInertiaRequests
中间件。在此中间件的share
方法中,您可以返回将提供给应用程序中所有Inertia页面的共享数据。此共享数据可以作为定义用户授权信息的便捷位置:
<?php
namespace App\Http\Middleware;
use App\Models\Post;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
// ...
/**
* 定义默认共享的属性。
*
* @return array<string, mixed>
*/
public function share(Request $request)
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
'permissions' => [
'post' => [
'create' => $request->user()->can('create', Post::class),
],
],
],
];
}
}
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