Laravel5.8+Dingo+JWT+Swagger 开发API

在Laravel5.8中使用“Laravel必知必会”的两个轮子dingo/apitymon/jwt-auth以及文档系统swagger-api/swagger-ui开发一个规范优雅的API。

Dingo

安装

1
composer require dingo/api

image
组件包含自动包发现配置,无需手动注册Provider

配置

1
php artisan vendor:publish --provider="Dingo\Api\Provider\LaravelServiceProvider"

无特殊需求直接通过.env文件配置

1
2
3
4
5
6
7
8
9
10
# Dingo Api 
API_STANDARDS_TREE=x
API_SUBTYPE=mp_admin
API_PREFIX=api # 前缀
API_VERSION=v1 # 默认版本
API_NAME="Mp Admin API" # 名称
API_CONDITIONAL_REQUEST=false # 条件请求
API_STRICT=false # 严格模式,开启时请求头必要带标准的Accept信息
API_DEFAULT_FORMAT=json
API_DEBUG=true # 调试

由于子域已被占用,这里采用前缀的格式。接口地址格式为:https://admin.mp.example.com/api

路由

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
/* @var \Dingo\Api\Routing\Router $api */
$api = app('Dingo\Api\Routing\Router');

$api->version('v1', function () use ($api) {
$api->group(['middleware' => ['api', 'bindings']], function () use ($api) {
// 系统调试日志
$api->get('dev-logs', ['as' => 'dev-logs', 'uses' => '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index', 'middleware' => ['auth']]);

// 验证
$api->group(['prefix' => 'auth'], function () use ($api) {
$api->post('login', ['as' => 'auth.login', 'uses' => 'App\Http\Controllers\AuthController@login']);
$api->get('me', ['as' => 'auth.me', 'uses' => 'App\Http\Controllers\AuthController@me']);
$api->post('refresh', ['as' => 'auth.refresh', 'uses' => 'App\Http\Controllers\AuthController@refresh']);
$api->post('logout', ['as' => 'auth.logout', 'uses' => 'App\Http\Controllers\AuthController@logout']);
$api->put('reset-pwd', ['as' => 'auth.reset-pwd', 'uses' => 'App\Http\Controllers\AuthController@resetPwd']);
});

// 系统
$api->group(['prefix' => 'sys'], function () use ($api) {
// 当前用户能看到的菜单与拥有的权限(别名)
$api->get('menu', ['as' => 'sys.menu', 'uses' => 'App\Http\Controllers\System\AdminController@menu']);

// 系统资源
$api->resource('permissions', 'App\Http\Controllers\System\PermissionController', ['names' => 'sys.permissions']);
$api->resource('roles', 'App\Http\Controllers\System\RoleController', ['names' => 'sys.roles']);
$api->resource('admins', 'App\Http\Controllers\System\AdminController', ['names' => 'sys.admins']);
});

...
});
});

所有控制器都需要完整的命名空间,不支持为群组配置命名空间

异常

定义异常

Dingo已经定义了接口场景下常用的异常,所以相关异常可以继承\Dingo\Api\Exception\下的异常。例如:

1
2
3
4
5
6
7
8
9
class UpdateResourceFailedException extends \Dingo\Api\Exception\UpdateResourceFailedException
{
protected $message = '更新失败';

public function __construct($message = null, $errors = null, \Exception $previous = null, $headers = [], $code = 0) {
$message = $message ?? $this->getMessage();
parent::__construct($message, $errors, $previous, $headers, $code);
}
}

自定义异常响应

Dingo会先于Laravel自带的Handle获取Symfony\Component\HttpKernel\Exception,所以在系统Handle::render()中处理不了这些异常。需要这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
// 优化显示验证异常
app('Dingo\Api\Exception\Handler')->register(function (ValidationException $exception) {
$error = $exception->validator->errors()->first();
throw new ValidationHttpException($error);
});
}

...

Transformers

目前通过Eloquent ORM的$casts属性来自动转化字段类型,还没复杂的需求需要用到Transformers。

JWT

安装

Laravel 5.5以上需要使用1.0.0版本

1
composer require "tymon/jwt-auth:1.0.0-rc.4.1"

config/app.php中手动注册Provider

1
2
3
4
5
6
'providers' => [

...

Tymon\JWTAuth\Providers\LaravelServiceProvider::class,
]

配置

发布配置

1
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

生成密钥(可选)

1
php artisan jwt:secret

配置ENV

1
2
3
# JWT Oauth
JWT_SECRET=F5C5Qodnaa78PGFTFGhWgt7cNaHCOcXTI6SdtfuCfjHpotu7uwmlTy8HlbvsXeNt #64位密钥
JWT_TTL=1440 #Token过期时间

使用验证

定义一个控制器基类,在构造函数中指定验证中间件即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests, Helpers;

/**
* Create a new AuthController instance.
*
* @return void
*/
public function __construct()
{
if (needAuth()) { // 助手函数,开发环境不验证Token
$this->middleware('auth:api', ['except' => ['login']]);
}
}
}

之后所有路由到此控制器子类的请求必须带有Authorization头,值为Bearer $token才能通过检验。

只有继承这个基类的控制器才会进行检验,如果不是其子类需要在路由中指定中间件,例如上面路由中dev-logs的例子。

验证控制器

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class AuthController extends Controller
{
public function login(LoginRequest $request)
{
$credentials = $request->only(['email', 'password']);

$auth = auth();
if (!$token = $auth->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized', 'status_code' => 401], 401);
}

return $this->respondWithToken($token);
}

public function me()
{
return response()->json(auth()->user());
}

public function logout()
{
auth()->logout();

return response()->json(['message' => 'Successfully logged out']);
}

public function refresh()
{
/** @noinspection PhpUndefinedMethodInspection */
return $this->respondWithToken(auth()->refresh());
}

public function resetPwd(ResetPwdRequest $request)
{
$oldPassword = $request->get('old_password');
$newPassword = $request->get('new_password');

// 检查旧密码
$admin = auth()->user();
if (!password_verify($oldPassword, $admin->password)) {
throw new AuthenticationException('密码错误');
}

// 更新密码
$admin->update([
'password' => password_hash($newPassword, PASSWORD_DEFAULT)
]);

return success();
}

protected function respondWithToken($token)
{
/** @noinspection PhpUndefinedMethodInspection */
return $this->response->array([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth()->factory()->getTTL() * 60
])->withHeader('Authorization', $token);
}
}

Swagger

这里采用l5-swagger扩展,集成了swagger-ui(使用json配置的文档系统)和php-swagger(使用代码注释生成json配置)。

安装

1
composer require "darkaonline/l5-swagger:5.8.*"

config/app.php中手动注册Provider

1
2
3
4
5
6
'providers' => [

...

L5Swagger\L5SwaggerServiceProvider::class,
]

配置

发布配置与视图模板

1
php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"

修改配置config/l5-swagger.php

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
return [
'api' => [
/*
|--------------------------------------------------------------------------
| Edit to set the api's title
|--------------------------------------------------------------------------
*/

'title' => '后台Api文档', # 文档页面的标题
],

'routes' => [
/*
|--------------------------------------------------------------------------
| Route for accessing api documentation interface
|--------------------------------------------------------------------------
*/

'api' => 'api/docs', # 文档页面的路由

/*
|--------------------------------------------------------------------------
| Route for accessing parsed swagger annotations.
|--------------------------------------------------------------------------
*/

'docs' => 'docs', # 路由别名

...

.env中添加

1
2
3
# Swagger
SWAGGER_VERSION=3.0 # php-swagger的版本,不同版本注释写法不同!
L5_SWAGGER_GENERATE_ALWAYS=true # 自动生成文档json,不要在生产环境打开此项

最后记得将文档的json配置加入.gitignore

1
2
...
/storage/api-docs/api-docs.json

编写Swagger注释

这里不详细介绍php-swagger的注释语法,只放出几个例子:

  • swagger.php

    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
    <?php

    /**
    *
    * @OA\OpenApi(
    * security={
    * {
    * "Bearer":{}
    * }
    * },
    * @OA\Server(
    * url=L5_SWAGGER_CONST_HOST
    * )
    * )
    *
    * @OA\Info(
    * version="1.0",
    * title="小程序后台Api文档",
    * @OA\Contact(
    * name="火星救援网络科技有限公司",
    * url="http://www.example.com/"
    * )
    * )
    *
    * @OA\SecurityScheme(
    * securityScheme="Bearer",
    * type="apiKey",
    * name="Authorization",
    * in="header",
    * )
    *
    */
  • swagger-tags.php

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /**
    * @OA\Tag(
    * name="Auth",
    * description="验证模块"
    * )
    *
    * @OA\Tag(
    * name="System.Permission",
    * description="系统模块中的权限管理"
    * )
    *
    * @OA\Tag(
    * name="System.Role",
    * description="系统模块中的角色管理"
    * )
    *
    * @OA\Tag(
    * name="System.Admin",
    * description="系统模块中的用户管理"
    * )
    */
  • AuthController.php

    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
    43
    class AuthController extends Controller
    {
    /**
    * @OA\Post(
    * path="/auth/login",
    * summary="获取凭证",
    * tags={"Auth"},
    * description="通过账号密码获取Access Token",
    * @OA\Parameter(
    * name="email",
    * in="query",
    * required=true,
    * @OA\Schema(
    * type="string"
    * ),
    * example="test@example.com",
    * description="邮箱"
    * ),
    * @OA\Parameter(
    * name="password",
    * in="query",
    * required=true,
    * @OA\Schema(
    * type="string"
    * ),
    * description="密码"
    * ),
    * @OA\Response(
    * response=200,
    * description="返回凭证",
    * ref="#/components/schemas/Token"
    * ),
    * @OA\Response(
    * response=401,
    * description="账号不存在或密码错误"
    * )
    * )
    *
    * @param LoginRequest $request
    * @return \Illuminate\Http\JsonResponse
    */
    public function login(LoginRequest $request) {
    ...

注释可以写在项目任意php文件中,建议是有归属语义的注释写在归属代码上(例如接口注释写在控制器中,模型注释写在模型中),公共语义的注释一样单独写在一个php文件中。

生成文档json

1
php artisan l5-swagger:generate

最终效果

访问上面配置的路由地址

image

点击Authorize按钮,value填入Authorization头的值即可在生产环境的文档页面请求接口。

image

注释可以写在项目任意php文件中,建议是有归属语义的注释写在归属代码上(例如接口注释写在控制器中,模型注释写在模型中),公共语义的注释一样单独写在一个php文件中。

结语

至此,三个轮子在入门应用就介绍完了,这些轮子可以让你更快速的搭建API,专注于业务逻辑。

References

[0] dingo/api
[1] tymon/jwt-auth
[2] swagger-api/swagger-ui