zoukankan      html  css  js  c++  java
  • PHP代码篇(九)PHP接口开发如何使用JWT进行验证身份

    • 前言

        事情是这样的,在我进入目前公司的时候。因为公司是一家创业公司,所以在我进去的时候,里面的开发配置就是web前端,ui设计,加我PHP后台各一个。接手的是一个公益小程序,业务倒是不怎么复杂,负责人说这个项目是之前委托外包公司开发的,用的是uniapp开发的小程序,基于ThinkPHP6.0做的接口开发,和一个CatchAdmin开发的管理后台。公司主要业务这个上面都已经开发完了,但是由于微信小程序的一些功能的限制,我们想把目前的这个项目转移到微信公众号上面,就是改成H5开发的模式。

    • 八月的雨

        当时负责人,说前端可能需要重写,因为之前是小程序,现在是H5嘛,前端是一个刚服役退伍的年轻小伙子,看了下说,用uniapp开发,到时候打包成H5就可以了,基本问题不大,于是就卡卡的开始写静态页面了,说一周写完静态页面。

        到后台这边,就有两个选择了。因为不管怎么变,最终还是做接口开发。所以有两个选择,要么在原来外包公司开发的api上修改,做二开,这样应该基本改下登陆注册方式,和部分业务逻辑即可;听取负责人的态度和想法目前对外包公司开发的api项目主要有如下不满点:

          1、说这个外包公司开发的项目,里面有很多业务逻辑和现在他们想要不一样;

          2、有些功能业务,实际上还没开发完,或者有些开发完了,还没测试,有很多问题;

          3、对这个项目的完成度,负责人,自己也不是完全清楚;

        根据负责人的描述,再结合自己这几天看项目源代码,发现外包写的代码确实比较差,看出来,虽然是用的tp框架,但是写的基本上非常赶。有些接口,基本上是写完,就没有具体测试。还有最主要的就是,基本所有的业务代码逻辑承载都在一个controller里面,非常不适合后期的迭代维护。所以第二种选择就是,直接进行重写,根据负责人对与项目的业务要求,直接重新设计数据库,重写api接口。

    • 八月中下旬的选择

        当时我的想法,当然是重写是最好,原因如下:

          1、这个原有项目真实逻辑没有开发人员真实了解(就是我来的时候,都没有对接人);

          2、这个项目本身业务逻辑比较单一,而且二期改版有十分大;

          3、如果二开,我既要去熟悉旧有代码,还要修改,对于新功能的开发,还需要切合旧有逻辑;

        想到如上,确实还是直接重写最好,这样,一个是代码的标准,质量,逻辑都在自己的掌握之中,也便于后期的工作。

    • 我承认自己有赌的成分

        重写的原因说来,一条一条的,清晰完整。但是在做的时候,因为之前一般进入公司的,去的时候,公司项目基本上已经上线了,我们主要是做维护,和调优;自己主导写一个完整的项目,还没有;而且之前大多是做前后端不分离的方式,像这种做接口开发,前后端分离的方式,也有点欠缺理解。

        于是入职这一周时间,我一直在重复一个流程:

          1、想一个自己能说的通的开发方案,在脑子里过几遍;

          2、如果感觉没问题,就下载相应框架源码,搭建demo,开始按方案写代码逻辑。在写的过程中,如果出现方案外的问题,就想办法解决;

          3、如果方案外的问题实在,无力解决,就只能推倒当前方案;

        最后又回到流程1,这样我记得有很多次了,当时感觉已经很奔溃了,有点后悔,因为我目前的问题主要是对于这个接口验证JWT不是特理解,虽然现在写的时候,已经明白。但是当时是真的,有点晕(今天是10月14日,入职是8月12日)。这里说后悔是什么意思呢,因为在上一家公司做的时候,我们有做过一个新项目,就是前后端分离的模式。前端用的是vue,后端纯接口开发,接口验证用的就是jwt。但是当时基础部分是另外一个同事写的,我只负责写业务端,像登录注册,验证,板块设计都是他做的。记得当时是有在看他验证这块是怎么写的,有不懂的也有当面问过,但是还是没有具体弄明白,后来想着如果后面需要自己处理这块,再说,我承认当时确实有赌的成份,现在果然赌输了。

        人生很多事,就是这样,有的时候,不知道珍惜,到后来需要自己独自面对的时候,就要多劳力了。经历了上面的不断的试错和尝试,时间在一天一天过去,但是我这边进展,还是一直没有。每次项目进度开会,明显感觉到,像是就我这边进度缓慢。但是有一点可以确认的是,每次在自己的演示中,虽然方案行不通,回到了最初点,但是感觉对于jwt的理解和架构的设计,有了一些实际的理解。我有一种感觉,应该快出来了。果然在一个现在已经忘记了时间上,出来了。

    • 言归正传

      PHP通过jwt实现接口验证,设计思路如下:

        1、先定义controller控制器基类Base.php,作用是继承改类的,都需要进行token验证;

        2、在定义一个前端api基类IndexBase.php,作为一个中间层,里面存放验证后token里面的用户信息

        3、书写PHP实现jwt基类PhpJwt.php,改类主要是获取token,和验证token;

      上面三者的关系是,IndexBase.php继承Base.php继承PhpJwt.php。

      前端与后端验证逻辑如下:

        1、前端请求接口,后台验证token;

        2、没有token,给出提示,前端输入账号密码(微信公众号类,直接通过非静默授权,获取用户openid),进行验证用户信息。验证通过后,将用户uid加载到jwt载荷中,生成token。一个验证toekn(过期时间比较短),一个刷新token(过期时间较长,用于避免每次段时间内,用户重复登录)。

        3、前端通过登录拿到两个token后,存起来。然后在请求需要验证的接口时,在header头部加入参数authorization:用户toekn;来进行验证,后台通过token来解析出当前请求用户的信息。

    • 核心代码

        1、PhpJwt.php

    <?php
    /**
     * PHP实现jwt
     * 
     */
    namespace lib;
    
    class PhpJwt {
        //头部
        private static $header = array(
            'alg'=>'HS256', //生成signature的算法
            'typ'=>'JWT'  //类型
        );
     
        //使用HMAC生成信息摘要时所使用的密钥 md5('jjgw2021')
        private static $key = '99dc2d62ab85bcd9185f3e9324db5567';
        //md5('jjgw2021admin')
        private static $admin_key = '12d44e568140bf62d84d9cb3e20b1103';
    
        //请求jwt 过期时间 2小时(上线后改为10分钟)
        private static $request_expect = 3600;
    
        //刷新jwt 过期时间 24小时
        private static $refresh_expect = 86400;
    
        private static $admin_request_expect = 1800;
    
        private static $admin_refresh_expect = 7200;
        //判断是否后端token
        private static $is_admin = 'is_admin';
    
    
        /*** 获取jwt token
         * @param array $payload jwt载荷  格式如下非必须
         * [
         * 'iss'=>'jwt_admin', //该JWT的签发者
         * 'iat'=>time(), //签发时间
         * 'exp'=>time()+7200, //过期时间
         * 'nbf'=>time()+60, //该时间之前不接收处理该Token
         * 'sub'=>'www.admin.com', //面向的用户
         * 'jti'=>md5(uniqid('JWT').time()) //该Token唯一标识
         * ]
         * @param int $refresh 是否刷新token 1是
         * @param int $is_admin 是否后台调用 1是 0 admin
         * @return string
         */
        public static function getToken(array $payload, $refresh = 0, $is_admin = 0)
        {
            $exp = $refresh ? ($is_admin ? self::$admin_refresh_expect : self::$refresh_expect) : ($is_admin ? self::$admin_request_expect : self::$request_expect);
            $load = [
                'iat' => time(),
                'exp' => time() + $exp,
                'jti' => md5(uniqid('JWT').time())
            ];
            $payload = array_merge($load, $payload);
            $key = ($is_admin == 1 ? self::$admin_key : self::$key);
            $base64header = self::base64UrlEncode(json_encode(self::$header,JSON_UNESCAPED_UNICODE));
            $base64payload = self::base64UrlEncode(json_encode($payload,JSON_UNESCAPED_UNICODE));
            $token = $base64header.'.'.$base64payload.'.'.self::signature($base64header.'.'.$base64payload,$key,self::$header['alg']);
            return $token;
        }
     
     
      /**
       * 验证token是否有效,默认验证exp,nbf,iat时间
       * @param string $Token 需要验证的token
       * @return array
       */
      public static function verifyToken($Token)
      {
            $tokens = explode('.', $Token);
            if (count($tokens) != 3){
                return [
                    'code' => 100,
                    'msg' => '验证失败'
                ];
            }
        
            list($base64header, $base64payload, $sign) = $tokens;
        
            //获取jwt算法
            $base64decodeheader = json_decode(self::base64UrlDecode($base64header), JSON_OBJECT_AS_ARRAY);
            if (empty($base64decodeheader['alg'])){
                return [
                    'code' => 100,
                    'msg' => '验证失败'
                ];
            }
            $payload = json_decode(self::base64UrlDecode($base64payload), JSON_OBJECT_AS_ARRAY);
            $key = !empty($payload[self::$is_admin]) ? self::$admin_key : self::$key;
            //签名验证
            if (self::signature($base64header . '.' . $base64payload, $key, $base64decodeheader['alg']) !== $sign){
                return [
                    'code' => 100,
                    'msg' => '签名验证失败'
                ];
            }
        
            //签发时间大于当前服务器时间验证失败
            if (isset($payload['iat']) && $payload['iat'] > time()) {
                return [
                    'code' => 100,
                    'msg' => '签发时间大于当前服务器时间,验证失败'
                ];
            }
        
            //过期时间小宇当前服务器时间验证失败
            if (isset($payload['exp']) && $payload['exp'] < time()) {
                return [
                    'code' => 200,
                    'msg' => '已过期'
                ];
            }
        
            //该nbf时间之前不接收处理该Token
            if (isset($payload['nbf']) && $payload['nbf'] > time()) {
                return [
                    'code' => 100,
                    'msg' => '验证失败'
                ];
            }
        
            return [
                'code' => 0,
                'msg' => '验证成功',
                'data' =>$payload
            ];
      }
     
     
     
     
      /**
       * base64UrlEncode  https://jwt.io/ 中base64UrlEncode编码实现
       * @param string $input 需要编码的字符串
       * @return string
       */
      private static function base64UrlEncode($input)
      {
            return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
      }
     
      /**
       * base64UrlEncode https://jwt.io/ 中base64UrlEncode解码实现
       * @param string $input 需要解码的字符串
       * @return bool|string
       */
      private static function base64UrlDecode($input)
      {
            $remainder = strlen($input) % 4;
            if ($remainder) {
                $addlen = 4 - $remainder;
                $input .= str_repeat('=', $addlen);
            }
            return base64_decode(strtr($input, '-_', '+/'));
      }
     
      /**
       * HMACSHA256签名  https://jwt.io/ 中HMACSHA256签名实现
       * @param string $input 为base64UrlEncode(header).".".base64UrlEncode(payload)
       * @param string $key
       * @param string $alg  算法方式
       * @return mixed
       */
      private static function signature($input, $key, $alg )
      {
            $alg_config = array(
                'HS256'=>'sha256'
            );
            return self::base64UrlEncode(hash_hmac($alg_config[$alg], $input, $key,true));
      }
    }
     

        2、Base.php

    <?php
    /*
     * @Fun: 控制器基类
     * @User: JessieK
     * @Date: 2021-08-19 18:06:47
     */
    namespace appcontroller;
    
    use appBaseController;
    use thinkfacadeRequest;
    use libPhpJwt;
    
    class Base extends BaseController
    {
        //会员uid
        protected $uid;
        //会员unionid
        protected $unionid;
        //会员openid
        protected $openid;
        //jwt
        protected $payload;
    
        public function __construct()
        {
            //更新中
            // $this->apiResult(-100, '网站正在火速更新中,请稍后---');
            // $controller = strtolower(Request::controller());
            $action = strtolower(Request::action());
            if(!in_array($action, ['wechatlogin', 'index', 'coc', 'arealist', 'uploadimg', 'uploadimgstring'])){
                //验证token
                $this->checkToken();
            }
            if(!in_array($action, ['wechatlogin', 'index', 'coc', 'arealist', 'uploadimg', 'getjwt', 'uploadimgstring'])){
                //验证sign
                // $this->verifySign();
            }
        }
    
        /**
         * 验证token
         */
        public function checkToken()
        {
            $token = empty(Request::header()['authorization']) ? '' : Request::header()['authorization'];
            if(!$token){
                $this->apiResult(-100, 'Authorization不能为空');
            }
            $get_payload = PhpJwt::verifyToken($token);
            switch($get_payload['code']){
                case 100:
                    $this->apiResult(-100, 'token验证失败');
                    break;
                case 200:
                    $this->apiResult(1000, 'token已过期');
            }
            $this->uid = $get_payload['data']['uid'];
            $this->unionid = $get_payload['data']['unionid'];
            $this->openid = $get_payload['data']['openid'];
            $this->payload = $get_payload['data'];
        }
    
        /**
         * @name: 验证签名
         * @param {*}
         * @return {*}
         */    
        public function verifySign()
        {
            $token = Request::header()['authorization'];
            list($base64header, $base64payload, $jwtsign) = explode('.', $token);
            $params = Request::post();
            if(empty($params['sign'])){
                $this->apiResult(-100 ,'sign不能为空');
            }
            if(empty($params['timestamp'])){
                $this->apiResult(-100, 'timestamp不能为空');
            }
            //10分钟有效 毫秒级
            if (time() * 1000 - $params['timestamp'] > 600000) {
                // $this->apiResult(-100, '请求过期');
            }
            $request_sign = $params['sign'];
            //对关联数组按照键名进行升序排序
            unset($params['sign']);
            ksort($params);
            $param_str = '';
            foreach ($params as $k => $v) {
                $v = is_array($v) ? json_encode($v, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) : $v;
                $param_str .= $k.$v;
            }
            $restr = $param_str.$jwtsign;
            $sign = md5($restr);
            if (strtolower($request_sign) != strtolower($sign)) {
                $this->apiResult(-100, '签名验证失败');
            }
       
        }
    
        public function apiResult($code, $msg, $data = [])
        {
            $result = [
                'code' => $code,
                'msg' => $msg,
                'data' => $data,
            ];
            exit(json_encode($result, JSON_UNESCAPED_UNICODE));
        }
    }

        3、IndexBase.php

    <?php
    /*
     * @Fun: 前台api基类
     * @User: JessieK
     * @Date: 2021-08-19 18:06:47
     */
    namespace appcontroller;
    
    use appmodelMember;
    use thinkfacadeRequest;
    class IndexBase extends Base
    {
        //会员信息
        protected $member_info;
    
        public function __construct()
        {
            parent::__construct();
            if(!$this->openid){
                $this->apiResult(-100, '缺少参数', ['msg' => 'openid为空']);
            }
            $memberModel = new Member();
            $member_info = $memberModel->getUserInfoGather($this->openid);
            if(!$member_info){
                $this->apiResult(1001, '会员信息不存在');
            }
            //锁粉操作
            $from_uid = Request::get('from_uid');
            if(empty($member_info['from_uid']) && !empty($from_uid)){
                $memberModel->setFromUid($member_info['uid'], $from_uid, Request::url(true));
            }
            // addlog('errorlog/request/', 'pro', '请求url='.json_encode(request()->get(), JSON_UNESCAPED_UNICODE));
            $this->member_info = $member_info;
        }
    }
    • 最后书写业务代码

        1、对于需要验证token的,只需要继承IndexBase.php即可,基类里面直接对前端传过来的token进行验证是否合法。

        2、用户在登录后,获取到请求token,进行接口验证;请求token过期后,不用重新登录,用户用刷新token刷新,获取到新的请求token,既可以重新获取验证,拿到用户信息,避免频繁登录。

        3、上述代码,还附带verfySign签名,这个主要是可以配合token进行一起使用。jwt实现验证用户身份,签名实现接口请求是否合法。大致逻辑,在每次请求接口时带上签名和时间戳,具体签名逻辑可看上述代码。


    -----END

    影子是一个会撒谎的精灵,它在虚空中流浪和等待被发现之间;在存在与不存在之间....
  • 相关阅读:
    border-radius的8个属性值_画半圆、叶子等
    CSS的background简写方式(转)
    frameset左右栏锚点定位实例
    HTML5新增
    安装MSI报2503的错误
    当前标识(IIS APPPOOLDefaultWebSite)没有对“C:WindowsMicrosoft.NETFramework64v2.0.50727Temporary ASP.NET Files”的写访问权限 解决方案
    Windows设置相关性AFFINITY,修改使用核心数
    Yaml格式文件处理
    Vs2017离线安装包制作
    Vs2017常用快捷键
  • 原文地址:https://www.cnblogs.com/camg/p/15407658.html
Copyright © 2011-2022 走看看