zoukankan      html  css  js  c++  java
  • 全栈微信小程序商城 学习笔记之九 微信登录与令牌

    初识T:意义与作用

    任何人都可以访问和调用服务器接口,但有些接口是不能随意访问的,需要确定用户身份,令牌就是管理这个的,登录就是获取令牌

    微信身份体系设计

    微信的code相当于账号密码,向微信服务器发送后,返回代表用户身份的唯一标识openid,(还有一个session_key,在这个项目不需要使用),将openid存储数据库,生成token并缓存,再返回token

    小程序只需要携带token令牌通过校验就可调用下单接口

    实现T身份权限体系

    applicationapicontrollerv1Token

    class Token
    {
        public function getToken($code = '') {
    
        }
    }
    

    applicationapivalidateTokenGet

    class TokenGet extends BaseValidate
    {
        protected $rule = [
            'code' => 'require|isNotEmpty'
        ];
        protected $message = [
            'code' => '没有code,不给你获取token'
        ];
    
    }
    

    applicationapivalidateBaseValidate.php

    protected function isNotEmpty($value)
    {
        if (empty($value))
        {
            return false;
        }
        else
        {
            return true;
        }
    }
    

    application oute.php

    // 注意这里是post,提高安全性
    Route::post('api/:version/token/user', 'api/:version.Token/getToken');
    

    user表

    建立模型
    applicationapimodelUser

    class User extends BaseModel
    {
    
    }
    

    新建服务层
    applicationapiserviceUserToken

    class UserToken
    {
        public function get($code) {
            
        }
    }
    

    applicationapicontrollerv1Token

    use appapiserviceUserToken;
    class Token
    {
        /**
         * 用户获取令牌(登陆)
         * @url /token
         * @POST code
         * @note 虽然查询应该使用get,但为了稍微增强安全性,所以使用POST
         */
        public function getToken($code='')
        {
            (new TokenGet())->goCheck();
            $ut = new UserToken($code);
            $token = $ut->get($code);
            return [
                'token' => $token
            ];
    
        }
    }
    

    获取openid

    写配置
    applicationextrawx.php

    return [
        // 小程序app_id
        'app_id' => 'your appid',
        // 小程序app_secret
        'app_secret' => 'your appsecret',
    
        // 微信使用code换取用户openid及session_key的url地址
        // appid=%s&secret=%s&js_code=%s表示有三个参数是要动态填入的
        'login_url' => "https://api.weixin.qq.com/sns/jscode2session?" .
            "appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
    ];
    

    applicationapiserviceUserToken

    class UserToken
    {
        protected $code;
        protected $wxAppID;
        protected $wxAppSecret;
        protected $wxLoginUrl;
    
        function __construct($code)
        {
            $this->code = $code;
            //读取配置文件
            $this->wxAppID = config('wx.app_id');
            $this->wxAppSecret = config('wx.app_secret');
            $this->wxLoginUrl = sprintf(config('wx.login_url'), $this->wxAppID
    ,$this->wxAppSecret, $this->code);
        }
    
        public function get() {
            
        }
    }
    

    新建一个http调用方法
    applicationapicommon.php

    function curl_get($url, &$httpCode = 0)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    
        //不做证书校验,部署在linux环境下请改为true
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        $file_contents = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        return $file_contents;
    }
    

    applicationapiserviceUsertoken

    class Usertoken
    {
        protected $code;
        protected $wxAppID;
        protected $wxAppSecret;
        protected $wxLoginUrl;
    
        function __construct($code)
        {
            $this->code = $code;
            $this->wxAppID = config('wx.app_id');
            $this->wxAppSecret = config('wx.app_secret');
            $this->wxLoginUrl = sprintf(config('wx.login_url'), $this->wxAppID
    ,$this->wxAppSecret, $this->code)
        }
    
        public function get() {
           $result = curl_get($this->wxLoginUrl);
            // 注意json_decode的第二个参数true,这将使字符串被转化为数组而非对象
           $wxResult = json_decode($result, true);
           if (empty($wxResult)) { 
                // 为什么以empty判断是否错误,这是根据微信返回规则摸索出来的
                // 这种情况通常是由于传入不合法的code
                // 为什么用框架的异常处理,因为是服务器异常,要记录日志
               throw new Exception('获取session_key及openID时异常,微信内部错误');
           }
           else{
                // 建议用明确的变量来表示是否成功
                // 微信服务器并不会将错误标记为400,无论成功还是失败都标记成200
                // 这样非常不好判断,只能使用errcode是否存在来判断
               $loginFail = array_key_exists('errcode', $wxResult);
               if ($loginFail) {
    
               }
               else {
    
               }
           }
        }
    

    }

    调用接口异常处理和用postman做测试

    applicationlibexceptionWeChatException.php

    <?php
    
    namespace applibexception;
    
    /**
     * 微信服务器异常
     */
    class WeChatException extends BaseException
    {
        public $code = 400;
        public $msg = 'wechat unknown error';
        public $errorCode = 999;
    }
    

    使用

    class Usertoken
    {
        public function get() {
           $result = curl_get($this->wxLoginUrl);
           $wxResult = json_decode($result, true);
           if (empty($wxResult)) {
               throw new Exception('获取session_key及openID时异常,微信内部错误');
           }
           else{
               $loginFail = array_key_exists('errcode', $wxResult);
               if ($loginFail) {
                    $this->processLoginError($wxResult)
               } 
               else {
                    return $this->grantToken($wxResult)
               }
           }
        }
        // 处理微信登陆异常
        // 那些异常应该返回客户端,那些异常不应该返回客户端
        // 需要认真思考
        private function processLoginError($wxResult)
        {
            throw new WeChatException(
                [
                    'msg' => $wxResult['errmsg'],
                    'errorCode' => $wxResult['errcode']
                ]);
        }
        private function grantToken($wxResult){
            // 拿到openid
            // 到数据库看一下,这个openid是不是已存在
            // 如果存在,则不处理,如果不存在则新增一条user记录
            // 生成令牌准备缓存数据,写入缓存
            // 把令牌返回到客户端去
            $openid = $wxResult['openid'];
        }
    }
    

    使用微信小程序的wx.login方法获取code进行测试
    查看微信接口返回数据,如下表示成功

    数据库中插入用户并将用户信息存入缓存

    applicationapimodelUser

    class User extends BaseModel
    {
        /**
         * 用户是否存在
         * 存在返回uid,不存在返回0
         */
        public static function getByOpenID($openid)
        {
            $user = User::where('openid', '=', $openid)
                ->find();
            return $user;
        }
    }
    

    applicationapiserviceUserToken.php

    class UserToken
    {
        private function grantToken($wxResult){
            // 拿到openid
            // 到数据库看一下,这个openid是不是已存在
            // 如果存在,则不处理,如果不存在则新增一条user记录
            // 生成令牌准备缓存数据,写入缓存
            // 把令牌返回到客户端去
            $openid = $wxResult['openid'];
            $user = UserModel::getByOpenID($openid);
            if ($user) {
                $uid = $user->id;
            }        
            else { //用户不存在
                $uid = $this->newUser($openid);
            }
            $cachedValue = $this->prepareCachedValue($wxResult, $uid);
        }
        // 创建新用户
        private function newUser($openid)
        {
            // 有可能会有异常,如果没有特别处理
            // 这里不需要try——catch
            // 全局异常处理会记录日志
            // 并且这样的异常属于服务器异常
            // 也不应该定义BaseException返回到客户端
            $user = User::create(
                [
                    'openid' => $openid
                ]);
            return $user->id;
        }
    }
    

    将用户信息存入缓存

    class UserToken
    {
        // 存入缓存的键值对如下:
        // key: 令牌
        // value: [wxResult, uid(数据库中用户唯一身份), scope(权限)]
        // 此方法用于构建value
        private function prepareCachedValue($wxResult,$uid) {
            $cachedValue = $wxResult;
            $cachedValue['uid'] = $uid;
            $cachedValue['scope'] = 16; //数字越大,权限越大
            return $cachedValue;
        }
    
        private function saveToCache($cacheValue) {
            //...
        }
    }
    

    生成token

    生成token的方法需要复用,写在基类Token
    让UserToken继承基类Token
    applicationapiserviceToken

    class Token
    {
        public static function generateToken()
        {
            //32个字符组成的随机字符串
            $randChar = getRandChar(32);
            //当前访问时间戳
            $timestamp = $_SERVER['REQUEST_TIME_FLOAT'];
            //salt 盐,特殊加密信息
            $tokenSalt = config('secure.token_salt');
            // 用三组字符串,进行md5加密
            return md5($randChar . $timestamp . $tokenSalt);
        }
    }
    

    定义一个获取随机字符串的通用方法

    applicationlibcommon.php

    function getRandChar($length)
    {
        $str = null;
        $strPol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
        $max = strlen($strPol) - 1;
    
        for ($i = 0;
             $i < $length;
             $i++) {
            $str .= $strPol[rand(0, $max)];
        }
    
        return $str;
    }
    

    敏感信息配置文件
    applicationextrasecure

    return [
        'token_salt' => 'your_salt'
    ];
    

    缓存时间配置
    applicationextrasetting.php

    return [
        'token_expire_in' => '7200'
    ];
    

    applicationapiserviceUserToken

    class UserToken extends Token
    {
        private function saveToCache($wxResult)
        {
            $key = self::generateToken();
            //转成字符串
            $value = json_encode($wxResult);
            $expire_in = config('setting.token_expire_in');
            //tp5的cache缓存方法,当前项目使用文件缓存
            $result = cache($key, $value, $expire_in);
            if (!$result){
                throw new TokenException([
                    'msg' => '服务器缓存异常',
                    'errorCode' => 10005
                ]);
            }
            return $key; //返回令牌
        }
    }
    

    异常处理
    applicationlibexceptionTokenException.php

    <?php
    namespace applibexception;
    
    /**
     * token验证失败时抛出此异常 
     */
    class TokenException extends BaseException
    {
        public $code = 401;
        public $msg = 'Token已过期或无效Token';
        public $errorCode = 10001;
    }
    
    class UserToken extends Token
    {
        private function grantToken($wxResult){
            $openid = $wxResult['openid'];
            $user = UserModel::getByOpenID($openid);
            if ($user) {
                $uid = $user->id;
            }        
            else { //用户不存在
                $uid = $this->newUser($openid);
            }
            $cachedValue = $this->prepareCachedValue($wxResult, $uid);
            $token = $this->saveToCache($cachedValue)
            return $token; //返回令牌
        }
    }
    

    小程序测试工具

    let baseUrl = 'http://localhost:8000/public/index.php/api/v1'
    Page({
      getToken () {
        wx.login({
          success (res) {
            var code = res.code;
            console.log(code, 'code');
            wx.request({
              url: baseUrl + '/token/user',
              data: {
                code
              },
              method: 'POST',
              success (res) {
                console.log(res.data);
                wx.setStorageSync('token', res.data.token)
              },
              fail (res) {
                console.log(res.data);
              }
            })
          }
        })
      }
    })
    

    商品详情分析与初步编写

    新建路由
    application oute.php

    Route::get('api/:version/product/:id', 'api/:version.Product/getOne');
    

    applicationapicontrollerv1Product

    class Product
    {
        public function getOne($id)
        {
            (new IDMustBePositiveInt())->goCheck();
        }
    }
    

    applicationapimodelProduct

    class Product
    {
        public static function getProductDetail($id)
        {
    
        }
    }
    

    一张商品详情图片只能属于一个商品,一个商品可以有多个商品详情图片
    product_property商品参数和product商品也是一对多关系
    product_image表只能通过img_id从image表间接拿到url

    product_image表

    product_property表

    定义模型关联

    class Product
    {
        public static function getProductDetail($id)
        {
    
        }
        public function imgs()
        {
            return $this->hasMany('ProductImage', 'product_id', 'id');
        }
        public function properties()
        {
            return $this->hasMany('ProductProperty', 'product_id', 'id');
        }
    }
    

    定义两个模型
    applicationapimodelProductImage.php

    <?php
    namespace appapimodel;
    
    class ProductImage extends BaseModel
    {
        protected $hidden = ['img_id', 'delete_time', 'product_id'];
        public function imgUrl()
        {
            return $this->belongsTo('Image', 'img_id', 'id');
        }
    }
    

    applicationapimodelProductProperty.php

    <?php
    
    namespace appapimodel;
    
    class ProductProperty extends BaseModel
    {
        protected $hidden=['product_id', 'delete_time', 'id'];
    }
    

    applicationapimodelProduct

    class Product
    {
        public static function getProductDetail($id)
        {
            //注意with的参数不能加空格
            $products = self::with('imgs,properties')->find($id);
            return $products;
        }
    }
    

    applicationapicontrollerv1Product

    class Product
    {
        public function getOne($id)
        {
            (new IDMustBePostiveInt())->goCheck();
            $product = ProductModel::getProductDetail($id);
            if (!$product) {
                throw new ProductException();
            }
            return $product;
        }
    }
    

    路由变量规则与分组

    application oute.php

    // Route::get('api/:version/product/:id', 'api/:version.Product/getOne')
    // Route::get('api/:version/product/recent', 'api/:version.Product/getRecent')
    // 以上的情况会导致recent接口不可用,recent会被当成id
    // 使用正则,限定参数必须是正整数
    Route::get('api/:version/product/:id', 'api/:version.Product/getOne', [], ['id' => 'd+']) 
    Route::get('api/:version/product/recent', 'api/:version.Product/getRecent')
    

    分组路由效率更高

    Route::group('api/:version/product', function () {
        Route::get('/by_category',  'api/:version.Product/getAllInCategory')
        Route::get('/:id', 'api/:version.Product/getOne', [], ['id' => 'd+'])
        Route::get('/recent', 'api/:version.Product/getRecent')
    })
    

    闭包函数构建查询器

    添加嵌套关联

    applicationapimodelProduct

    class Product
    {
        public static function getProductDetail($id)
        {
            $products = self::with('imgs.imgUrl,properties')->find($id);
            return $products;
        }
    }
    

    按照order字段进行排序
    applicationapimodelProduct

    class Product
    {
        public static function getProductDetail($id)
        {
            $products = self::with([
                'imgs' => function($query) {
                    $query->with(['imgUrl'])->order('order', 'asc');
                }
            ])->with(['properties'])->find($id);
            return $products;
        }
    }
    

    查看返回结果

    用户收货地址,通过令牌获取用户标识

    收货地址需要接口保护
    applicationapicontrollerv1Address

    class Address
    {
        public function createOrUpdateAddress ()
        {
    
        }
    }
    

    新建路由

    Route::get('api/:version/address', 'api/:version.Address/createOrUpdateAddress');
    

    user和user_address是一对一关系,本项目限定了一个用户只能有一个收货地址

    applicationapivalidateAddressNew

    class AddressNew extends BaseValidate
    {
        //uid不能做为接口的参数传,令牌是A用户的,如果参数里的uid是B用户的,就会导致A用户更改B用户的收货地址
        //项目将使用令牌换取uid
        protected $rule = [
            'name' => 'require|isNotEmpty',
            'mobile' => 'require|isMobile',
            'province' => 'require|isNotEmpty',
            'city' => 'require|isNotEmpty',
            'country' => 'require|isNotEmpty',
            'detail' => 'require|isNotEmpty',
        ]
    }
    

    新增一个手机号验证器
    applicationapivalidateBaseValidate.php

    class BaseValidate extends Validate
    {
        protected function isMobile($value)
        {
            $rule = '^1(3|4|5|7|8)[0-9]d{8}$^';
            $result = preg_match($rule, $value);
            if ($result) {
                return true;
            } else {
                return false;
            }
        }
    }
    

    面向对象的方式封装获取uid方法

    class Address
    {
        public function createOrUpdateAddress()
        {
            (new AddressNew()) -> goCheck();
            // 根据Token来获取uid
            // 根据uid 查找用户数据,判断用户是否存在,如果不存在抛异常
            // 获取用户从客户端提交来的地址信息
            // 根据用户地址信息是否存在,从而判断是添加地址还是更新地址
    
        }
    }
    

    applicationapiserviceToken

    class Token
    {
        
        public static function getCurrentTokenVar($key) {
            //获取http请求头的token
            $token = Request::instance()->header('token');
            //获取缓存中的值
            $vars = Cache::get($token);
            if (!$vars) {
                throw new TokenException();
            }
            else{
                if (!is_array($vars))
                {
                    $vars = json_decode($vars, true);
                }
                if (array_key_exists($key, $vars)) {
                    return $vars[$key];
                }
                else {
                    throw new Exception('尝试获取的Token变量并不存在');
                }
            }
        }
    
        public static function getCurrentUid() {
            //token
            $uid = self::getCurrentTokenVar('uid');
            return $uid;
        }
    }
    

    applicationapicontrollerv1Address.php

    class Address
    {
        public function createOrUpdateAddress()
        {
            (new AddressNew())->goCheck();
            $uid = TokenService::getCurrentUid();
            $user = UserModel::get($uid);
            if (!$user) {
                throw new UserException();
            }
        }
    }
    

    applicationlibexceptionUserException.php

    class UserException extends BaseException
    {
        public $code = 404;
        public $msg = '用户不存在';
        public $errorCode = 60000;
    }
    

    applicationapicontrollerv1Address.php

    class Address
    {
        public function createOrUpdateAddress ()
        {
            (new AddressNew()) -> goCheck();
            $uid = TokenService::getCurrentUid();
            $user = UserModel::get($uid);
            if (!$user) {
                throw new UserException();
            }
            $dataArray = getDatas(); //伪代码,先讲下一步
            $userAddress = $user->address;
            if (!$userAddress)
            {
                $user->address()->save($dataArray);
            }
            else {
                $user->address->save($dataArray);
            }
            //不需要返回模型,返回一个成功的提示
            return new SuccessMessage();
        }
    }
    

    在user模型新建关联关系,以读取地址
    applicationapimodelUser.php

    class User
    {
        public function address()
        {
            //一对一关系,在拥有外键的一方就是用belongTo,没有就用hasOne
            return $this->hasOne('UserAddress', 'user_id', 'id'); 
        }
    }
    

    applicationapimodelUserAddress.php

    class UserAddress extends BaseModel
    {
        //
    }
    

    applicationlibexceptionSuccessMessage.php

    <?php
    
    namespace applibexception;
    
    class SuccessMessage extends BaseException
    {
        //201创建成功
        public $code = 201;
        public $msg = 'ok';
        public $errorCode = 0;
    }
    

    用户收货地址,参数过滤

    当前没有限制用户传多余参数,可能会影响到正常的数据,以下是限制方法
    applicationapivalidateBaseValidate

    class BaseValidate extends Validate
    {
        public function getDataByRule($arrays){
            if (array_key_exists('user_id', $arrays) | array_key_exists('uid', $arrays)) {
                //可能是恶意请求
                throw new ParameterException([
                    'msg' => '参数中包含有非法的参数名user_id或者uid'
                ]);
            }
            $newArray = [];
            foreach ($this->rule as $key=>$value)
            {
                $newArray[$key] = $arrays[$key];
            }
            return $newArray;
        }
    
    }
    

    更改刚刚的伪代码
    applicationapicontrollerv1Address.php

    class Address
    {
        public function createOrUpdateAddress ()
        {
            $validate = new AddressNew();
            $validate  -> goCheck();
    
            $uid = TokenService::getCurrentUid();
            $user = UserModel::get($uid);
            if (!$user) {
                throw new UserException();
            }
            $dataArray = $validate->getDataByRule(input('post.'));
    
            $userAddress = $user->address;
            if (!$userAddress)
            {
                $user->address()->save($dataArray);
            }
            else {
                $user->address->save($dataArray);
            }
            //使用自定义状态码
            return json(new SuccessMessage(), 201);
        }
    }
    
  • 相关阅读:
    精品教程Android程序的建立以及发布
    项目源码Android团购类客户端
    精品教程Android4.1开发环境搭建
    项目源码Android本地视频播放器
    精品教程Android组件详解
    精品教程NDK环境搭建(2)NDK的安装与配置
    ios 块语法的本质
    ios 属性易错点
    ios 打印结构体的方法
    ios开发 UITableCell 可重用问题
  • 原文地址:https://www.cnblogs.com/Qyhg/p/14116821.html
Copyright © 2011-2022 走看看