安装配置什么的dingo wiki都说得很清楚,方便英文不好的同学我还是从头开写

安装

为了演示我新建了个laravel项目,首先在 composer.json里面加入

"require": {
    "php": ">=5.5.9",
    "laravel/framework": "5.2.*",
    "dingo/api": "1.0.*@dev"
},

接着我们

composer update

配置

如果你使用的laravel

在config/app.php中添加上dingo的ServiceProvider

接着我们通过artisan命令将dingo中的配置文件发布到config目录下

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

现在config目录下多了一个api.php配置文件,之后的一些配置会用到

如果你使用的lumen

在bootstrap/app.php下注册dingo的ServiceProvider

$app->register(Dingo\Api\Provider\LumenServiceProvider::class);

接着我们还需要配置一些其他的环境变量

我们可以在.env或刚才生成的api.php文件中添加或修改如下选项

API_SUBTYPE 你接口项目的名字,我随便设成dingo,注意,值需要全部小写

API_SUBTYPE=dingo

对于一个接口地址可以是www.homestead.app/api 这种二级目录的形式

API_PREFIX=api

也可以是二级域名api.homestead.app的形式,二选一

API_DOMAIN=api.homestead.com

接着是api版本号,才开始都是v1

API_VERSION=v1

API_NAME 主要是为了dingo自带的blueprint生存api文档时的标题用的,可以不设置,如果设置一定加双引号

API_NAME="My Dingo Demo"

严格模式开启后,顾名思义严格检验http header,如果无效则抛出ymfony\Component\HttpKernel\Exception\BadRequestHttpException异常,这个异常需要你自己去handle

API_STRICT=false

默认传输格式json

API_DEFAULT_FORMAT=json

debug模式,开发时暂时设为true

API_DEBUG=true

至此基本配置已经完成,如果就这样翻译wiki就太没意思了

设计RESTful接口

现在我们着手设计一个类似秘密的restful接口,实践才是硬道理!

接口需要完成如下功能:

注册,

鉴权(restful接口里面已经不存在登录的概念),

获取用户信息,

发布秘密,

获取秘密列表,

获取秘密评论,

评论秘密,

删除秘密,

暂时就考虑如上接口,毕竟是个玩具应用

—————————————————————–

API节点初步设计如下

注册

——POST /user/{user_name}/register

鉴权

——POST  /user/{user_name}/auth

获取用户信息

——GET    /user/{user_name}

发布秘密

——POST /user/{user_name}/secrets

删除秘密

——DELETE /user/{user_name}/secrets/{secret_id}

获取秘密列表

——GET /secrets

获取秘密评论列表

——GET   /secrets/{secret_id}/comments

评论秘密

——POST /secrets/{secret_id}/comments

上面的api节点稍难实现的应该是鉴权,毕竟看这篇博文的同学肯定也是第一次构建restful接口,鉴权的过程和原理都不太清楚而其他节点实现逻辑基本上和一般的网站后台无异,只是返回的数据格式稍有变化,但是要实现鉴权首先得注册,所以我就拿这两个接口作为演示

创建好api节点路由

我们同时在routes.php下进行restful节点路由定义但与平时采用

Route::get('/', function () {
    return 'Hello World';
});

定义节点不同,我们使用dingo封装上的路由来定义restful api节点

$api = app('Dingo\Api\Routing\Router');
$api->version('v1', function ($api) {
    $api->get('/hello/', function () {
        return "hello";
    });
});

如上我们已经定义好了一个get方法节点,在匿名函数的嵌套中我们依次定义了节点及其实现

其中 $api->version方法定义了接口的版本号,env文件里面我们默认设置是v1

我们在homestead里面run起来,这个接口已经是可以访问的了

我们用postman测试一下已经是可以访问的了,至于为什么一个简单的接口耗费了400ms,一方面是虚拟机共享目录,另一方面我为了测试开启了xdebug所以性能损耗很大

restful laravel hello api

可能你会说这和

Route::get('/hello', function () {
    return 'hello';
});

有什么区别,确实在这里还看不出dingo的优势,我们带着好奇心继续探索

根据dingo上wiki文档我们发现dingo完全是兼容Route常见写法的

我贴几段示例代码就不一一测试了

我们同样可以在节点定义中使用中间件

$api->version('v1', ['middleware'=>foo'],function($api){

});

group也是可以使用的

$api->version('v1', function ($api) {
    $api->group(['middleware' => 'foo'], function ($api) {
        // Endpoints registered here will have the "foo" middleware applied.
    });
});

实现一个注册结点

当然如果仅能使用闭包函数来实现逻辑就太惨了,我们弄个controller来看看

php artisan make:controller Api/V1/UserController

我们把所有api接口相关的Controller都放到Api目录下的V1目录中

将hello写到index方法里面来,UserController的命名空间太长了,为了长远打算我们使用namespcace统一命名空间

修改后routes.php如下

$api = app('Dingo\Api\Routing\Router');
$api->version('v1', function ($api) {
    $api->group(['namespace' => 'App\Http\Controllers\Api\V1'], function ($api) {
        $api->get('/hello/', 'UserController@index');
    });
});

看上去已经有那么点意思了,小试牛刀后我们正式着手接口设计,首先我们要实现注册接口

至于数据库我就偷懒使用自带的migration来实现了,为了方便我稍稍做了修改

Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name')->unique();;
    $table->string('password', 60);
    $table->rememberToken();
    $table->timestamps();
});

创建好了注册节点

$api->post('/users/{user_name}/register','UserController@register');

实现具体逻辑前我们对User Model稍稍修改一下,删去了不用的email相关字段

回到UserController,我们实现最基本的注册逻辑,

<?php

namespace App\Http\Controllers\Api\V1;

use App\User;
use App\Http\Requests;
use Illuminate\Http\Request;
use Dingo\Api\Routing\Helpers;
use App\Http\Controllers\Controller;

class UserController extends Controller {

    use Helpers;

    public function register($user_name, Request $request)
    {
        if (User::where('name', $user_name)->count() > 0) {
            $this->response->errorBadRequest('该用户已经存在');
        }
        if (!preg_match('/^[a-z0-9]{6,15}$/', $user_name)) {
            $this->response->errorBadRequest('用户名由6-15位小写字母和数字组成');
        }
        $password = $request->json('password');
        $user = new User();
        $user->name = $user_name;
        $user->password = bcrypt($password);
        $user->save();
        return $this->response->created("/users/" . $user_name);
    }
}

因为是演示程序,我只做了很简单的数据校验,如果用户存在的或不符合用户名规范都使用dingo的helper链式调用errorBadRequest返回400相应码告诉客户端这是个错误的请求

我们注册一个用户试试

注册失败

因为我的dingo开启了debug模式所以返回的json包含了debug字段

我们把H换成小写

注册成功

成功注册用户,返回201 created相应码并在header出现了新建资源的location

鉴权

简单实现了注册逻辑后我们该实现鉴权的逻辑

关于RESTful的鉴权方式常见的有以下几种

  • HTTP basic authentication

这种鉴权方式最常见的地方是登录无线路由器配置时弹出的帐号登录对话框,其密码仅仅通过base64简单编码后明文传输,如果不使用https加密传输极其不安全,不建议

  • Custom 自定义

顾名思义自己定义一套自己的授权方式,客户端和服务端通过此方式交互

  • OAuth 2.0

关于OAuth2.0可以主要参考rfc6749,Oauth2.0的workflow可以由下图表示

Oauth2.0 workflow

Oauth2.0 简单的说就是一个关于授权的标准,在世界范围广泛应用

在整个授权过程中至少有三个参与者参与:

Client即被授予权限的客户,

Resouce Owner即资源所有者,

Authorization Server授权服务器,

Resource Server 资源服务器,可以和Resource Owner为同一服务器

我们以常见的论坛使用QQ号登录为例

当我们进入某个论坛又不想注册帐号的时候我们会发现大多数论坛有使用QQ号登录的选项,这个过程就是一个Oauth的过程,

我们假设用户小王想查看某摄影论坛的高清图片,但是小王不想注册帐号便使用QQ号登录,整个过程与workflow的对应如下

A:点击登录按钮请求登录的过程

B:服务器接受登录请求返回登录框

C:输入帐号密码点击登录

D:帐号密码无误,返回Access Token

E:论坛带上Access Token向腾讯服务器请求用户头像,用户昵称等受保护资源

F:服务器验证Access Token是否有效,如果无误返回受保护的资源

这个OAuth授权到此结束

可以说OAuth主要应用在web程序为第三方应用短期授权上,为第三方应用访问保护资源提供了可靠安全的鉴权方式

依我的经验大多数服务器对自己应用的授权可以说是OAuth的精简版

即砍掉A B过程

#参考资料 secure-your-rest-api-right-way

关于JWT

现在我们要实现C D 来完成授权,“权“体现在哪呢,当然是Access Token上,OAuth并没有强制规定token如何生成采用何种算法 ,但是比较通用的做法是采用JWT(json web token,参考rfc7519)

我们先来看看jwt长什么样子,嗯,就是下面这一长串的符号

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyOTEwMjYiLCJpc3MiOiJodHRwOlwvXC8xMzkuMTI5LjkzLjU4XC9hcGlcL3VzZXJcLzI5MTAyNlwvYXV0aCIsImlhdCI6MTQ1NjA2OTQ4MSwiZXhwIjoxNDU2Njc0MjgxLCJuYmYiOjE0NTYwNjk0ODEsImp0aSI6ImQ2YzIwYTUzNTg5OTJlN2M1ZmVjYWI0ODFhOTRlZDQwIn0.V0ILcwzI47AkoEccMnxo3-mnR4MdrVlyFaZy_t78nys

稍微仔细看大家可以找到3个  .   符号,这个3个.将jwt分成了3个部分

  • Header
  • Payload
  • Signature

分别用base64编码后拼接构成了jwt

Header主要阐述了token类型和加密算法

{
"typ": "JWT",
"alg": "HS256"
}

jwt通常使用HMACSHA256或者RSA公私钥对加解密

Payload段主要存储事先定义好的声明(claims),但是有几个字段是jwt推荐保留的,他们分别是

iss (issuer),

exp (expiration time),

sub (subject),

aud (audience)

其他的可以自定义

最后是Signature

签名的算法如下,以HMAC算法为例

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

一个JWT就这样生成了

至于我们使用jwt的workflow就是我刚才说的精简版的Oauth

jwt-diagram

在Dingo中集成JWT

扯了一堆概念,现在我们要着手实现JWT,我们当然不用重新造轮子,Dingo兼容的JWT实现我们将它配置好(jwt-auth)集成进去,具体方法参加项目wiki

我们在config/api.php配置文件中加上

'jwt' => 'Dingo\Api\Auth\Provider\JWT'

或者在ServiceProvider或bootstrap文件中加入

app('Dingo\Api\Auth\Auth')->extend('jwt', function ($app) {
    return new Dingo\Api\Auth\Provider\JWT($app['Tymon\JWTAuth\JWTAuth']);
});

修改一下routes.php

$api = app('Dingo\Api\Routing\Router');
$api->version('v1', function ($api) {
    $api->group(['namespace' => 'App\Http\Controllers\Api\V1'], function ($api) {
        $api->post('/users/{user_name}/register', 'UserController@register');
        $api->post('/users/{user_name}/auth','UserController@auth');
        $api->group(['middleware' => 'api.auth'], function ($api) {
            $api->get('/users/{user_name}/', 'UserController@showUserInfo');
            $api->get('/users/{user_name}/secrets','UserController@showUserSecrets');
            $api->post('/users/{user_name}/secrets','UserController@postSecret');
            $api->get('/secrets/{secret_id}/comments','SecretController@showSecretComments');
            $api->post('/secrets/{secret_id}/comments','SecretController@postSecretComment');
        });
    });
});

 

篇幅原因,我们主要实现一下鉴权的逻辑,大家可以抽空实现剩下的业务逻辑

    public function auth(Request $request, $user_name)
    {
        $password = $request->json('password');
        $credentials = ['password' => $password, 'name' => $user_name];
        try {
            if (!$access_token = JWTAuth::attempt($credentials)) {
                $this->response->errorUnauthorized('帐号或密码错误');
            }
            return $this->response->array(['access_token' => $access_token]);
        } catch (JWTException $e) {
            $this->response->errorInternal('暂时无法生成token');
        }
    }

credentials即鉴权所需的凭证在这里就是一个数组键值对,对应我们的数据库模型就是用户名和密码,键值一定是和数据库相应字段一一对应的

jwt-auth库会自动的通过数据库校验该条记录是否存在

如果存在则返回对应的access_token,至于返回格式客户端和服务端事先商量好

我事先在数据库中插入了一条用户记录,我们直接通过curl 来试试

curl -X POST "http://localhost:8000/api/users/helloworld/auth" -d '{"password":"helloworld"}'

返回如下

{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDAwXC9hcGlcL3VzZXJzXC9oZWxsb3dvcmxkXC9hdXRoIiwiaWF0IjoxNDU3MjQ0NzIyLCJleHAiOjE0NTcyNDgzMjIsIm5iZiI6MTQ1NzI0NDcyMiwianRpIjoiZDBhZjA4ZmFjZjJmMDQ2ZDRmNTY4OWI2MzgyZWRiNTMifQ.m20CfmrpajEGcRqD-SIDyqkDe-95M3WUyorTEYT-360"}

成功获取access_token,如果我们稍微修改一下密码呢

拿到了401响应码和对应的message,由于debug信息很长我就不复制了

接下来,我们要通过token访问其他在api.auth中间件保护下的接口

token 通过http header Authorization: Bearer 字段传送到服务器端

curl -X GET -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEsImlzcyI6Imh0dHA6XC9cL2xvY2FsaG9zdDo4MDAwXC9hcGlcL3VzZXJzXC9oZWxsb3dvcmxkXC9hdXRoIiwiaWF0IjoxNDU3MjQ0NzIyLCJleHAiOjE0NTcyNDgzMjIsIm5iZiI6MTQ1NzI0NDcyMiwianRpIjoiZDBhZjA4ZmFjZjJmMDQ2ZDRmNTY4OWI2MzgyZWRiNTMifQ.m20CfmrpajEGcRqD-SIDyqkDe-95M3WUyorTEYT-360 " -i  "http://localhost:8000/api/users/helloworld/"

我简单的返回了该条用户的json

{"user":{"id":1,"name":"helloworld","created_at":"2016-02-27 05:51:17","updated_at":"2016-02-27 05:51:17"}}

之后需要受保护的接口都需要带上token才能访问

暂时先写到这,dingo还有许多实用的功能,以后有空再来补充