最近在学习微信小程序的开发,记录一下学习与思考过程
微信官方对于小程序登录的整体框架
小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。
说明:
- 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
- 调用 code2Session 接口,换取 用户唯一标识 OpenID 和 会话密钥 session_key。
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
注意:
- 会话密钥 session_key 是对用户数据进行 加密签名的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。
- 临时登录凭证 code 只能使用一次
个人的理解
用户通过微信登录,获取令牌(代表用户身份 Token),Token 分级别(管理员、用户),在下下来获取接口,有令牌就可以 访问,没有的话就不能访问。
令牌的设计及原理
客户端 携带账号密码 访问getToken接口,在Controller层 给用户上生成令牌
在以后,客户端携带Token 访问下单接口,对比token验证身份。如果身份合法、在过期时间之前、token权限足够的情况下,对比成功。
在微信体系下,用户登录后会生成Code码,不需要重新设计令牌
小程序携带 code
访问getToken 接口,去微信服务器进行校验,返回openid
(用户身份的唯一标识)、session_key
(解密微信服务器拿到的加密信息) ,然后携带openid
生成Token
,存储openid
,最后向客户端返回Token
。这里的Token
是加密的,而为什么不直接返回openid
呢,是因为openid
只是一个字符串,返回去不是很安全,于是返回进行md5
加密的Token
。
Token的校验
为了加快访问速度,在校验Token时,是实际上将Token和用户存储到缓存(Cache)中,而并没有直接对数据库进行操作。
实现Token身份权限体系
Controller
层新建Token.php
编写getToken
接口
class Token
{
public function getToken($code = '')
{
(new TokenGet())->goCheck();
$ut = new UserToken($code);
$token = $ut->get();
return [
'token' => $token
];
}
}
新建TokenGet
校验类继承于BaseValidate
class TokenGet extends BaseValidate
{
protected $rule = [
'code' => 'require|isNotEmpty'
];
protected $message = [
'code' => '没有code还想获取Token,是不是在做梦啊!'
];
}
BaseValidate.php
protected function isNotEmpty($value){
if (empty($value)){
return false;
}else{
return true;
}
}
在Service层新建UserToken.php
class UserToken
{
public function get($code){
//暂时不写
}
}
注:在这里需要强调的是,在Model层上封装了一个
Service
层,是因为业务逻辑比较复杂。Model
除了处理业务逻辑之外,还有一个很重要的功能就是调用和访问数据库,实现数据库的增删改查,而Service
是建立在Model
之上的。所有复杂的放到Service层,简单的粒度比较小的放到Model层。
实现Token身份权限体系
前文已经提到,小程序通过wx.login(Object object)
调用接口获取登录凭证(code)
。通过凭证进而换取用户登录态信息,包括用户的唯一标识(openid)
及本次登录的会话密钥(session_key)
等。
微信小程序给一个接口地址:
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
调用此接口地址,获取openid
。所需要传入的一系列参数appid
(小程序id),secrect
(小程序秘钥),js_code
(code)。
总结:
身份权限验证成分为两步:
- 小程序自己获取
code
码; - 把
code
码发送到我们的服务器,再由我们的服务器调用微信给我们的接口,最终获取到openid
和session_key
。
写配置文件wx.php
return [
'app_id' => '自己的小程序ID',
'app_secret' => '小程序密钥',
'login_url' => "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
];
把需要传参的地方用
%s
占位符
编写UserToken.php
class UserToken extends Token
{
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);
$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 grantToken($wxResult)
{
//拿到openid
//数据库看一下,这个openid是不是已经存在
//如果存在就不处理,如果不存在就新增一条
//生成令牌,准备缓存数据,写入缓存
//把令牌返回到客户端去
$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;
}
private function saveToCache($cacheValue){
$key = self::generateToken();
$value = json_encode($cacheValue);
$expire_in = config('setting.token_expire_in');
$request = cache($key,$value,$expire_in);
if (!$request){
throw new TokenException([
'msg' => '服务器缓存异常',
'errorCode' => 10005
]);
}
return $key;
}
private function prepareCachedValue($wxResult, $uid){
$cacheValue = $wxResult;
$cacheValue['uid'] = $uid;
$cacheValue['scope'] = 16;
return $cacheValue;
}
private function newUser($openid){
$user = UserModel::create([
'openid' => $openid
]);
return $user->id;
}
private function processLoginError($wxResult)
{
throw new WechatException([
'msg' => $wxResult['errmsg'],
'errorCode' => $wxResult['errcode'],
]);
}
}
发送http
请求的curl_get()
方法放到公共文件里面 common.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;
}