Thinkphp5实现接口开发
Contents
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,可以根据这个判断是否支付成功
}
这样一次支付的流程就走完了.其中代码为学习的思路。如果要在实战项目中使用,请仔细检查代码的强壮性!
发表评论