TP6+JWT开发APP接口

1. 开发环境
  1. PHP框架: ThinkPHP6
  2. 操作系统: MacOs
  3. Web服务器: Nginx
2. APP接口
2.1. 简介

客户端与服务端进行数据交互的通道, 大部分APP接口都是通过HTTP协议通信的

2.2. HTTP通信的三要素
  1. URL 通信地址
  2. Method 通信方式(GET|POST|PUT|DELETE)
  3. Params 通信的内容
2.3 APP接口的输入

客户端传递参数

  1. URL参数传递, 将参数拼接在URL后边, GET方式
  2. Request Body, 将参数放在Body体内, POST方式, 常见的传输格式Content-Type: multipart/form-data , application/x-www-form-urlencoded, json, 其中multipart/form-data , application/x-www-form-urlencoded会将变量以关联数组传入

服务端获取参数

  1. 获取GET方式的参数, 可以通过$_GET获取,
  2. 获取POST方式的参数, form-data与x-www-form-urlencoded格式通过$_POST获取, json格式通过file_get_contents(“php://input”)获取
  3. 对于body中提交的json对象, 你无需使用php://input去获取, 可以直接当做表单提交的数据使用,因为框架已经自动处理过了已经封装好
2.4 APP接口的输出

JSON是一种轻量级的数据交换格式
接口输出三要素

  1. code 错误码
  2. msg 错误码对应的描述
  3. data 接口返回的数据

封装接口JSON格式输出
在app下新建公共common文件夹, 在common新建lib文件夹用来存放一些公共类库文件

  1. 定义一个Class类, 在控制器引用后静态化调用

/**
 * 老王
 *
 **/
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);
    }
}

控制器调用


/**
 * 老王
 *
 **/

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, '未知错误');
    }
}
  1. 定义一个Trait, 用户代码复用, 在控制器引用后, 可以像调用该控制器类的方法一样

/**
 * 老王
 *
 **/
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);
    }
}

控制器调用


/**
 * 老王
 *
 **/

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接口, 即有权限才可以调用
TP6+JWT开发APP接口_第1张图片

3.1传统web的cookie session

TP6+JWT开发APP接口_第2张图片
web是基于浏览器, 可以采用该方式, 但是APP不是基于浏览器cooike和session的机制, 需要采用JWT

3.2 JWT (Json Web Token)

JWT原理即服务端认证以后, 生成一个JSON对象, 返回给客户端, 后续客户端所有的请求都必须带上这个JSON对象, 而服务端依靠这个JSON来认定用户身份
完整的JWT格式输出是以 . 分割的三段Base64编码
TP6+JWT开发APP接口_第3张图片
组成

  1. header, 通常包含两部分: 类型和采用的加密算法, header需要经过Base64Url编码后作为JWT的第一部分
  2. payload, 载体, 包含了claim, iss: 签发者, exp: 过期时间戳, sub: 面向的用户, aud: 接收方, iat: 签发时间, 可以存放不敏感的信息, payload需要经过Base64Url编码后作为JWT的第二部分
  3. signature, 创建签名, 使用编码后的header和payload以及一个密钥, 使用header中指定的签名算法进行签名, 生成的签名作为JWT的第三部分, 该签名是在服务端完成的, 客户端不知道密钥, 更安全
3.3 代码封装JWT的使用

JWT的官网 JWT
找到PHP的库, 利用composer (PHP用来管理依赖关系的工具) 下载包文件lcobucci/jwt
切换到项目目录执行composer

composer require lcobucci/jwt

在app/common/auth文件下, 创建JWT封装类库文件


/**
 * 老王
 *
 **/

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;
    }

}

控制器调用


/**
 * 老王
 *
 **/

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请求, 并进行必要的业务处理

  1. 前置中间件: 请求时不会先去执行到控制器的某个方法, 而是先经过中间件, 然后再到某个控制器的某个方法去执行代码,如果中间件有拦截, 流程就会在中间件截断, 不会再往下执行方法, 而且获取不到控制器和方法, 因为会先执行中间件, 再执行控制器的方法
  2. 后置中间件: 访问某个控制器的方法, 会先去执行方法里面的代码内容(不返回输出), 然后执行中间件的代码, 最后才是方法的输出返回, 如果中间件有拦截, 流程就会在中间件内截断, 但是方法内的代码也在前面执行了, 可以获取控制器和方法, 因为会先去执行这个控制器的方法, 再执行中间件

在app/api模块下, 新建middleware文件夹, 然后在该文件夹下创建类


/**
 * 老王
 *
 **/

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文件


// 该模块下的中间件定义文件, 对该模块下的所有控制器都有效, 在middleware下写好中间件后, 需要在该文件下配置绑定
// 如果使得该中间件只针对某一个控制器有效, 可以借助路由来设置, 不需要在该文件下配置绑定, 直接在middleware下写好中间件然后通过路由绑定->middleware()
return [
    app\api\middleware\CheckAuth::class
];
4. 业务逻辑异常处理
4.1 错误码

错误码是用来描述当前接口处理的结果, 是前后端共同的约束
格式:

  1. code 错误码
  2. msg 错误码对应的描述

一般以配置文件( config/status.php )或者类的常量来定义错误码, 统一管理便于维护
在app/common/lib文件夹下新建error, 并在该文件夹下创建类库


/**
 * 老王
 *
 **/

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文件夹下新建异常处理的类
自定义接收异常并格式化输出类


/**
 * 老王
 *
 **/

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);
    }

}

自定义抛出异常的类


/**
 * 老王
 *
 **/

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方法接管异常信息, 并格式化输出

你可能感兴趣的:(TP6,JWT,PHP,php,jwt)