简述接口 Token 加密认证机制及实现 Demo

 流氓凡   2020-08-29 14:36   1996 人阅读  0 条评论

为何要使用 Token 认证机制?

    简单的说,token 适合接口开发进行相关访问授权来避免其他用户进行接口滥用的一种防护措施。无论是前后端分离(例如:小程序开发、Vue、单应用之间的交互等)后需要此方法来进行权限验证。

    本篇全文使用 thinkPHP 5.0框架做演示原理的实现, 根本不难主要是也方便自己做一次复盘。

简要的概述实现原理和方法:

    客户端拿着账号或秘钥来请求服务端生成 Token 的接口,服务端返回 token 给客户端,客户端存储到缓存后续客户端请求服务端其他接口的时候带着这个 token,服务端在其他接口前进行 token 鉴权,成功继续走,失败返回错误信息。

我们还有哪些需要注意的?

    token 的有效期

    token 接口每个账号的请求次数

    请求数据加密

接下来我们分开来说下,首先就是 token 的 生成及有效期:

    token 的有效期可根据自己的项目来定,我们那微信小程序的access_token来做个例子吧,这个access_token有效期是7200 秒,也就是 2 小时,重复刷新会导致上次access_token失效!需要注意的是,为了保证业务的平滑过度,旧的 token 也需要超时保留 5 分钟可用。OK,我们看下代码的实现:

    // 获取 token
    public function token()
    {
        $params = $this->request->param();
        // 这个是第三方参数校验,意思是参数不能空且字符串类型
        Validation::validate($params, [
            'appid' => 'Required|Str',
            'secret' => 'Required|Str',
        ]);
        // 鉴权
        $this->authVerification($params['appid'], $params['secret']);
        $expires_in = 7200; // 有效期(秒)
        $token = md5($params['appid'] . $params['secret'] . time() . '[随机字符串]'); // 生成本次 token
        cache($token, time() + $expires_in, $expires_in + 300);    // 缓存 token 参数
        $this->queryNumLimit($params['appid']);  // 接口请求次数+1
        return JSon(['token' => $token, 'expires_in' => $expires_in]); // 返回 token 及有效期
    }
    
    // 检查用户的账号和秘钥以及接口请求次数上限
    private function authVerification($app_id, $secret)
    {
        $userInfo = db('user')->where('app_id', $app_id)->find();
        if (!$userInfo) {
            // 抛出异常
            throw new BaseException(['msg' => 'appid verification failed']);
        }
        if ($userInfo['app_secret'] != $secret) {
            throw new BaseException(['msg' => 'secret verification failed']);
        }
        // 账号禁用
        if ($userInfo['status'] != 10) {
            throw new BaseException(['msg' => 'This account has been disabled']);
        }
        // 请求次数达到上限
        if ($this->getQueryNum($app_id) > $userInfo['query_num']) {
            throw new BaseException(['msg' => 'The number of requests reached the limit']);
        }
        return true;
    }

    为了方便演示,这里创建一个数据表进行模拟:

SET NAMES utf8;
SET time_zone = '+00:00';
SET foreign_key_checks = 0;
SET sql_mODe = 'NO_AUTO_VALUE_ON_ZERO';

DROP TABLE IF EXISTS `qd_user`;
CREATE TABLE `qd_user` (
  `user_id` int(11) NOT NULL AUTO_INCREMENT,
  `app_id` varchar(30) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '账号应用 ID',
  `app_secret` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '账号应用 key',
  `salt` varchar(50) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT '盐',
  `status` tinyint(3) NOT NULL DEFAULT '10' COMMENT '账号状态 10 正常 30 封禁',
  `query_num` int(11) NOT NULL DEFAULT '3000' COMMENT '请求次数限制',
  `create_time` int(11) NOT NULL DEFAULT '0',
  `update_time` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `app_id` (`app_id`)
) ENGINE=InnODB DEFAULT CHARSET=utf8 COMMENT='用户表';

    此表设计涉及到盐加密,如果觉得麻烦的就删除此字段吧。

    我们大概说下token 方法的实现思路

        首先我们先检查 appid 和 secret 是否正确,那正确的就看下属于 这个 appid 的请求次数是否达到上限,这个后面也可以按具体接口应用的 appid 进行限制处理。

        然后,我们定义一个 token 的有效期,这里是 7200 秒,我们将生成的token 进行缓存,缓存的键就是这个 token 值就是当前时间 + 有效期,失效时间就是有效期+300 也就是保留 5 分钟的过度时间。

        最后我们增加下这个 appid 的请求次数,然后返回 token 给客户端即可。

    // 客户端请求接口获取 token
    public function getToken()
    {
        // 因为有请求次数限制,所以我们需要把每次请求回来的 token 进行缓存,缓存时间比服务端接口的有效期略小就 ok
        if (!cache('queryToken')) {
            // 这里用到了curl 内置封装方法,随便百度即可
            $token = curl(config('bppUrl') . 'api/getToken?appid=7e2d8335&secret=9c4a72f0cc64c');
            cache('queryToken', $token,7000);
        }
        return JSon_decode(cache('queryToken'), true);
    }

token 的验证:

    我们的 token 验证还是比较简单的,上一步我们将当前时间+有效期时间存储到了已 token 为键的缓存中,这次客户端拿着 token 来请求服务端接口,服务端去用这个 token 读缓存的值,把读出来的值与当前时间对比,如果这个值小于了当前时间,那么表示 token 过了有效期,当然 token 不存在什么的就很好判断了。

    另外,多说一句,如果所有接口都需要验证此 token ,那么 token 验证直接写在基类即可

    // thinkPHP 自带控制器初始化方法
    public function _initialize()
    {
        $this->checkToken(input('token');     
    }
    
    // token 校验
    private function checkToken($token)
    {
        // token 不存在
        if (empty($token) || !cache($token)) {
            throw new BaseException(['msg' => 'Token verification failed']);
        }
        // token 失效
        if (cache($token) < time()) {
            throw new BaseException(['msg' => 'Token has expired']);
        }
        return true;
    }

接下来我们说说请求次数:

    我们为什么要给这个接口增加请求次数限制?还是为了避免自己的接口凭证泄露的一种保护措施,也是减少服务端压力的一种有效措施。这里我们还是使用缓存来做:

    // 增加请求次数
    private function queryNumLimit($appid)
    {
        if (!cache($appid . 'tokenNum')) {
            // 缓存到当前的 24 点,24 点之后重新计算次数
            cache($appid . 'tokenNum', 1, strtotime(date('Y-m-d', time())) + 24 * 60 * 60 - time());
        }
        Cache::inc($appid . 'tokenNum');
    }
    
    // 获取当前调用次数
    private function getQueryNum($appid)
    {
        return cache($appid . 'tokenNum');
    }

到了这里,我们要说的 token 完整的实现基本就差不多了,下面说点题外的

        关于上次开发的单应用之间的数据同步对接。toked 的有效期总是有一段时间的,如果发生接口泄露,那么一个脚本在 1 秒直接做很多事情了,更何况几千秒呢?

    所以为了保证数据的安全,数据加密有时候也是必不可少的,我们经常对接一些支付接口,腾讯的接口都会有个 sdk 包,里面就含有秘钥的获取和参数的加密,这足以证明上面我们说的 token 机制实际上只能做一些内部应用接口上,一旦应用到公开接口比如说 商城的订单处理、工具类的视频上传等等,还是需要单独开发一套完整的认证系统的。

总结

    总的来说,本篇主要讲解了 token 生成和认证的实现思路,主要的意义在于给自己开发的单应用之间的数据交互做一次复盘,还有目前在做一个开放接口,但是接口数量上还是比较少的还没有开源出来,有兴趣的话可以联系我一起加入维护进来,也是为了给自己将来开发的道路上带来一点方便吧。


发表评论:


表情

还没有留言,还不快点抢沙发?