1. 开发环境
- PHP框架: ThinkPHP6
- 操作系统: MacOs
- Web服务器: Nginx
2. APP接口
2.1. 简介
客户端与服务端进行数据交互的通道, 大部分APP接口都是通过HTTP协议通信的
2.2. HTTP通信的三要素
- URL 通信地址
- Method 通信方式(GET|POST|PUT|DELETE)
- Params 通信的内容
2.3 APP接口的输入
客户端传递参数
- URL参数传递, 将参数拼接在URL后边, GET方式
- Request Body, 将参数放在Body体内, POST方式, 常见的传输格式Content-Type: multipart/form-data , application/x-www-form-urlencoded, json, 其中multipart/form-data , application/x-www-form-urlencoded会将变量以关联数组传入
服务端获取参数
- 获取GET方式的参数, 可以通过$_GET获取,
- 获取POST方式的参数, form-data与x-www-form-urlencoded格式通过$_POST获取, json格式通过file_get_contents(“php://input”)获取
- 对于body中提交的json对象, 你无需使用php://input去获取, 可以直接当做表单提交的数据使用,因为框架已经自动处理过了已经封装好
2.4 APP接口的输出
JSON是一种轻量级的数据交换格式
接口输出三要素
- code 错误码
- msg 错误码对应的描述
- data 接口返回的数据
封装接口JSON格式输出
在app下新建公共common文件夹, 在common新建lib文件夹用来存放一些公共类库文件
- 定义一个Class类, 在控制器引用后静态化调用
<?php
/*** 老王***/
namespace app\common\lib;
use app\common\lib\error\ApiErrDesc;Class ResponseJson{/*** @param null $data* @param int $httpCode* @return \think\response\Json*/public static function success($data = null, $httpCode = 200){$status = ApiErrDesc::SUCCESS[0];$message = ApiErrDesc::SUCCESS[1];return $this->jsonResponse($status, $message, $data, $httpCode);}/*** @param $status* @param $message* @param null $data* @param int $httpCode* @return \think\response\Json*/public static function error($status, $message, $data = null, $httpCode = 500){return $this->jsonResponse($status, $message, $data, $httpCode);}/*** @param $status* @param $message* @param $data* @param int $httpCode* @return \think\response\Json*/private function jsonResponse($status, $message, $data, $httpCode = 200){$result = ['status' => $status, // 业务状态码'message' => $message,'result' => $data];return json($result, $httpCode);}
}
控制器调用
<?php
/*** 老王***/namespace app\api\controller;
use app\BaseController;
use app\common\lib\ResponseJson;class Test extends BaseController
{public function index(){ResponseJson::success(['test' => 'test']);ResponseJson::error(1, '未知错误');}
}
- 定义一个Trait, 用户代码复用, 在控制器引用后, 可以像调用该控制器类的方法一样
<?php
/*** 老王***/
namespace app\common\lib;
use app\common\lib\error\ApiErrDesc;Trait ResponseJson{/*** @param null $data* @param int $httpCode* @return \think\response\Json*/public function success($data = null, $httpCode = 200){$status = ApiErrDesc::SUCCESS[0];$message = ApiErrDesc::SUCCESS[1];return $this->jsonResponse($status, $message, $data, $httpCode);}/*** @param $status* @param $message* @param null $data* @param int $httpCode* @return \think\response\Json*/public function error($status, $message, $data = null, $httpCode = 500){return $this->jsonResponse($status, $message, $data, $httpCode);}/*** @param $status* @param $message* @param $data* @param int $httpCode* @return \think\response\Json*/private function jsonResponse($status, $message, $data, $httpCode = 200){$result = ['status' => $status, // 业务状态码'message' => $message,'result' => $data];return json($result, $httpCode);}
}
控制器调用
<?php
/*** 老王***/namespace app\api\controller;
use app\BaseController;
use app\common\lib\ResponseJson;class Test extends BaseController
{use ResponseJson;public function index(){$this->success(['test' => 'test']);$this->error(1, '未知错误');}
}
3. APP接口的鉴权
客户端要带着凭证来调用APP接口, 即有权限才可以调用

3.1传统web的cookie session

web是基于浏览器, 可以采用该方式, 但是APP不是基于浏览器cooike和session的机制, 需要采用JWT
3.2 JWT (Json Web Token)
JWT原理即服务端认证以后, 生成一个JSON对象, 返回给客户端, 后续客户端所有的请求都必须带上这个JSON对象, 而服务端依靠这个JSON来认定用户身份
完整的JWT格式输出是以 . 分割的三段Base64编码

组成
- header, 通常包含两部分: 类型和采用的加密算法, header需要经过Base64Url编码后作为JWT的第一部分
- payload, 载体, 包含了claim, iss: 签发者, exp: 过期时间戳, sub: 面向的用户, aud: 接收方, iat: 签发时间, 可以存放不敏感的信息, payload需要经过Base64Url编码后作为JWT的第二部分
- signature, 创建签名, 使用编码后的header和payload以及一个密钥, 使用header中指定的签名算法进行签名, 生成的签名作为JWT的第三部分, 该签名是在服务端完成的, 客户端不知道密钥, 更安全
3.3 代码封装JWT的使用
JWT的官网 JWT
找到PHP的库, 利用composer (PHP用来管理依赖关系的工具) 下载包文件lcobucci/jwt
切换到项目目录执行composer
composer require lcobucci/jwt
在app/common/auth文件下, 创建JWT封装类库文件
<?php
/*** 老王***/namespace app\common\lib\auth;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer\Hmac\Sha256;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\ValidationData;/*** 单例模式* 一次请求中所有使用到jwt的地方都是一个用户* Class JwtAuth* @package app\common\lib\auth* 1. 接口鉴权* 2. 获取用户身份*/
class JwtAuth
{/*** @var*/private $token;/*** @var*/private $decodeToken;/*** claim iss* @var string*/private $iss = 'chao.com';/*** claim aud* @var string*/private $aud = 'tp6_server_app';/*** 身份 uid* @var string*/private $uid;/*** @var string*/private $secret = 'TP6&*chao1992#$LJL*&^&*9089';/*** @var null*/private static $instance = null;/** 私有化 构造函数*/private function __construct(){}/***/private function __clone(){// TODO: Implement __clone() method.}/*** 单例模式 获取jwtAuth句柄* @return JwtAuth|null*/public static function getInstance(){if (is_null(self::$instance)){self::$instance = new self();}return self::$instance;}/*** @param $uid* @return $this* 设置身份信息*/public function setUid($uid){$this->uid = $uid;return $this;}/*** @param $token* 设置token*/public function setToken($token){$this->token = $token;return $this;}/*** @param $token*/public function decode(){if (!$this->decodeToken){// 把字符串转成Token对象$this->decodeToken = (new Parser())->parse((string)$this->token);$this->uid = $this->decodeToken->getClaim('uid');}return $this->decodeToken;}/*** 校验signature, 判断token是否过期或者被篡改* @return bool*/public function verify(){$signer = new Sha256();$privateKey = new Key($this->secret);$result = $this->decode()->verify($signer, $privateKey);return $result;}/*** 校验参数* @return bool*/public function validate(){$data = new ValidationData();$data->setIssuer($this->iss);$data->setAudience($this->aud);return $this->decode()->validate($data);}/*** @return $this*/public function encode(){$signer = new Sha256();$privateKey = new Key($this->secret);$time = time(); // 颁发时间$this->token = (new Builder())->withHeader('alg', 'HS256')->issuedBy($this->iss)->permittedFor($this->aud)->issuedAt($time)->expiresAt($time + 3600) // 过期时间->withClaim('uid', $this->uid) // 自定义参数->getToken($signer, $privateKey);return $this;}/*** 获取token* @return string*/public function getToken(){return (string)$this->token;}/*** @return string*/public function getUid(){return $this->uid;}}
控制器调用
<?php
/*** 老王***/namespace app\api\controller;
use app\BaseController;
use app\common\lib\auth\JwtAuth;
use app\common\lib\ResponseJson;class Test extends BaseController
{use ResponseJson;public function index(){// 获取jwtAuth的句柄$jwtAuth = JwtAuth::getInstance();$token = $jwtAuth->setUid(1)->encode()->getToken();$this->success(['token' => $token]);}
}
3.4 中间件
中间件主要用于拦截或者过滤应用的HTTP请求, 并进行必要的业务处理
- 前置中间件: 请求时不会先去执行到控制器的某个方法, 而是先经过中间件, 然后再到某个控制器的某个方法去执行代码,如果中间件有拦截, 流程就会在中间件截断, 不会再往下执行方法, 而且获取不到控制器和方法, 因为会先执行中间件, 再执行控制器的方法
- 后置中间件: 访问某个控制器的方法, 会先去执行方法里面的代码内容(不返回输出), 然后执行中间件的代码, 最后才是方法的输出返回, 如果中间件有拦截, 流程就会在中间件内截断, 但是方法内的代码也在前面执行了, 可以获取控制器和方法, 因为会先去执行这个控制器的方法, 再执行中间件
在app/api模块下, 新建middleware文件夹, 然后在该文件夹下创建类
<?php
/*** 老王***/namespace app\api\middleware;use app\api\exception\ApiException;
use app\common\lib\auth\JwtAuth;
use app\common\lib\error\ApiErrDesc;
use app\common\lib\ResponseJson;/*** Class CheckAuth* @package app\api\middleware* 中间件*/
class CheckAuth
{/*** @param $request* @param \Closure $next*/public function handle($request, \Closure $next){$token = $request->header('token');if ($token){// 校验$jwtAuth = JwtAuth::getInstance();$jwtAuth->setToken($token);if ($jwtAuth->validate() && $jwtAuth->verify()){return $next($request);}else{throw new ApiException(ApiErrDesc::ERR_LOGIN);}}else{throw new ApiException(ApiErrDesc::ERR_PARAMS);}}
}
注册中间件
在app/api模块下, 新建middleware.php文件
<?php
// 该模块下的中间件定义文件, 对该模块下的所有控制器都有效, 在middleware下写好中间件后, 需要在该文件下配置绑定
// 如果使得该中间件只针对某一个控制器有效, 可以借助路由来设置, 不需要在该文件下配置绑定, 直接在middleware下写好中间件然后通过路由绑定->middleware()
return [app\api\middleware\CheckAuth::class
];
4. 业务逻辑异常处理
4.1 错误码
错误码是用来描述当前接口处理的结果, 是前后端共同的约束
格式:
- code 错误码
- msg 错误码对应的描述
一般以配置文件( config/status.php )或者类的常量来定义错误码, 统一管理便于维护
在app/common/lib文件夹下新建error, 并在该文件夹下创建类库
<?php
/*** 老王***/namespace app\common\lib\error;/*** Class ApiErrDesc* @package app\common\lib\error* APi返回码类库*/
class ApiErrDesc
{/*** Api通用错误码* error_code < 1000*/const SUCCESS = [1, 'Success'];const UNKNOWN_ERR = [0, '未知错误'];const ERR_URL = [2, '请求接口不存在'];const ERR_PARAMS = [100, '参数错误'];/*** 用户登录相关的错误码* error_code 1000-1100*/const UNKNOWN_USER = [1001, '用户不存在'];const ERR_PASSWORD = [1002, '密码错误'];const ERR_LOGIN = [1000, '登录过期'];}
4.2 业务异常
try…catch 可以捕获异常
set_exception_handler set_error_handler设置用户自定义的异常处理函数, 用于没有用try/catch块来捕获的异常
api模块是与客户端交互的接口类库, 出现异常需要按照接口的输出格式返回给客户端, 便于客户端进行操作, TP6中app/ExceptionHandler.php是系统自带的应用异常处理类, 通过页面形式输出错误信息, 但这种并不适用api接口
所以需要在app/api/exception文件夹下新建异常处理的类
自定义接收异常并格式化输出类
<?php
/*** 老王***/namespace app\api\exception;
use think\exception\Handle;
use think\Response;
use Throwable;
use app\common\lib\ResponseJson;
use app\common\lib\error\ApiErrDesc;
/*** Class ApiHandler* @package app\api\exception* api模块下的异常处理类*/
class ApiHandle extends Handle
{protected $httpCode = 500;/*** Render an exception into an HTTP response.** @access public* @param \think\Request $request* @param Throwable $e* @return Response*/public function render($request, Throwable $e): Response{if ($e instanceof ApiException){$status = $e->getCode();$message = $e->getMessage();}else{$status = $e->getCode();if(!$status || $status < 0){$status = ApiErrDesc::UNKNOWN_ERR[0];}$message = $e->getMessage() ?: ApiErrDesc::UNKNOWN_ERR[1];}// 添加自定义异常处理机制if (method_exists($e, 'getStatusCode')){$this->httpCode = $e->getStatusCode();}return ResponseJson::error($status, $message, null, $this->httpCode);}}
自定义抛出异常的类
<?php
/*** 老王***/namespace app\api\exception;use think\Exception;
use Throwable;class ApiException extends Exception
{public function __construct(array $apiErrConst, Throwable $previous = null){$message = $apiErrConst[1];$code = $apiErrConst[0];parent::__construct($message, $code, $previous);}
}
后边在控制器中返回错误错误信息, 就可以直接调用ApiException抛出异常, 再由ApiHandle中的handle方法接管异常信息, 并格式化输出















