Thinkphp5实现接口开发

  • 2018-06-16
  • 0
  • 0

Thinkphp5系列教程

时隔2个月,由于工作的事情太忙抽不出空,年底前的计划就是再出一套API接口开发的教程,上一篇教程主要讲的是开发一个简单的博客系统.此套教程我决定来说一下api接口开发.本套教程会围绕下面几个知识点做一个开发

这篇文章是17年底完成的.其中演示代码大量使用Db类来直接操作数据库.是一种不好的写好.我会在之后的文章来说明。经过半年的精进,在回过头看之前写的代码.很多瑕疵.

这套教程围绕的几个点:

* APP开发接口流程

* Token 生成及其验证

* Restful API 思想

* API数据安全

* API 异常处理 及 性能检测

* APP接入支付宝

* API 如何接收客户端上传的数据

本套教程的目录如下:
[TOC]


如何用PHP开发API接口

做过 API 的人应该了解,其实开发 API 比开发 WEB 更简洁,但可能逻辑更复杂,因为 API 其实就是数据输出,不用呈现页面,所以也就不存在 MVC(API 只有 M 和 C)

1、和 WEB 开发一样,首先需要一些相关的参数,这些参数,都会由客户端传过来,也许是 GET 也许是 POST,这个需要开发团队相互之间约定好,或者制定统一规范。

2、有了参数,根据应用需求,完成数据处理,例如:任务进度更新、APP内购、一局游戏结束数据提交等等

3、数据逻辑处理完之后,返回客户端所需要用到的相关数据,例如:任务状态、内购结果、玩家信息等等

数据怎么返给客户端?

直接输出的形式,如:JSON、xml、TEXT 等等。

4、客户端获取到你返回的数据后,在客户端本地和用户进行交互

什么是接口?接口用来做什么?

接口通过上面的简单介绍,可以理解为API就是就是通道,负责一个程序和其他软件的沟通,本质是预先定义的函数。

接口将数据给客户端,由客户端渲染在页面上,大部分的逻辑都是在服务端进行判断和验证.客户端只需要管请求API的这个人有没有权限?

那我们如何验证请求这个API是否有权限.这个时候就需要了解 token 验证的思想。

什么是OAuth?

OAuth是一个授权的标准,目前最新的版本是2.0

那什么是 OAuth呢?我们举个栗子~

有一个网站提供了一个url接口,这个接口我们只给通过认证的用户的使用。给出 了这样一条接口:http://www.xxx.com/api.php?username=admin&password=admin
很容易就会发现暴露出来的几点问题!

这条API一但被其他人知道,就容易被无限制的盗刷,无法控制这个接口的使用时间,而且通过地址传输的是明文很不安全。

那如何能避免这种问题呢?

我们可以设置一个授权层,用户不能直接通过账号密码去登录,只能通过授权层来获取一个 令牌(Token) , 通过获取的Token来与我们服务端进行其他接口的验证.

如何实现授权层,完成Token的生成及其验证

我们来想一想,用户什么时候获取Token?什么时候需要Token呢?

在第一次访问接口的时候 我们不可能平白无故的捏造出一个Token.我们需要在用户请求登录接口成功后返回一个Token,Token里保存了用户的信息

咦.这和我们开发混排的web站的时候一样啊.登录成功将信息存入Session,我们参考一下之前的代码

public function login()
{
    // 判断是否为post请求,如果为post请求就是登录请求 为get就是访问登录页面
    if (Request()->isPost()) {
        // 获取数据库中数据 如果获取到则是存在这条记录
        $res = \think\Db::table('admin')->where('u_name', input('post.u_name'))->where('u_password', md5(input('post.u_password')))->find();
        // 存在就返回数据集 所以判断是否存在此数据集
        if (!$res) {
            // 登录失败
            $this->error('账号或密码错误 请检查后 再次输入');
        } else {
            // 登录成功,将登录后的数据存入session 在Base类中用于判断是否登录的条件
            session('admin.admin_id', $res['id']);
            session('admin.admin_name', $res['u_name']);
            $this->success('登录成功', 'admin/entry/index');
        }
    }
    return $this->fetch();
}

此时我们只需要改写 else部分的代码!

将存入session的代码 改成存入数据库或者缓存系统的代码即可.我们将Key返回给用户即可,下次请求的时候 用户根据这个Key查询它的Value,获取用户信息进行操作.

那么来了一个问题!为什么不能继续存入Session了.

session是基于cookie的,cookie是由浏览器来接受处理的,当然听App端的小伙伴说App也可以处理Cookie,但是没有Token运用的稳定.

现在我们改写代码,改成下面样子:

public function login()
{
    // 判断是否为post请求,如果为post请求就是登录请求 为get就是访问登录页面
    if (Request()->isPost()) {
        // 获取数据库中数据 如果获取到则是存在这条记录
        $res = \think\Db::table('admin')->where('u_name', input('post.u_name'))->where('u_password', md5(input('post.u_password')))->find();
        // 存在就返回数据集 所以判断是否存在此数据集
        if (!$res) {
            // 登录失败
            return json(['code'=>400,'msg'=>'登录失败','data'=>[]]);
        } else {
            // 登录成功,将登录后的数据存入session 在Base类中用于判断是否登录的条件
            $token = md5(microtime()); // 这里的生成算法只作用于Demo
            cache($token,$res['id'],24*60*7);
            return json(['code'=>200,'msg'=>'登录成功','data'=>['Token'=>$token]]);
        }
    }

}

通过两个代码对比发现:

  • 缺少了View的部分,直接将数据转为Json格式返回给客户端.

  • 原本的Session存储变为了Cache存储.

通过这个登录的小栗子,希望大家能了解什么是接口.后面的课程会更加精彩!

Restful Api思想

网络应用程序,随着时代的变迁.之前的网页都是前后端混排的.

现在随着前端设备层出不穷,为了统一接口 于是便有了Restful Api

这套规范总结: 就是用URL定位资源,用HTTP描述操作。

那如何理解这段话呢?

我们通过一个小栗子实现。我们要做一个相册的功能 那如何用Restful定义url

动作 URI 行为
GET /photos 显示相册内容列表
GET /photos/create 相片上传页面
POST /photos 上传相片操作
GET /photos/{id} 通过ID查看相片
GET /photos/{id}/edit 通过ID编辑相片页面
PUT/PATCH /photos/{id} 通过ID上传相片操作
DELETE /photos/{id} 通过ID删除相片

通过上面定义路由的方式就是Restful 思想。

API开发- 让异常显示的更加优雅

作为程序员难免会出点小BUG!哪如何捕获呢。在APP上出现bug通常会出现闪退,和无法解析错误一直加载.

有一个想法。将错误也变成json格式.code码定义为500 如果移动端发现错误为500的话 就温柔提醒.并且服务端保存错误信息.供开发者修改.

首先修改配置项 application/config.php

// 异常处理handle类 留空使用 \think\exception\Handle
'exception_handle'       => '\app\common\exception\Http',

原本是留空的 现在改为我们自定义的控制器

创建一个Http控制器 继承thinkexceptionHandle类 重写 render方法. 这里注意一点 最好不要用框架里的一些方法了.这个文件的启动顺序大于一些方法.

<?php

namespace app\common\exception;

use app\api\controller\Log;
use Exception;
use think\exception\Handle;
use think\exception\HttpException;

class Http extends Handle
{

    public function render(\Exception $e)
    {

        // 只要有错误就返回错误json
        $arr = [
            'code' => 500,
            'msg' => $e->getMessage(),
            'data' => 'URL : http://'.$_SERVER['SERVER_NAME'].':'.$_SERVER["SERVER_PORT"].$_SERVER["REQUEST_URI"]
        ];
        $error_info = json_encode($arr, 512) . PHP_EOL;
        echo $error_info;
        if (!is_dir('../runtime/errorlog/')) mkdir('../runtime/errorlog/', 0777, true);


        file_put_contents('../runtime/errorlog/' . date('Ymd', time()) . '.txt', $error_info, FILE_APPEND);
        exit;
    }
}

这样就能将原本的报错页面变成可识别的json串.并且将错误的日志记录在 runtime/errorlog 目录下。

如何接收客户端上传的数据

上传的接收方法有很多.

TP5 已经帮我们封装好了 FILE文件上传。我们可以直接调用它的方法即可.

下面有file文件上传的demo:

public function upload(){
    // 获取表单上传文件 例如上传了001.jpg
    $file = request()->file('image');

    // 移动到框架应用根目录/public/uploads/ 目录下
    if($file){
        $info = $file->move(ROOT_PATH . 'public' . DS . 'uploads');
        if($info){
            // 成功上传后 获取上传信息

            // 输出 20160820/42a79759f284b767dfcb2a0197904287.jpg
            $path =  $info->getSaveName();

            return json(['code'=>200,'msg'=>'上传成功','data'=>['path'=>$path]]);
        }else{
            return json(['code'=>400,'msg'=>'上传失败']);
        }

    }
}

当然还有另外一种方式上传文件到服务器 那就是直接发送文件流:

public function byteUpload()
{
    // 生成的文件格式
    $filename = md5(microtime().mt_rand(10000,99999)) . '.jpeg';
    // 通过POST接收文件 , 将文件的字节流放在file参数内传入即可
    $byte = $_POST['file'];
    $dir = ROOT_PATH . 'public' . DS . 'uploads' . DS . date('Ymd');
    $path = $dir . DS . $filename;
    is_dir($dir) || mkdir($dir, 0777, true);
    if ($byte) {
        // 将字节流写入文件
        file_put_contents($path, $byte);//写入文件中!
    } else {
        return false;
    }
    // 上传成功后 返回文件路径 如果失败则返回false
    return $path;
}

PS:本例子只是用来演示 不能直接用于API需要修改返回的格式为json .

这种方式也是最好理解的一种,直接把文件通过字节的方式传入.我们后台直接保存即可.

APP接入支付宝支付

对于没有接入过支付的小伙伴,我先说几句!

不要畏惧接触第三方,第三方的调用可以说是相对比较简单的,他们会把大部分的逻辑封装在sdk中,我们只要调用接口.一般正规的第三方接口都会有说明文档。也不要畏惧读文档,耐下心来仔细看一篇.其实也就是那么回事

我们现在开始给自己的APP接入支付宝

  • 首先我们了解下支付宝的支付接口调用原理
sequenceDiagram
participant 用户
participant 商户客户端
participant 支付宝客户端
participant 支付宝服务端
participant 商户服务端

用户->> 商户客户端: 1. 使用支付宝付款
商户客户端-->> 商户服务端: 2. 请求客户服务端,返回签名后的订单信息
商户服务端-->> 商户客户端: 3.返回签名后的订单信息
商户客户端->> 支付宝客户端: 4.由移动端去调用支付宝接口
支付宝客户端->>支付宝服务端: 5.支付请求
支付宝服务端->>支付宝服务端:6.支付完成
支付宝服务端->>支付宝客户端:7.返回同步支付结果
支付宝客户端->>商户客户端:8.接口返回支付结果
商户客户端-->>商户服务端:9.同步支付结果返回服务端,解析支付结果
商户服务端-->>商户客户端:10.返回最终结果
商户客户端->>用户:11.显示结果
支付宝服务端-->>商户服务端:12.异步发送支付结果
商户服务端-->>支付宝服务端:13.接收响应(支付宝确认支付)

其中虚线的就是我们要处理的步骤 我们下面将一步一步来介绍如何操作

  • 首先我们先要去申请支付应用,获取APPid、rsaPrivateKey 、alipayrsaPublicKey

    怎么开通应用?这一步骤还是看官方手册吧!官方的手册才是最详细的 手册地址

    请确保账号有开通APP支付的权限,下面我们来配置一下开发环境,在我们创建的支付应用管理页面里面

    应用网关 填写你服务器的域名:http://www.webhuang.cn
    授权回调地址 填写支付宝给你异步回调的地址

    后面还有接口加密方式要填写,我们可以通过下载支付宝官方提供的工具生成公钥和私钥。这里官方手册也说的很详细了.

    官方生成签名工具地址:下载地址

    生成秘钥的格式按照地址中的图生成即可,最后将生成的应用公钥上传到平台即可。

  • 将SDK下载放置在TP5的vendor目录下的alipay文件夹(可根据实际使用框架技术进行实际调整)。

    SDK下载地址:下载地址

  • 用户点击付款按钮的时候,客户端会请求我们,我们需要返回一个签名的信息给客户端,再由客户端通过支付宝SDK传给支付宝的服务端

    //vendor();为TP5框架的方法,作用:导入第三方框架类库
    
    vendor('alipay.aop.AopClient');
    
    vendor('alipay.aop.request.AlipayTradeAppPayRequest');
    
    //实例化支付接口
    
    $aop = new \AopClient();
    
    $aop->gatewayUrl = "https://openapi.alipay.com/gateway.do"; //支付宝网关
    
    $aop->appId = “应用ID,填写你的APPID”;
    
    $aop->rsaPrivateKey = "商户私钥,您的原始格式RSA私钥()";
    
    $aop->alipayrsaPublicKey = "支付宝公钥";
    
    $aop->apiVersion = '1.0';
    
    $aop->signType = "签名方式,如 RSA2 ";
    
    $aop->postCharset = 'UTF-8';
    
    $aop->format = "json";
    
    //实例化具体API对应的request类,类名称和接口名称对应,当前调用接口名称:alipay.trade.app.pay
    
    $appRequest = new \AlipayTradeAppPayRequest();
    
    //SDK已经封装掉了公共参数,这里只需要传入业务参数
    
    $bizcontent = json_encode([
    
         'body' => '余额充值', //订单描述
    
         'subject' => '充值', //订单标题
    
         'timeout_express' => '30m',
    
         'out_trade_no' => ‘20170125test01’, //商户网站唯一订单号
    
         'total_amount' => '0.01', //订单总金额
    
         'product_code' => 'QUICK_MSECURITY_PAY', //固定值
    
    ]);
    
    $appRequest->setNotifyUrl($url); //设置异步通知地址
    
    $appRequest->setBizContent($bizcontent);
    
    //这里和普通的接口调用不同,使用的是sdkExecute
    
    $response = $aop->sdkExecute($appRequest);
    
    echo $response;//就是orderString 可以直接给客户端请求,无需再做处理。
    

通过上面的代码就可以生成一个签名的订单信息,供客户端去调用支付宝,发起支付请求。

  • 当用户支付成功,会异步的向我们服务器请求一条接口,只有我们返回 success 支付宝才认可已经付款成功,不然就会一直请求.返回fail 则说明我们不承认这次请求。
    /**
     * 接收支付宝推送的支付结果,并按照其要求返回需求内容
     */
    public function rebackPayResult(){
        vendor('alipay.aop.AopClient');
        vendor('alipay.aop.request.AlipayTradeAppPayRequest');
        $conf = config('alipay');
        $data = $_POST;
        $aop = new \AopClient;
        $aop->alipayrsaPublicKey = $conf['alipayrsaPublicKey'];
        $flag = $aop->rsaCheckV1($data, NULL, "RSA2");

        //当支付宝回调回来,先根据订单号查询一下订单状态,如果支付已经成功,直接return success ,就不用再往下执行了,防止支付宝那边回调出错,不停的异步调用接口
        $recharge_info = (new InternalLogic())->getOrderInfoByOrdersId($data['out_trade_no']);
        if ($recharge_info['status'] == 1) {
            echo 'success';
            die;
        }

        if($flag){
            //验证成功
            //这里可以做一下你自己的订单逻辑处理
            if ($data['trade_status'] == 'TRADE_SUCCESS'){
                try
                {
                    $alipay_result = (new InternalLogic())->storeOrdersStatusAndEvent($data['out_trade_no'],1,'支付宝','收到支付宝回执', $recharge_info['uid']);
                    if ($alipay_result) {
                        echo 'success';die;
                    } else {
                        echo 'fail';die;
                    }
                }
                catch(\Exception $e)
                {
                    echo "fail";
                }
            }else{

                echo "fail";
            }

        } else {
            //验证失败
            echo "fail";
        }
        //$flag返回是的布尔值,true或者false,可以根据这个判断是否支付成功
    }

这样一次支付的流程就走完了.其中代码为学习的思路。如果要在实战项目中使用,请仔细检查代码的强壮性!

评论

还没有任何评论,你来说两句吧