Api

后盾人 houdunren.com @ 向军大叔

REST是所有Web应用都应该遵守的架构设计指导原则。 Representational State Transfer,翻译是”表现层状态转化”。

面向资源是REST最明显的特征,对于同一个资源的一组不同的操作。资源是服务器上一个可命名的抽象概念,资源是以名词为核心来组织的,首先关注的是名词。REST要求,必须通过统一的接口来对资源执行各种操作。对于每个资源只能执行一组有限的操作。

动作

GET (SELECT):从服务器检索特定资源,或资源列表
POST (CREATE):在服务器上创建一个新的资源
PUT (UPDATE):更新服务器上的资源,提供整个资源
PATCH (UPDATE):更新服务器上的资源,仅提供更改的属性
DELETE (DELETE):从服务器删除资源

命名

路径又称"终点"(endpoint),表示API的具体网址。

在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表名对应。一般来说,数据库中的表都是同种记录的"集合"(collection),所以API中的名词也应该使用复数。

举例来说,有一个API提供动物园(zoo)的信息,还包括各种动物和雇员的信息,则它的路径应该设计成下面这样。

接口尽量使用名词,禁止使用动词,下面是一些例子。

GET         /zoos:列出所有动物园
POST        /zoos:新建一个动物园
GET         /zoos/ID:获取某个指定动物园的信息
PUT         /zoos/ID:更新某个指定动物园的信息(提供该动物园的全部信息)
PATCH       /zoos/ID:更新某个指定动物园的信息(提供该动物园的部分信息)
DELETE      /zoos/ID:删除某个动物园
GET         /zoos/ID/animals:列出某个指定动物园的所有动物
DELETE      /zoos/ID/animals/ID:删除某个指定动物园的指定动物

再比如,某个URI是/posts/show/1,其中show是动词,这个URI就设计错了,正确的写法应该是/posts/1,然后用GET方法表示show。

版本

应该将API的版本号放入URL。如:

https://api.example.com/v1

过滤

如果记录数量很多,服务器不可能都将它们返回给用户。API应该提供参数,过滤返回结果。
下面是一些常见的参数。

?limit=10:指定返回记录的数量
?offset=10:指定返回记录的开始位置。
?page_number=2&page_size=100:指定第几页,以及每页的记录数。
?sortby=name&order=asc:指定返回结果按照哪个属性排序,以及排序顺序。
?animal_type_id=1:指定筛选条件

状态码

200 OK - [GET]:服务器成功返回用户请求的数据,该操作是幂等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用户新建或修改数据成功。
202 Accepted - [*]:表示一个请求已经进入后台排队(异步任务)
204 NO CONTENT - [DELETE]:用户删除数据成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用户发出的请求有错误,服务器没有进行新建或修改数据的操作
401 Unauthorized - [*]:表示用户没有权限(令牌、用户名、密码错误)。
403 Forbidden - [*] 表示用户得到授权(与401错误相对),但是访问是被禁止的。
404 NOT FOUND - [*]:用户发出的请求针对的是不存在的记录,服务器没有进行操作,该操作是幂等的。
406 Not Acceptable - [GET]:用户请求的格式不可得(比如用户请求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用户请求的资源被永久删除,且不会再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 当创建一个对象时,发生一个验证错误。
429 Too Many Requests - 由于请求频次达到上限而被拒绝访问
500 INTERNAL SERVER ERROR - [*]:服务器发生错误,用户将无法判断发出的请求是否成功。

响应格式

从可读性和通用性来讲 JSON 是最好的响应数据格式,下面是一个错误消息响应数据结构。

{   
    'message' => ':message',
    'errors' => ':errors',
    'code' => ':code',
    'status_code' => ':status_code',
    'debug' => ':debug'
}
  • message:表示在API调用失败的情况下详细的错误信息,这个信息可以由客户端直接呈现给用户
  • errors:参数具体错误,比如字段较对错误内容
  • code:自定义错误码
  • status_code:http状态码
  • debug:debug调试信息
错误返回值根据情况进行删减

Postman

postman可以高效的测试和维护接口。https://www.getpostman.com/apps

aa

aa

使用 postMan 工具测试结果

aa

环境变量

我们在本地和服务器上都想测试接口,可以将域名定义为环境变量,这样我们只要改变环境域名就会自动变化。

aa

aa

aa

Dingo

Dingo Api 是致力于提供给开发者一套工具,帮助你方便快捷的建造你自己的API。这个包的目标是保持尽可能的灵活,它并不能覆盖所有的情况,也不能解决所有的问题。

官网:https://github.com/dingo/api/

文档:https://github.com/dingo/api/wiki/Configuration

安装组件

composer require dingo/api:2.0.0-alpha2

执行下面命令生成配置文件 /config/api.php

php artisan vendor:publish

配置说明

配置统一定义在 config/api.php 文档中

#接口围绕:[x]本地和私有环境 [prs]公司内部app使用 [vnd]公开接口
'standardsTree' => env('API_STANDARDS_TREE', 'x')

#项目名称
'subtype' => env('API_SUBTYPE', 'hdcms')

#Api前缀 通过 www.hdcms.com/api 来访问 API。
'prefix' => env('API_PREFIX', 'api')

#api域名
'domain' => env('API_DOMAIN', 'api.hdcms.com'),

#版本号
'version' => env('API_VERSION', 'v1')

#开发时开启DEBUG便于发现错误
'debug' => env('API_DEBUG', false)
prefix 与 domain 只能二选一

接口版本

routes/api.php 文件定义

$api = app(\Dingo\Api\Routing\Router::class);

#默认配置指定的是v1版本,可以直接通过 {host}/api/version 访问到
$api->version('v1', function ($api) {
    $api->get('version', function () {
        return 'v1';
    });
});

#如果v2不是默认版本,需要设置请求头  
#Accept: application/[配置项 standardsTree].[配置项 subtype].v2+json
$api->version('v2', function ($api) {
    $api->get('version', function () {
        return 'v2';
    });
});

基础控制器

php artisan make:controller Api/Controller

修改内容如下

namespace App\Http\Controllers\Api;

use Dingo\Api\Routing\Helpers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller as SysController;

class Controller extends SysController
{
    use Helpers;
}

Transformers

Transformers 允许你便捷地、始终如一地将对象转换为一个数组。通过使用一个 transformer 你可以对整数和布尔值,包括分页结果和嵌套关系进行类型转换。

基本使用

一个 transformer 是一个类,它会获取原始数据并将返回一个格式化之后的标准数组。

namespace App\Transformers;

use App\User;
use League\Fractal\TransformerAbstract;

class UserTransformer extends TransformerAbstract
{
    public function transform(User $user)
    {
        return [
            'id'        => $user['id'],
            'name'      => $user['name'],
            'created_at'=> $user->created_at->toDateTimeString()
        ];
    }
}

返回单个数据

return $this->response->item(User::find(1),new UserTransformer());

返回集合

return $this->response->collection(User::get(),new UserTransformer());

分页数据

return $this->response->paginator(User::paginate(2),new UserTransformer());

include

获取文章时我们希望获取文章的栏目数据,include的特性就非常方便了。

下面是ContentTransformer中的定义,

class ContentTransformer extends TransformerAbstract
{
    # 定义可以include可使用的字段
    protected $availableIncludes = ['category'];

    public function transform(Content $content)
    {
        return [
            'id'   => $content['id'],
            'name' => $content['title'],
        ];
    }

    public function includeCategory(Content $content)
    {
        return $this->item($content->category,new CategoryTransformer());
    }
}

当我们调用 {host}/api/contents?include=category 接口时,栏目数据也一并会返回

return $this->response->paginator(Content::paginate(1),new ContentTransformer());

返回结果如下

{
    "data": [
        {
            "id": 1,
            "name": "后盾人 人人做后盾",
            "category": {
                "data": {
                    "id": 2,
                    "name": "编程"
                }
            }
        }
    ],
    "meta": {
        "pagination": {
            "total": 100,
            "count": 1,
            "per_page": 1,
            "current_page": 1,
            "total_pages": 100,
            "links": {
                "next": "http://xiang.houdunren.com/api/contents?page=2"
            }
        }
    }
}

响应结果

设置响应状态码

return $this->response->array(User::get())->setStatusCode(200);
return response()->json(['error' => 'Unauthorized'], 401);

错误响应

// 一个自定义消息和状态码的普通错误。
return $this->response->error('This is an error.', 404);

// 一个没有找到资源的错误,第一个参数可以传递自定义消息。
return $this->response->errorNotFound();

// 一个 bad request 错误,第一个参数可以传递自定义消息。
return $this->response->errorBadRequest();

// 一个服务器拒绝错误,第一个参数可以传递自定义消息。
return $this->response->errorForbidden();

// 一个内部错误,第一个参数可以传递自定义消息。
return $this->response->errorInternal();

// 一个未认证错误,第一个参数可以传递自定义消息。
return $this->response->errorUnauthorized('帐号或密码错误');

限制请求数

使用 api.throttle中间件结合 limit、expires 参数可实现接口次数限制。下面是定义在 routes/api.php 路由文件中的示例。

$api->version('v1', ['namespace' => '\App\Api'], function ($api) {
    $api->group(['middleware' => 'api.throttle', 'limit' => 2, 'expires' => 1], function ($api) {
        $api->get('user', 'UserController@all');
    });
});

限制1分钟只能访问2次。

身份验证

可以通过 api.auth 路由中间件来启用路由或者路由群组的保护,我们使用下面讲解的jwt组件完成接口验证。

在所有的路由上启用

$api->version('v1', ['middleware' => 'api.auth'], function ($api) {
    // 在这个版本群组下的所有路由将进行身份验证。
});

特定的路由上启用

$api->version('v1', function ($api) {
    $api->get('user', ['middleware' => 'api.auth', function () {
        // 这个路由将进行身份验证。
    }]);

    $api->get('posts', function () {
        // 这个路由不会验证身份。
    });
});

控制器上进行身份验证

Laravel可以在控制器里启用中间件。您可以在构造函数里使用 middleware 的方法。

class UserController extends Illuminate\Routing\Controller
{
    use Helpers;

    public function __construct()
    {
        $this->middleware('api.auth');

        // 这个中间件只在  index  中启用
        $this->middleware('api.auth', ['only' => ['index']]);
    }
...

Jwt

Jwt是高效简单的接口验证组件,使用非常广泛。

GitHub:https://github.com/tymondesigns/jwt-auth

Packagist:https://packagist.org/packages/tymon/jwt-auth

在线文档: http://jwt-auth.readthedocs.io/en/develop/quick-start/

安装组件

目前2.0版本正在开发中还不可以正常使用,所以我们使用 1.0.0-rc.2。

composer require tymon/jwt-auth:1.0.0-rc.2

生成配置文件

php artisan vendor:publish

生成密钥

这是用来给你的token签名的钥匙,使用以下命令生成一个密钥:

php artisan jwt:secret

这将用 JWT_SECRET=foobar 更新.env文件

配置说明

JWT配置文件是 config/jwt.php,下面有部分配置项进行说明:

#令牌过期时间(单位分钟),设置null为永不过期
'ttl' => env('JWT_TTL', 60)

#刷新令牌时间(单位分钟),设置为null可永久随时刷新
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160)

更新用户模型

首先,您需要在用户模型上实现 Tymon\JWTAuth\Contracts\JWTSubject 契约,它要求您实现两个方法 getJWTIdentifier()getJWTCustomClaims()

下面的示例应该能让您了解这可能是什么样子的。显然,您应该根据需要进行任何更改,以满足自己的需要。

<?php

namespace App;

use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    /**
     * 获取将存储在JWT主题声明中的标识符.
     * 就是用户表主键 id
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * 返回一个键值数组,其中包含要添加到JWT的任何自定义声明.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        return [];
    }
}

配置验证守卫

修改 config/auth.php 文件以使用jwt保护来为接口身份验证提供支持。

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
]

修改dingo配置文件 config/api.php 文件中的身份验证提供者

'auth' => [
    'jwt' => \Dingo\Api\Auth\Provider\JWT::class,
],

验证操作

路由定义

$api = app(\Dingo\Api\Routing\Router::class);
$api->version('v1', ['namespace' => 'App\Http\Controllers\Api',], function ($api) {
    $api->post('login', 'AuthController@login');
    $api->get('logout', 'AuthController@logout');
    $api->get('me', 'AuthController@me');
});

控制器定义

class AuthController extends Controller
{
    public function __construct()
    {
        // 除login外都需要验证
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    //登录获取token
    public function login()
    {
        $credentials = request(['email', 'password']);

        if (!$token = auth('api')->attempt($credentials)) {
            return $this->response->errorUnauthorized('帐号或密码错误');
        }

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

    //获取用户资料
    public function me()
    {
        return response()->json(auth('api')->user());
    }

    //销毁token
    public function logout()
    {
        auth('api')->logout();

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

    //刷新token
    public function refresh()
    {
        return $this->respondWithToken(auth('api')->refresh());
    }

    //响应token
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type'   => 'bearer',
            'expires_in'   => auth('api')->factory()->getTTL() * 60,
        ]);
    }
}

使用令牌

当请求需要验证的api时必须带有token,下面是使用header头携带令牌数据

Authorization: Bearer 令牌数据

在postman 工具中可以使用以下方式简化操作

aa

点个赞呗2

最后修改:2019 年 04 月 02 日
如果觉得我的文章对你有用,请随意赞赏