本文介绍如何运用thinkphp6开发RESTful API接口,实现管理系统前后分离登录验证功能。
输出JSON格式封装
首先把系统输出数据的格式进行统一,创建common文件夹并且创建Output.php。
<?php
namespace app\adminApi\common;
use think\Response;
trait Output{
/**
* 处理成功返回API数据
* @param mixed $data 要返回的数据
* @param integer $code 返回的code
* @param mixed $message 提示信息
* @param string $type 返回数据格式
* @param array $header 发送的Header信息
* @return Response
*/
public function Success($data , string $message = '请求成功', int $code = 200, string $type = 'json',$header = []) :Response {
$result = [
'code' => $code,
'message' => $message,
'time' => date('Y-m-d H:i:s',time()),
'data' => $data
];
return json($result,$code);
}
/**
* 处理失败返回API数据
* @param mixed $data 要返回的数据
* @param integer $code 返回的code
* @param mixed $message 提示信息
* @param string $type 返回数据格式
* @param array $header 发送的Header信息
* @return Response
*/
public function Error($data , string $message = '请求失败', int $code = 500, string $type = 'json',$header = []) :Response {
$result = [
'code' => $code,
'message' => $message,
'time' => date('Y-m-d H:i:s',time()),
'data' => $data
];
return json($result,$code);
}
/**
* @param int $code
* @param string $message
* @param array $data
* @param array $header
*/
public function returnMsg($data = [], $message = '',$code = 500,$header = []) {
$res = [
'code' => $code,
'message' => $message,
'time' => date('Y-m-d H:i:s',time()),
'data' => $data
];
return json($res,$code);
}
}
在app下BaseController.php文件引入封装方法
<?php
declare (strict_types = 1);
namespace app\adminApi;
use think\App;
use think\exception\ValidateException;
use think\Validate;
/**
* 引用封装方法
*/
use app\adminApi\common\Output;
/**
* 控制器基础类
*/
abstract class BaseController{
use Output;
/**
* Request实例
* @var \think\Request
*/
protected $request;
/**
* 应用实例
* @var \think\App
*/
protected $app;
/**
* 是否批量验证
* @var bool
*/
protected $batchValidate = false;
/**
* 控制器中间件
* @var array
*/
protected $middleware = [];
/**
* 构造方法
* @access public
* @param App $app 应用对象
*/
public function __construct(App $app) {
$this->app = $app;
$this->request = $this->app->request;
// 控制器初始化
$this->initialize();
}
// 初始化
protected function initialize() {}
/**
* 验证数据
* @access protected
* @param array $data 数据
* @param string|array $validate 验证器名或者验证规则数组
* @param array $message 提示信息
* @param bool $batch 是否批量验证
* @return array|string|true
* @throws ValidateException
*/
protected function validate(array $data, $validate, array $message = [], bool $batch = false) {
if (is_array($validate)) {
$v = new Validate();
$v->rule($validate);
} else {
if (strpos($validate, '.')) {
// 支持场景
list($validate, $scene) = explode('.', $validate);
}
$class = false !== strpos($validate, '\\') ? $validate : $this->app->parseClass('validate', $validate);
$v = new $class();
if (!empty($scene)) {
$v->scene($scene);
}
}
$v->message($message);
// 是否批量验证
if ($batch || $this->batchValidate) {
$v->batch(true);
}
return $v->failException(true)->check($data);
}
}
在route路由文件配置RESTFul请求的资源路由
<?php
use think\facade\Route;
Route::resource('blogs', 'Blogs')->only(['index', 'read']);
Blogs示例控制器
<?php
namespace app\adminApi\controller;
use app\adminApi\BaseController;
class Blogs extends BaseController{
/*** @OA\GET(
* tags={"博客管理"},
* summary="博客列表",
* path="/api/admin.php/blogs",
* @OA\Response(
* response="200",
* description="状态码、提示消息",
* )
*)
*/
public function index() {
return self::Success([]);
}
/**
* @OA\Get(path="/api/admin.php/blogs/{id}",
* tags={"博客管理"},
* summary="博客文章",
* @OA\Parameter(name="id", in="path", description="文章id", @OA\Schema(type="number"),required=true),
* @OA\Response(response="200", description="The User")
* )
*/
public function read($id) {
return self::Error();
}}
用swagger测试http://域名/api/admin.php/blogs,swagger使用方法请查看(0到1实现thinkphp6+swagger-php3.0配置管理接口文档)
自定义异常处理
thinkphp6的异常是HTML格式输出,既然我们开发RESTful API接口,也希望异常按照统一规格以json输出。(thinkphp6官方异常处理文档)
应用文件夹下创建ExceptionHandle.php并且重写render()方法。
<?php
namespace app\adminApi;
use think\db\exception\DataNotFoundException;
use think\db\exception\ModelNotFoundException;
use think\exception\Handle;
use think\exception\HttpException;
use think\exception\HttpResponseException;
use think\exception\ValidateException;
use think\Response;use Throwable;
/**
* 应用异常处理类
*/
class ExceptionHandle extends Handle{
private $code;
private $message;
private $statuscode;
/**
* @param \think\Request $request
* @param Throwable $e
* @return Response
*/
public function render($request, Throwable $e): Response {
$this->statuscode = $this->code = 500;
if($e instanceof BaseException){//是否异常实例
$this->code = $e->code;
$this->message = $e->message;
}else{
if (config('app_debug')){
return parent::render($request, $e); // TODO: Change the autogenerated stub
}
$this->code = 500;
$this->message = $e->getMessage() ?: '服务器内部错误';
}
// Http异常
if ($e instanceof \think\exception\HttpException) {
$this->statuscode = $this->code = $e->getStatusCode();
}
$data = [
'code' => $this->statuscode,
'message' => $this->message,
'time' => time(),
'data' => null,
'request_url' => $request->url()
];
return json($data,$this->statuscode);
}
}
在对应的模块文件夹创建provider.php文件
<?php
use app\adminApi\ExceptionHandle;
use app\adminApi\Request;
// 容器Provider定义文件
return [
'think\Request' => Request::class,
'think\exception\Handle' => '\\app\\adminApi\\ExceptionHandle'
];
用swagger测试http://域名/api/admin.php/blogs1,可以看到异常已经可以按json格式输出。
权限校验处理
权限校验我们使用thinkphp6的中间件来处理,在应用middleware文件夹新建Auth.php文件。
<?php
declare (strict_types = 1);
namespace app\adminApi\middleware;
use app\adminApi\common\Output;
class Auth{
use Output;
/**
* 处理请求
*
* @param \think\Request $request
* @param \Closure $next
* @return Response
*/
public function handle($request, \Closure $next) {
return self::returnMsg(null,'请先登陆!',404);
}
}
继续使用blogs.php作为示例,引入中间件。
<?php
namespace app\adminApi\controller;
use app\adminApi\BaseController;
//引入权限中间件
use app\adminApi\middleware\Auth;
class Blogs extends BaseController{
protected $middleware = [Auth::class];
/*** @OA\GET(
* tags={"博客管理"},
* summary="博客列表",
* path="/api/admin.php/blogs",
* @OA\Response(
* response="200",
* description="状态码、提示消息",
* )
*)
*/
public function index() {
return self::Success([]);
}
/**
* @OA\Get(path="/api/admin.php/blogs/{id}",
* tags={"博客管理"},
* summary="博客文章",
* @OA\Parameter(name="id", in="path", description="文章id", @OA\Schema(type="number"),required=true),
* @OA\Response(response="200", description="The User")
* )
*/
public function read($id) {
return self::Error();
}}
用swagger测试http://域名/api/admin.php/blogs
把protected $middleware = [Auth::class]; 修改为protected $middleware = [Auth::class => ['except' => ['index']]];,可配置该控制器不作权限校验,修改后
访问read则带有校验,
使用jwt实现信息加密
安装jwt扩展
composer require firebase/php-jwt
在应用services文件夹新建JwtService.php文件,封装加密跟解密方法
<?php
namespace app\adminApi\services;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JwtService{
private $key = "md5";
private $iss = "https://域名";
private $aud = "XXX";
private $expTime = 36000;
// jwt生成
public function setJwtToken($uid) {
$payload = array(
"iss" => $this->iss,
"aud" => $this->aud,
"iat"=>time(), //签发时间
"nbf"=>time(), //在什么时候jwt开始生效
"exp"=> time()+$this->expTime, //token 过期时间
'uid' => $uid, //前端页面所传uid
);
$jwt = JWT::encode($payload, md5($this->key), 'HS256');
return $jwt;
}
// jwt解密
public function decryptJwtToken($token) {
$decoded = JWT::decode($token, new Key(md5($this->key), 'HS256'));
return $decoded;
}
}
在中间件Auth.php进行权限校验
<?php
declare (strict_types = 1);
namespace app\adminApi\middleware;
use think\facade\Session;
use app\adminApi\common\Output;
use app\adminApi\services\JwtService;
use Firebase\JWT\ExpiredException;
class Auth{
use Output;
/**
* 处理请求
* @param \think\Request $request
* @param \Closure $next
* @return Response
*/
public function handle($request, \Closure $next) {
//获取请求的时候头部携带着这个Authorization
$token = $request->header('Authorization');
$tokenJwt = new JwtService();
if (!$token) return self::returnMsg(null, '请先登陆!', 401);
//判断token是否为空
$token = substr($token,7);
try {
$jwtAuth = $tokenJwt->decryptJwtToken($token);
$authInfo = json_decode(json_encode($jwtAuth), true);
if (!$authInfo['uid']) {
return self::returnMsg(null, '用户不存在', 401);
}
return $next($request);
} catch (ExpiredException $e) {
return self::returnMsg(null, 'token过期', 401);
} catch (\Exception $e) {
return self::returnMsg(null, $e->getMessage(), 401);
}
}
}
在Login控制器写登录方法
<?php
namespace app\adminApi\controller;
use think\facade\Db;
use think\facade\Request;
use app\adminApi\CommonController;
use think\exception\ValidateException;
use app\adminApi\validate\Admin;
use app\adminApi\services\JwtService;
class Login extends CommonController {
/** @OA\Post(
* path="/api/admin.php/login/checklogin",
* tags={"用户相关"},
* summary="用户登录",
* @OA\RequestBody(required=true,description="body",content={
* @OA\MediaType(mediaType="application/json",
* @OA\Schema(
* @OA\Property(
* property="admin_user",
* type="string",
* description="账号"
* ),
* @OA\Property(
* property="admin_pwd",
* type="string",
* description="密码"
* ),
* )
* )
* }),
* @OA\Response(
* response="200",
* description="状态码、提示消息"
* )
* )
*/
public function checklogin(){
$loginData=Request::param();
$tokenJwt = new JwtService();
try {
validate(Admin::class)->check($loginData);
$post_pwd = $loginData['admin_pwd'];
$where['admin_user'] = $loginData['admin_user'];
$admin = Db::name('admin')->where($where)->field('uid,admin_pwd')->find();
if ($admin['admin_pwd'] == md5($password)) {
$uid = $admin['uid'];
$admin_user['uid'] = $uid;
$admin_user['username'] = $where['admin_user'];
$addData['login_time'] = time();
return self::Success($tokenJwt->setJwtToken($uid),'登录成功');
}else{
return self::Error(null,'登录失败',400);
}
} catch (ValidateException $e) {
// 验证失败 输出错误信息
return self::Error(null,$e->getError(),400);
}
}
用swagger测试 http://域名/api/admin.php/login/checklogin
带上token请求Blog示例,请求成功
最后,关于如何处理Authorization过期的问题,有两种解决办法,第一种就是,将Authorization的时间设置长一些,这样Authorization就不会过期,但是这样就有一个弊端,一旦客户端拿到了这个Authorization就相当于有了密钥,主动权也就掌握在了用户的手上。所以不推荐这种方案。第二种就是,后端处理,当Authorization过期的时候重新获取Authorization,将新的token传给前端,前端在将新的Authorization存储起来,替换掉原来的Authorization,下一次请求的时候就携带着新的Authorization请求。
至于,后端整个登录校验功能已经完成。
文章评论