微信登录与令牌
初识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形式
];
}
}