1、使用 composer 安装 EasyWeChat
$ composer require overtrue/wechat:~4.0 -vvv
或者在composer.json文件renquire里面添加
"overtrue/wechat": "4.2.11",
接着 composer update 就可以了,不会用composer的需要现在本地配置一下,这里要提示一下如果你的php版本没有达到7.4以上不建议装高版本的EasyWeChat,一般4.x就可以了,目前遇到的问题都可以解决。
2、数据库字段准备
DROP TABLE IF EXISTS `fa_user`;
CREATE TABLE `fa_user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`platform` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '平台',
`unionid` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '厂商ID',
`openid` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT 'openid',
`nickname` varchar(60) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '昵称',
`avatar` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '头像',
`mobile` varchar(11) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '手机号',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `indexes` (`openid`,`unionid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPACT COMMENT='会员表';
在这里字段我就不全部展示了,这里我要说一下索引的问题,我在 fa_user
表里面添加了一个唯一组合索引目的是为了防止生成重复数据,索引方式是 B + 树
模式,因为我小程序和公众号两个平台登录所以我绑定的微信开放平台多一个 unionid
字段,在索引里面要注意如果这里 unionid
没有要设立为NULL
值,虽然为空值也是没毛病的那是因为这里是组合索引只要组合值唯一就可以了,如果是单个唯一索引一定不能为空,唯一索引最多允许有一条记录为空到了第二条数据出现就会出错了,因为违反唯一值约束了,都是空值啊,空值也是相同的一种,为什么为 NULL
就可以呢?如果多条数据为 NULL
那不应该也算是重复数据吗,在 MYSQL
的 InnoDB
表引擎中是允许在唯一索引的字段中出现多个 NULL
值的,因为根据 NULL
的定义表示的是未知,因此两个 NULL
比较的结果既没有相等,也没有不相等,所以结果仍然是未知。根据这个定义,多个 NULL
值的存在也是不违反唯一约束的,所以是合理的,在oracel也是如此,这里是比较基础的索引概念知识但是也容易错,所以我特别的花费一段篇幅交代一下,这里是细节问题很容易出错要当一回事去看待!
3、配置公众号和小程序基础信息,我个人比较喜欢把公用信息放在公共类里面,这样会方便调用,当然你也可以放在其他地方,只要基于命名空间可以去调用它就可以了
<?php
namespace app\common\library;
use app\common\model\Config;
use EasyWeChat\Factory;
/**
* 微信登录模型
*/
class Wechat
{
protected $app;
protected $config;
public function __construct($platform)
{
$this->setConfig($platform);
switch ($platform) {
case 'wxOfficialAccount': //微信公众号
$this->app = Factory::officialAccount($this->config);
break;
case 'wxMiniProgram': //微信小程序
$this->app = Factory::miniProgram($this->config);
break;
}
}
// 返回实例
public function getApp()
{
return $this->app;
}
//小程序:获取openid&session_key
public function code($code)
{
return $this->app->auth->session($code);
}
public function oauth()
{
return $this->app->oauth;
}
//解密信息
public function decryptData($session, $iv, $encryptData)
{
return $this->app->encryptor->decryptData($session, $iv, $encryptData);
}
public function unify($orderBody)
{
return $this->app->order->unify($orderBody);
}
public function bridgeConfig($prepayId)
{
$jssdk = $this->app->jssdk;
$config = $jssdk->bridgeConfig($prepayId, false);
return $config;
}
public function notify()
{
return $this->app;
}
//获取accessToken
public function getAccessToken()
{
$accessToken = $this->app->access_token;
$token = $accessToken->getToken(); // token 数组 token['access_token'] 字符串
return $token;
}
/**
* 合并默认配置
*
* @param [type] $platform
* @return void
*/
private function setConfig($platform) {
$debug = config('app_debug');
$defaultConfig = [
// 指定 API 调用返回结果的类型:array(default)/collection/object/raw/自定义类名
'response_type' => 'array',
'log' => [
'default' => $debug ? 'dev' : 'prod', // 默认使用的 channel,生产环境可以改为下面的 prod
'channels' => [
// 测试环境
'dev' => [
'driver' => 'single',
'path' => ROOT_PATH . 'public/logs/wechat_login.log',
'level' => 'debug',
],
// 生产环境
'prod' => [
'driver' => 'daily',
'path' => ROOT_PATH . 'public/logs/wechat_login.log',
'level' => 'info',
],
],
],
];
$oauthConfig = [
'oauth' => [
'scopes' => ['snsapi_userinfo'],
'callback' => request()->domain() . '/api/User/wxOfficialAccountOauth'
],
];
// 获取对应平台的配置
$this->config = Config::getEasyWechatConfig($platform);
// 根据框架 debug 合并 log 配置
$this->config = array_merge($this->config, $defaultConfig);
// 根据框架 平台 合并 oauth 配置
if ($platform === 'wxOfficialAccount') {
$this->config = array_merge($this->config, $oauthConfig);
}
}
}
1、获取公众号授权code前端代码
// #ifdef H5
// 微信公众号网页登录&刷新头像昵称&绑定
wxOfficialAccountOauth() {
if ($platform.get() !== "wxOfficialAccount") {
uni.showToast({
title: "请在微信浏览器中打开",
icon: "none"
});
throw false;
}
let host = $platform.host();
let payloadObject = {
host: host,
event,
token: (event !== "login" && store.getters.isLogin) ? uni.getStorageSync("token") : ""
};
let payload = encodeURIComponent(JSON.stringify(payloadObject));
let redirect_uri = encodeURIComponent(`${API_URL}user/wxOfficialAccountOauth`);
let oauthUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=" + appid +
`&redirect_uri=${redirect_uri}&response_type=code&scope=snsapi_userinfo&state=1`;
uni.setStorageSync("lastPage", window.location.href);
window.location = oauthUrl;
},
2、后端代码
/**
* 网页授权获取code
*/
public function getWxOfficialAccountCode()
{
// TODO 前期测试用于清空值 Session::set('wechat_user', NULL);
if (!Session::has('wechat_user')) {
$wechat = new Wechat('wxOfficialAccount');
$oauth = $wechat->oauth();
return $oauth->redirect()->send();
} else {
$this->success('授权登录成功(缓存)', Session::get('wechat_user'));
}
}
/**
* 微信公众号登录、更新信息、绑定(授权页 非api)
*
* @param string $code 加密code
* @return array
*/
public function wxOfficialAccountOauth()
{
$params = $this->request->get();
$wechat = new Wechat('wxOfficialAccount');
$oauth = $wechat->oauth();
$decryptData = $oauth->user()->getOriginal();
if (empty($decryptData['openid'])) {
$this->error('code错误,请重试!');
}
/* $decryptData 输出结果
* Array (
[openid] => o5dGW6J0AIQxnM2nM549ukHrYC-8
[nickname] => 霍文霆
[sex] => 0
[language] =>
[city] =>
[province] =>
[country] =>
[headimgurl] => https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJj3N9WvKoQia9ibbIe6AtdT7vZ8cCpzK53ycnFMCFwsg3fTovotvEzvKbHXm5iaqLayc0ALe1nO1WBw/132
[privilege] => Array()
[unionid] => oqN-A6hQFIM-drK14aF8kxt0ajcA
)*/
$result = [];
try {
$user = \app\common\model\User::get([
'platform' => 'wxOfficialAccount',
'openid' => $decryptData['openid']
]);
if ($user) {
if ($user->status !== 'normal') {
$this->error(__('Account is locked'));
}
// 每次调用都会更新用户基本数据---例如用户修改头像和昵称,这样就会实时更新数据了
$user->nickname = base64_decode(base64_encode($decryptData['nickname']));
$user->avatar = $decryptData['headimgurl'];
$user->save();
// 直接登陆
$result = $this->auth->direct($user->id);
} else {
// 写入个人数据
$decryptData['headimgurl'] = $decryptData['avatar'];
$result = $this->auth->oauthRegister($decryptData, 'wxOfficialAccount');
}
} catch (\Exception $e) {
$this->error($e->getMessage());
}
if ($result) {
$wechat_user = $this->auth->getUserInfo();
// 把用户登录信息写入session中,这样可以减少服务器不必要的开销
Session::set('wechat_user', $wechat_user);
$this->success('授权登录成功', $wechat_user);
} else {
$this->error('授权登录失败了!');
}
}
接口提示:$decryptSession 解密个人用户信息只能是后端去解密,千万不要让前端解密然后后端获取,这样做后端是省事了但是风险太大了,私密信息客户端传值数据都不可信,因为 openid 和 unionid 属于个人私密信息而且关乎全局,如果这个错了那就都错了,比如:openid 前端传值 123456 也是可以的,因为后端很难去判断这个值的正确性,因为字符串长度格式都不固定,所以一定需服务端解密用户信息才是最安全的
/**
* 获取微信小程序session_key
*
* @params string $code 加密code
* @return array
*/
public function getWxMiniProgramSessionKey()
{
$code = $this->request->param('code', '');
$wechat = new Wechat('wxMiniProgram');
$decryptSession = $wechat->code($code);
if (!isset($decryptSession['session_key'])) {
$this->error('未获取session_key,请重启应用');
}
/* $decryptSession 返回信息
* Array (
[session_key] => +T6cy02bJ9JZ1qNmxPdIyA==
[openid] => o5qMj5cHODXencTC3oZOqKslhHMw
[unionid] => oqN-A6hQFIM-drK14aF8kxt0ajcA
)*/
\think\Cache::set($decryptSession['session_key'], $decryptSession, 24 * 3600); // 强制1天过期
$this->success('获取session_key', $decryptSession);
}
/**
* 微信小程序登录
*
* @params string $openid 唯一标识
* @params string $sessionKey 秘钥
* @params string $iv 偏移量
* @params string $encryptedData 加密串
* @return array
*/
public function wxMiniProgramLogin()
{
$params = $this->request->post();
// 入参
extract($params);
if (empty($iv) || empty($sessionKey) || empty($encryptedData)) {
$this->error('缺少必要参数!');
}
$result = [];
try {
$wechat = new Wechat('wxMiniProgram');
$decryptSession = \think\Cache::get($sessionKey);
if (!$decryptSession || !isset($decryptSession['openid'])) {
$this->error('未获取到登录态,请重试!');
}
$decryptUserInfo = $wechat->decryptData($sessionKey, $iv, $encryptedData); // 私密信息客户端传值数据都不可信,需服务端解密用户信息
/* $decryptUserInfo 解密信息
* Array (
[nickName] => 霍文霆
[gender] => 0
[language] => zh_CN
[city] =>
[province] =>
[country] =>
[avatarUrl] => https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKo4aG0dR4xibGA0RFaqwr7NkXtxoSlhK87nuuib0geVYLzMF3UboicfTsYpnBXwiadYhP9A908wJynfQ/132
[watermark] => Array (
[timestamp] => 1650307101
[appid] => wxe633132f7b03f458
)
)*/
$decryptUserInfo = array_merge($decryptUserInfo, $decryptSession);
//组装decryptData
$decryptData = array_change_key_case($decryptUserInfo, CASE_LOWER); // 将数组中的所有键名修改为小写
if (empty($decryptData['openid'])) {
$this->error('code错误,请重试!');
}
/* $decryptData 合并解密信息
* Array (
[nickname] => 霍文霆
[gender] => 0
[language] => zh_CN
[city] =>
[province] =>
[country] =>
[avatarurl] => https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKo4aG0dR4xibGA0RFaqwr7NkXtxoSlhK87nuuib0geVYLzMF3UboicfTsYpnBXwiadYhP9A908wJynfQ/132
[watermark] => Array (
[timestamp] => 1650310060
[appid] => wxe633132f7b03f458
)
[session_key] => oOirEkIjeu/5f9FoTmxEKQ==
[openid] => o5qMj5cHODXencTC3oZOqKslhHMw
[unionid] => oqN-A6hQFIM-drK14aF8kxt0ajcA
)*/
$user = \app\common\model\User::get([
'platform' => 'wxMiniProgram',
'openid' => $decryptData['openid']
]);
if (!empty($user)) {
if ($user->status != 'normal') {
$this->error(__('Account is locked'));
}
$user->nickname = base64_decode(base64_encode($decryptData['nickname'])); // 为了解析昵称当中的表情信息
$user->avatar = $decryptData['avatarurl'];
$user->save();
// 直接登陆
$result = $this->auth->direct($user->id);
} else {
// 写入个人数据
$decryptData['avatarurl'] = $decryptData['avatar'];
$result = $this->auth->oauthRegister($decryptData, 'wxMiniProgram');
}
} catch (\Exception $e) {
$this->error($e->getMessage());
}
if ($result) {
$this->success('授权登录成功', $this->auth->getUserInfo());
} else {
$this->error('授权登录失败了!');
}
}
/**
* 微信公众号&微信小程序写入数据
*
* @param array $decryptData 解密信息
* @param string $platform 注册平台
* @return boolean
*/
public function oauthRegister($decryptData, $platform)
{
// 分参
extract($decryptData);
$params = [
'platform' => $platform,
'unionid' => (isset($unionid) && $unionid) ? $unionid : NULL,
'openid' => $openid,
'nickname' => base64_decode(base64_encode($nickname)),
'avatar' => $avatar,
'jointime' => time(),
'joinip' => request()->ip(),
'logintime' => time(),
'loginip' => request()->ip(),
'prevtime' => time(),
'status' => 'normal'
];
Db::startTrans();
try {
$user = User::create($params, true);
$this->_user = User::get($user->id);
//设置Token
$this->_token = Random::uuid();
Token::set($this->_token, $user->id, $this->keeptime);
Db::commit();
} catch (Exception $e) {
$this->setError($e->getMessage());
Db::rollback();
return false;
}
return true;
}
问题思考:这里有的小伙伴会想能不能把获取手机号的解密信息和小程序登录时的解密信息共用呢?答案是不能的,因为偏移量和加密串每次请求都不一样,我已经试过了确实不可以。
/**
* 获取用户手机号
*
* @params string $sessionKey 秘钥
* @params string $iv 偏移量
* @params string $encryptedData 加密串
*/
public function getUserMobile()
{
$params = $this->request->post();
// 入参
extract($params);
if (empty($iv) || empty($sessionKey) || empty($encryptedData)) {
$this->error('缺少参数!');
}
try {
$wechat = new Wechat('wxMiniProgram');
$decryptData = $wechat->decryptData($sessionKey, $iv, $encryptedData);
if (!$decryptData || !isset($decryptData['phoneNumber'])) {
$this->error('获取手机号失败,请重试!');
}
$user = $this->auth->getUser();
$user->mobile = $decryptData['phoneNumber'];
$user->save();
} catch (\Exception $e) {
$this->error($e->getMessage());
}
if (!empty($user)) {
$this->success('获取成功');
} else {
$this->error('获取失败');
}
}