基于Hyperf框架初始化,数据库配置,生成模型,生成错误码枚举类,错误监听,登录退出接口,token处理

494 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天 点击查看活动详情

前言

前面说到了要做一个爬虫数据的管理后台,新建了一个管理后台前端和后端接口的项目,今天来完善在Hyperf框架中实现登录相关的接口以及相关的初始化配置。

后端数据库准备

Mysql&Redis

先复习一下Hyperf框架目录:

image.png 登录必须操作数据库,进行用户的账号密码验证,后面还要利用Redis做账号token缓存,数据缓存。这里尽量做到最简单,先在根目录下.env文件下设置Mysql和Redis的连接信息,主要是以下几项:

DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=spider-data
DB_USERNAME=root
DB_PASSWORD=123456


REDIS_HOST=127.0.0.1
REDIS_AUTH=
REDIS_PORT=6379
REDIS_DB=0

我连接的是我本地电脑的mysql 新建一个库,就叫spider_data的库

image.png

为了快速开发就不使用数据库迁移文件创建数据表了,这里手动创建admin管理员表:

CREATE TABLE `spider_data`.`admin`  (
    `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名称',
  `password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码',
  `status` tinyint NOT NULL DEFAULT 1 COMMENT '1正常0禁用',
  `create_time` timestamp   NOT NULL default CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp  NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`)
);

启动docker 进入容器,新建admin表模型文件:

 docker  exec -it spider-admin-api /bin/sh
 php bin/hyperf.php  gen:model admin
 

创建模型成功,生成了一个App\Model\Admin.php文件,后面会在控制器中使用到它。

枚举类使用

现在的作用是主要定义错误码,后期会使用一些不常修改的常量。

composer require hyperf/constants

快速生成一个枚举类供登录错误使用:

php bin/hyperf.php gen:constant ErrorCode

生成了一个文件App\Constants\ErrorCode,然后新增两个错误码:

...

/**
 * 账号密码错误
 * @Message("Account Error!")
 */
const ACCOUNT_ERROR = 5001;

/**
 * 账号被禁用
 * @Message("Account Status Error!")
 */
const ACCOUNT_STATUS_ERROR = 5002;
...

封装公共返回规范&错误捕获

在Controller文件夹基类(App\Controller\AbstractController)中添加两个方法:

...
use App\Exception\BusinessException;
...

/**
 * 操作成功跳转的快捷方法.
 *
 * @param mixed $msg 提示信息
 * @param mixed $data 返回的数据
 * @return array $result
 */
protected function success($data = '', string $msg = '成功!')
{
    return [
        'code' => 200,
        'msg' => $msg,
        'data' => $data,
    ];
}

/**
 * 操作失败跳转的快捷方法.
 *
 * @param int $code 返回的错误代码
 * @param string $msg 返回错误信息
 */
protected function error(int $code = 0, string $msg = '', array $params = [])
{
    throw new BusinessException($code, $msg, null, $params);
}
...

这里使用了一个自定义的BusinessException代码异常类,位置:App\Exception\BusinessException:

<?php


declare(strict_types=1);

namespace App\Exception;

use App\Constants\ErrorCode;
use Hyperf\Server\Exception\ServerException;
use Throwable;

class BusinessException extends ServerException
{
    public function __construct(int $code = 0, string $msg = null, Throwable $previous = null)
    {
        if (is_null($msg) || $msg == '') {
            $msg = ErrorCode::getMessage($code);
        }
        parent::__construct($msg, $code, $previous);
    }
}

parent::__construct($msg, $code, $previous);这句代码意思是重写父类构造方法,同时为了保证父类构造方法能够执行需要再次调用父类构造方法。

这里需要增加一个对应的定义异常处理器,位置:App\Exception\Handler\BusinessExceptionHandler:

<?php

declare(strict_types=1);

namespace App\Exception\Handler;

use App\Constants\ErrorCode;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use App\Exception\BusinessException;
use Throwable;

/**
 * 系统内手动抛出异常处理器
 */
class BusinessExceptionHandler extends ExceptionHandler
{
    public function handle(Throwable $throwable, ResponseInterface $response)
    {
        if ($throwable instanceof BusinessException) {
            // 格式化输出
            $data = json_encode([
                'code' => (int)$throwable->getCode(),
                'msg' => $throwable->getMessage(),
            ], JSON_UNESCAPED_UNICODE);
            $this->stopPropagation();
            $code = 200;
            return $response->withHeader('Content-Type', 'application/json')->withStatus($code)->withBody(new SwooleStream($data));
        }
        return $response;
    }

    public function isValid(Throwable $throwable): bool
    {
        return true;
    }
}

接着在框架设置配置这个异常处理器,启动这个专属的错误监听,位置:config/autoload/exceptions.php:

<?php

declare(strict_types=1);
/**
 * This file is part of Hyperf.
 *
 * @link     https://www.hyperf.io
 * @document https://hyperf.wiki
 * @contact  group@hyperf.io
 * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
 */
return [
    'handler' => [
        'http' => [
            App\Exception\Handler\BusinessExceptionHandler::class,   // 手动抛出异常
        ],
    ],
];

接着根据文档,在 config/autoload/listeners.php 中添加监听器:

return [
    \Hyperf\ExceptionHandler\Listener\ErrorExceptionHandler::class
];

新建工具类

主要是操作Token的封装,写在Contrller方法中实在是不优雅,主要实现在Redis中数据的增删,实现了最简单的登录token新增和删除,感觉还有优化的空间。 App\Tool\Token:

<?php

declare(strict_types=1);
/**
 * This file is part of Hyperf.
 *
 * @link     https://www.hyperf.io
 * @document https://hyperf.wiki
 * @contact  group@hyperf.io
 * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
 */
namespace App\Tool;

use Hyperf\Di\Annotation\Inject;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;

/**
 * token 用来设置更新和获取token相关的信息.
 */
class Token
{
    /**
     * token名称.
     *
     * @var string
     */
    protected $token_name = '__adminToken__';

    /**
     * token过期时间(以秒计算).
     *
     * @var int
     */
    protected $expire = 3600;

    /**
     * token长度.
     * @var int
     */
    protected $token_length = 96;

    /**
     * 缓存.
     * @Inject
     * @var CacheInterface
     */
    protected $cache;

    /**
     * 校验token是否存在.
     *
     * @throws InvalidArgumentException
     */
    public function check(string $token): bool
    {
        $token_name = $this->token_name . ':' . $token;
        $info = $this->cache->get($token_name);
        return (bool) $info;
    }

    /**
     * 设置token.
     *
     * @param int $uid 用户id
     * @throws InvalidArgumentException
     * @return string str 成功返回token值,失败返回 false
     */
    public function set(int $uid, int $expire = 0): string
    {
        $expire = $expire ?: $this->expire;  // 默认过期时间为1个小时

        $token_name = $this->getTokenStr($this->token_length); // 生成一个token随机字符串为值
        $token_name_key = $this->token_name . ':' . $token_name;
        $this->cache->set($token_name_key, $uid, $expire);
        return $token_name;
    }

    /**
     * 删除admin token.
     * @throws InvalidArgumentException
     */
    public function delete(string $token)
    {
        $token_name = $this->token_name . ':' . $token;
        $this->cache->delete($token_name);
    }

    /**
     * 获取token字符串.
     *
     * @param int $len 长度
     * @return string 字符串
     */
    private function getTokenStr(int $len)
    {
        $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
        $string =(string)intval( microtime(true) * 10000);
        $len -= strlen($string);
        for (; $len > 1; --$len) {
            $position = rand() % strlen($chars);
            $position2 = rand() % strlen($string);
            $string = substr_replace($string, substr($chars, $position, 1), $position2, 0);
        }
        return $string;
    }
}

新建控制器

使用注解路由写两个接口,登录实现较为简单就不在Service层中实现了,在Controller层中实现,位置:App\Controller\LoginController.php

<?php

declare(strict_types=1);
/**
 * This file is part of Hyperf.
 *
 * @link     https://www.hyperf.io
 * @document https://hyperf.wiki
 * @contact  group@hyperf.io
 * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
 */
namespace App\Controller;

use App\Constants\ErrorCode;
use App\Model\Admin;
use App\Tool\Token;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\AutoController;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\HttpServer\Contract\RequestInterface;

/**
 * @AutoController
 */
class LoginController extends AbstractController
{
    /**
     * @Inject
     * @var Token
     */
    protected $token_tool;

    /**
     * 登录.
     * @RequestMapping(path="login", methods="post")
     */
    public function login(RequestInterface $request)
    {
        $user_name = $request->input('user_name');
        $password = $request->input('password');
        $user_info = Admin::where(['user_name' => $user_name, 'password' => md5($password . $this->salt), 'status' => 1])->first();
        if (! $user_info) {
            $this->error(ErrorCode::ACCOUNT_STATUS_ERROR, '账号或密码错误');
        }
        if ($user_info['status'] === 0) {
            $this->error(ErrorCode::ACCOUNT_STATUS_ERROR, '账号目前不可用');
        }
        $token = $this->token_tool->set($user_info['id']);
        return $this->success(['token' => $token], '登录成功!');  // 这里就使用了基类的统一返回方法
    }

    /**
     * 注销登录.
     * @RequestMapping(path="logout", methods="post")
     */
    public function logout(RequestInterface $request)
    {
        $this->token_tool->delete($request->getHeader('token')[0] ?? '');
        return $this->success();
    }
}

添加假数据,Postman脚本

讲到这里所有基本的逻辑就完成了,使用Postman测试之前要在数据库中添加一个假数据:

image.png 密钥是通过MD5加盐后混淆存在数据库的,效果:

image.png

需要注意账号密码安全,前端MD5混淆一次传给后端,后端在MD5混淆一次保存在数据,这样尽量避免了数据的泄露,避免一些中间人工具和mysql注入脱裤的攻击。

我在Postman写了一个脚本登陆后自动设置全局token变量,在退出测试或者其他接口测试的时候就不用手动复制在登录的时候时的token放在请求头了:

var data=pm.response.json();
console.log(data)
if (data['code']==200){
        pm.environment.set('spider-token', data['data']['token'])
}

image.png

image.png

总结

今天的这篇文章代码片段较长,很枯燥,没什么详细说的,这个全局异常监听通过今天的整理,终于思路清晰了很多,接下来会对接昨天的前端element管理后端框架中的登录相关的接口。