微信小程序电商实战(五)

微信登录与令牌

初识Token—意义与作用

在这里插入图片描述
在这里插入图片描述
在API中无法进行正常意义上的登陆,以令牌系统代替之。用户通过【账号/密码】调用getToken接口,获取一个token。
业务接口【如:下单】被访问时,通过token令牌确认用户有效性和权限。
最简单通用的权限校验模型:
1验证是否合法(是否有Token)。
2验证Token是否有效。
3验证 Token 对应权限分组是否有权限。

微信身份体系设计

在这里插入图片描述
我们借助微信的权限验证体系,不需要自己传递账号密码,相应的是小程序会为每一个登录的用户生成一个 code 码,需要将 code 码传向 getToken 接口。code 码是去微信服务器换取用户标识的凭证。我们在 getToken 中接收到 code 码之后就需要想服务器发送一个请求,将 code 码传递给微信服务器,获取到 openid 和 session_key。openid 就是身份用户的唯一标识。而 session_key 可以用来解密从服务器拿到的加密信息。之所以要解密,是因为里面包含一个变量,叫做 unionid,unionid 也是用户的唯一标识,但是与 openid 的区别是:不同微信小程序同一个账户的 openid 不同,而 unionid 相同。unionid 经常用于不同小程序之间的关联。

我们接下来需要存储 openid,由于该标识不具有时效期,安全性差,不能将 openid 直接返回到小程序。解决方法就是生成一个有时效期的 Token 令牌,将令牌返回至客户端。下一次用户访问时,需要携带令牌,间接地拿到 openid。

如果把 openid 和 Token 令牌全都记录在数据库里,每次访问 API 都需要携带,每次查询数据库会增大数据库的压力,为了减少数据库的压力,我们可以用数据库存储 openid,而 Token 令牌则缓存到本地,可以节约数据库资源,也可以加快用户的访问速度。但是我们要注意,缓存的维护十分困难,需要合理的使用缓存。
在这里插入图片描述

实现Token身份权限体系

目录
在这里插入图片描述
路由:

Route::post('api/:version/token/user','api/:version.Token/getToken');

按道理获取一个令牌,遵循REST原则应该用GET,但是由于传递参数code有一定安全性需求,用GET只能放在url路径里,而用POST可以把code参数放在POST的body里,从而把code隐藏起来,稍微提高一下安全性。

业务层分层:
一般简单的方法业务在model里编写,处理一些细粒度的简单的业务,模型层还有一重要的功能,负责调用数据库访问层,然后实现数据表的增删改查,但是业务比较复杂的话需要写在service,service是建立在model之上的,model的类名需要和数据库表名相对应的,例如模型User对应数据表user。service里的类的命名没有限制,无需和数据表一一对应。
获取token的业务逻辑比较复杂所以写在service层。

控制器Token.php的伪代码:

class Token
{
    public function getToken($code=''){
        (new TokenGet())->goCheck();
        $ut = new UserToken();
        $token = $ut->get($code);
        return $token;
    }
}

补充知识:
this是指向对象实例的一个指针,self是对类本身的一个引用,parent是对父类的引用。
static声明的静态方法里不可以使用$this 需要使用self来引用当前类中的方法或是变量。

获取openid:
wx.login API
小程序登录

首先在extra\wx.php配置参数app_id、app_secret、login_url

service/UserToken:

<?php


namespace app\api\service;


use app\lib\exception\TokenException;
use app\lib\exception\WeChatException;
use think\Exception;
use app\api\model\User as UserModel;

class UserToken extends Token
{
    protected $code;
    protected $wxAppID;
    protected $wxAppSecret;
    protected $wxLoginUrl;

    function __construct($code)//构造函数将wxLoginUrl拼写完整
    {
        $this->code = $code;
        $this->wxAppID = config('wx.app_id');
        $this->wxAppSecret = config('wx.app_secret');
        $this->wxLoginUrl = spintf(config('wx.login_url'),$this->wxAppID,$this->wxAppSecret,$this->code);
    }

    public function get(){
        //调用微信提供给我们的loginAPI 发送http请求 请求访问指定地址的url 从而获取需要的openid的等
        $result=curl_get($this->wxLoginUrl);//$result是字符串 需要变为数组或者对象
        $wxResult = json_decode($result,true);//字符串变为数组
        if(empty($wxResult)){//外部获取的结果一般都需要判断
            throw new Exception('获取session_key和openID异常,微信内部错误');//调用TP5自带的异常,不返回到客户端,会记录成日志
        }
        else{
            $loginFail = array_key_exists('errcode',$wxResult);//判断errcode是否存在 如果接口有问题微信会返回errcode码
            if($loginFail){
                $this->processLoginError($wxResult);
            }
            else{
                $this->grantToken($wxResult);
            }
        }
    }
    //异常处理函数
    private function processLoginError($wxResult){
        throw new WeChatException([//返回给客户端 因为$wxResult中包含微信具体错误提示异常
            'msg'=>$wxResult['errmsg'],
            'errorCode'=>$wxResult['errcode']
        ]);
    }

    private function grantToken($wxResult){
        //1拿到openid
        //2去数据库看一下,这个openid是否存在
        //3如果存在则不处理,如果不存在那么新增一条user记录
        //4生成令牌,准备缓存数据,写入缓存
        //5把令牌返回到客户端去
        //缓存-键值对
        //key;令牌-----随机的字符串
        //value:wxResult(包含openid和sessi_key),uid,scope(决定用户身份 权限)
        $openid = $wxResult['openid'];//1
        //2 3
        $user = UserModel::getByOpenID($openid);//$user为模型形式
        if($user){
            $uid=$user->id;
        }
        else{
            $uid = $this->newUser($openid);
        }
        //4 5
        $cachedValue = $this->prepareCachedValue($wxResult,$uid);
        $token = $this->saveToCache($cachedValue);
        return $token;
    }

    private function newUser($openid){
        $user = UserModel::create([//在数据库写入数据
           'openid'=>$openid
        ]);
        return $user->id;
    }

    //准备缓存中value的数据
    private function prepareCachedValue($wxResult,$uid){
        $cachedValue = $wxResult;
        $cachedValue['uid'] = $uid;
        $cachedValue['scope']=16;//数字越大,权限越大
        return $cachedValue;
    }
    //写入缓存 缓存应该是字符串
    private function saveToCache($cachedValue){
        $key = self::generateToken();
        $value = json_encode($cachedValue);//数组转换为字符串
        $expire_in = config('setting.token_expire_in');//缓存时间
        //TP5提供助手函数cache写入缓存 本项目使用文件缓存系统 也可以用redis等其他缓存
        $request = cache($key,$value,$expire_in);
        if(!$request){
            throw  new TokenException([
                'msg'=>'服务器缓存异常',
                'errorCode'=>10005
            ]);
        }
        return $key;
    }
}

注意:令牌过期时间转换为缓存过期时间

本项目有两种Token(UserToken和AppToken),所以创建一个Token基类,把公共方法写入基类中,例如generateToken()方法
service/Token:

<?php

namespace app\api\service;


class Token//令牌就是一组随机的字符串
{
    public static function generateToken(){//静态方法就是不需要实例化就可以访问的,也可以理解为所有对象共享的方法
        //32个字符组成一组随机字符串
        $randChars = getRandChar(32);
        //用三组字符串,进行md5加密
        $timestamp = $_SERVER['REQUEST_TIME_FLOAT'];//时间戳
        //salt 盐
        $salt = config('secure.token_salt');
        return md5($randChars.$timestamp.$salt);
    }
}

在这里插入图片描述
在这里插入图片描述
common(写在里面的方法直接可以调用):

function curl_get($url, &$httpCode = 0)//发送http请求的设置
{
    $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;
}

function getRandChar($length)//Token里是要32位 但是别的地方可能不是32位 所以$length不能写死 而是通过传参 动态写入
{
    $str = null;
    $strPol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
    $max = strlen($strPol) - 1;

    for ($i = 0;
         $i < $length;
         $i++) {
        $str .= $strPol[rand(0, $max)];
    }

    return $str;
}

控制器Token:

class Token
{
    public function getToken($code=''){
        (new TokenGet())->goCheck();
        $ut = new UserToken($code);//UserToken构造函数传递$code
        $token = $ut->get();//get返回的是字符串,不要单纯的把字符串返回给客户端 应该返回json形式的
        return [
            'token'=>$token//改成关联数组 框架会默认序列化位json形式
        ];
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章