jjzjj

基于Thinkphp5+EasyWeChat+fastadmin微信小程序授权登录&获取手机号&微信公众号网页---联合授权登录

霍文霆 2024-04-07 原文

战前准备

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 那不应该也算是重复数据吗,在 MYSQLInnoDB 表引擎中是允许在唯一索引的字段中出现多个 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('授权登录失败了!');
        }
    }

在 app\common\library 添加如下方法,我是基于fastadmin开发的,也可以改写我的数据写入方式

    /**
     * 微信公众号&微信小程序写入数据
     * 
     * @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('获取失败');
        }
    }

有关基于Thinkphp5+EasyWeChat+fastadmin微信小程序授权登录&获取手机号&微信公众号网页---联合授权登录的更多相关文章

  1. ruby-on-rails - rails : "missing partial" when calling 'render' in RSpec test - 2

    我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou

  2. ruby-on-rails - 由于 "wkhtmltopdf",PDFKIT 显然无法正常工作 - 2

    我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-

  3. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  4. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

  5. ruby - 检查 "command"的输出应该包含 NilClass 的意外崩溃 - 2

    为了将Cucumber用于命令行脚本,我按照提供的说明安装了arubagem。它在我的Gemfile中,我可以验证是否安装了正确的版本并且我已经包含了require'aruba/cucumber'在'features/env.rb'中为了确保它能正常工作,我写了以下场景:@announceScenario:Testingcucumber/arubaGivenablankslateThentheoutputfrom"ls-la"shouldcontain"drw"假设事情应该失败。它确实失败了,但失败的原因是错误的:@announceScenario:Testingcucumber/ar

  6. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  7. ruby-on-rails - 如何优雅地重启 thin + nginx? - 2

    我的瘦服务器配置了nginx,我的ROR应用程序正在它们上运行。在我发布代码更新时运行thinrestart会给我的应用程序带来一些停机时间。我试图弄清楚如何优雅地重启正在运行的Thin实例,但找不到好的解决方案。有没有人能做到这一点? 最佳答案 #Restartjustthethinserverdescribedbythatconfigsudothin-C/etc/thin/mysite.ymlrestartNginx将继续运行并代理请求。如果您将Nginx设置为使用多个上游服务器,例如server{listen80;server

  8. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  9. ruby - 主要 :Object when running build from sublime 的未定义方法 `require_relative' - 2

    我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby​​1.9+ 关于ruby-主要:Objectwhenrun

  10. ruby - 无法让 RSpec 工作—— 'require' : cannot load such file - 2

    我花了三天的时间用头撞墙,试图弄清楚为什么简单的“rake”不能通过我的规范文件。如果您遇到这种情况:任何文件夹路径中都不要有空格!。严重地。事实上,从现在开始,您命名的任何内容都没有空格。这是我的控制台输出:(在/Users/*****/Desktop/LearningRuby/learn_ruby)$rake/Users/*******/Desktop/LearningRuby/learn_ruby/00_hello/hello_spec.rb:116:in`require':cannotloadsuchfile--hello(LoadError) 最佳

随机推荐