网站调用微信小程序授权登录示例

前言

晓杰网站增加了多个登录方案包括公众号扫码登录,公众号快捷登录,QQ快捷登录,后面也研究了下小程序扫码登录方案,现在分享给大家。

技术栈

THinkphp5.0+Redis+Mysql

前端代码

index.html

<html>
<head>
    <title>微信小程序扫码授权登录</title>
    <meta name="wechat-enable-text-zoom-em" content="true">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="color-scheme" content="light dark">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0,viewport-fit=cover">
    <link rel="shortcut icon" type="image/x-icon" href="//res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico" reportloaderror>
    <link rel="mask-icon" href="//res.wx.qq.com/a/wx_fed/assets/res/MjliNWVm.svg" color="#4C4C4C" reportloaderror>
    <link rel="apple-touch-icon-precomposed" href="//res.wx.qq.com/a/wx_fed/assets/res/OTE0YTAw.png" reportloaderror>
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="format-detection" content="telephone=no">
    <meta name="referrer" content="origin-when-cross-origin">
    <meta name="referrer" content="strict-origin-when-cross-origin">
    <style>
        *{
            padding: 0;
            margin: 0;
        }
        .title {
            text-align: center;
            margin-top: 50px;
            font-size: 25px;
        }
        #createQrcode {
            border: none;
            padding: 12px;
            background: #07C160;
            color: #fff;
            font-size: 16px;
            border-radius: 10px;
            margin: 30px auto 0;
            display: block;
            cursor: pointer;
            outline: none;
            -webkit-tap-highlight-color:rgba(255,0,0,0);
        }

        #qrcode {
            width: 220px;
            height: 220px;
            margin: 30px auto 0;
            display: none;
        }

        #qrcode img {
            width: 220px;
            height: 220px;
        }

        #status {
            border: none;
            padding: 12px 15px;
            background: #eee;
            color: #666;
            font-size: 18px;
            border-radius: 100px;
            margin: 15 auto 0;
            display: block;
            cursor: pointer;
            outline: none;
            -webkit-tap-highlight-color:rgba(255,0,0,0);
            display: none;
        }
    </style>
</head>

<body>

<!--标题-->
<p class="title">微信小程序扫码授权登录示例</p>

<!--生成按钮-->
<button id="createQrcode" onclick="createQrcode()">生成微信小程序码</button>

<!--小程序码显示区域-->
<div id="qrcode"></div>

<!--状态-->
<button id="status"></button>

<!--scene隐藏域-->
<input type="hidden" id="scene" />

<script>

    // 定义一个全局变量来控制轮询状态
    var pollingInterval;

    // 用于记录轮询次数的变量
    var pollingCount = 0;

    // 创建小程序码
    function createQrcode() {

        var xhr = new XMLHttpRequest();
        xhr.open("GET", "createQrcode", true);

        xhr.onreadystatechange = function () {

            if (xhr.readyState === 4 && xhr.status === 200) {

                // 渲染小程序码
                var response = JSON.parse(xhr.responseText);
                document.getElementById("qrcode").style.display = "block";
                document.getElementById("qrcode").innerHTML = '<img src="data:image/jpg;base64,'+response.qrcode+'" id="miniproQrCode" />';
                document.getElementById("createQrcode").style.display = "none";
                document.getElementById("status").style.display = "block";
                document.getElementById("status").innerHTML = '请使用微信扫码';
                document.getElementById("scene").value = response.scene;

                // 重置轮询次数
                pollingCount = 0;

                // 开始轮询
                startPolling();
            }
        };
        xhr.send();
    }

    // 开始轮询
    // 1500毫秒轮询一次
    function startPolling() {
        var pollingInterval = setInterval(function () {
            pollDatabase(pollingInterval);
        }, 1500);
    }

    // 轮询扫码状态
    function pollDatabase(pollingInterval) {

        var sceneValue = document.getElementById("scene").value;
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "checkScanStatus?scene=" + sceneValue, true);

        xhr.onreadystatechange = function () {

            if (xhr.readyState === 4 && xhr.status === 200) {

                // 获取轮询结果
                var response = JSON.parse(xhr.responseText);
                document.getElementById("status").innerHTML = response.msg;

                // 轮询的信息
                console.log(response.msg)

                // 每次轮询递增计数
                pollingCount++;

                // 204状态码
                if (response.code == 204) {

                    // 修改为已取消的图片
                    document.getElementById("miniproQrCode").src = '__STATIC__/login/isCancel.png';

                    // 停止轮询
                    clearInterval(pollingInterval);
                }

                // 203状态码
                if (response.code == 203) {

                    // 修改为已扫码的图片
                    document.getElementById("miniproQrCode").src = '__STATIC__/login/isScan.png';
                }

                // 200状态码
                if (response.code == 200) {

                    // 修改为登录成功的图片
                    document.getElementById("miniproQrCode").src = '__STATIC__/login/loginSuccess.png';

                    // 登录成功的逻辑
                    // 例如修改DOM或者跳转到Url
                    // 以回调地址为例
                    // 检查URL中是否包含'?callback='
                    if (window.location.href.indexOf('?callback=') !== -1) {
                        var callbackUrl = window.location.href.split('?callback=')[1];

                        // 去掉参数部分
                        callbackUrl = callbackUrl.split('&')[0];

                        if (isValidCallback(callbackUrl)) {

                            // 添加斜杠结尾
                            callbackUrl = addTrailingSlash(callbackUrl);

                            // 跳转到回调地址并传递token
                            location.href = callbackUrl + '?token=' + response.token;

                        } else {

                            // 无需添加斜杠
                            // 跳转到回调地址并传递token
                            location.href = callbackUrl + '?token=' + response.token;
                        }
                    }

                    // 用于验证callback是不是符合格式的域名
                    function isValidCallback(callback) {

                        // 使用正则表达式验证是否是有效的域名或域名+目录
                        var pattern = /^(https?:\/\/)?([a-z\d]([a-z\d-]*[a-z\d])*\.)+[a-z]{2,}(\/\w*\/?)?$/i;
                        return pattern.test(callback);
                    }

                    // 添加/作为结尾
                    function addTrailingSlash(callback) {

                        // 如果字符串不以斜杠结尾,添加斜杠
                        if (!callback.endsWith('/')) {
                            callback += '/';
                        }
                        return callback;
                    }

                    // 停止轮询
                    clearInterval(pollingInterval);

                }else if (pollingCount >= maxPollingCount) {

                    // 修改为小程序码已过期的图片
                    document.getElementById("miniproQrCode").src = '__STATIC__/login/isExpire.png';
                    document.getElementById("status").innerHTML = '小程序码已过期,请刷新';

                    // 停止轮询
                    clearInterval(pollingInterval);
                }
            }
        };
        xhr.send();
    }

    // 设置最大轮询次数
    var maxPollingCount = 60;

</script>

</body>
</html>

后端代码

Login.php

<?php

namespace app\admin\controller;
use app\admin\model\Admin as AdminModel;;
use qqconnect\QC;
use think\cache\driver\Redis;
use think\Controller;
use think\Request;
use think\Session;
use tools\Mobile_Detect as Mobile_DetectModel;
use tools\Tools as ToolsModel;
class Login  extends Common
{
    public function getOpenid()
    {
        isset($_GET['scene']) ? $scene = $_GET['scene'] : exit(json_encode(array('code' => 0, 'msg' => '参数不能为空!'), JSON_UNESCAPED_UNICODE));
        isset($_GET['code']) ? $code = $_GET['code'] : exit(json_encode(array('code' => 0, 'msg' => '参数不能为空!'), JSON_UNESCAPED_UNICODE));
        $wxConfig = new WxConfig();
        $appidStr = $wxConfig::$appid;
        $secretStr = $wxConfig::$secret;
        // 换取openid的API
        $api = "https://api.weixin.qq.com/sns/jscode2session?appid=$appidStr&secret=$secretStr&js_code=$code&grant_type=authorization_code";
        $result = Index::httpGet($api);
        $arr_result = json_decode($result, true);
        if (empty($arr_result['session_key'])) {
            $ret = array(
                'code' => 202,
                'msg' => '授权失败'
            );
        }
        // 解析出openid
        $openid = $arr_result["openid"];
        // 验证scene参数
        $redis = new Redis();
        $redisKey = 'qrcode_'.$scene;
        $checkScene = $redis->get($redisKey);
        if (!empty($checkScene) && $checkScene['status'] == 1) {
            // 如果存在scene
            $ret = array(
                'code' => 200,
                'msg' => '已扫码',
                'openid' => $openid
            );
            $checkScene['status']=2;
            $checkScene['openid']=$openid;
            $redis->set($redisKey,$checkScene,300);
        }else{
            $ret = array(
                'code' => 201,
                'msg' => '小程序码已过期'
            );
        }
        echo json_encode($ret, JSON_UNESCAPED_UNICODE);
    }
    public function loginAuth(){
        header("content-type:application/json");
        isset($_GET['scene'])?$scene = $_GET['scene']:exit(json_encode(array('code'=>202,'msg'=>'授权失败,scene不存在'),JSON_UNESCAPED_UNICODE));
        $redis = new Redis();
        $redisKey = 'qrcode_'.$scene;
        $checkScene = $redis->get($redisKey);
        if(!empty($checkScene)) {
            $checkScene['status']=3;
            $checkScene['authTime']=date('Y-m-d H:i:s');
            $redis->set($redisKey,$checkScene,300);
            // 已授权
            $ret = array(
                'code' => 200,
                'msg' => '已授权'
            );
        }else {

            // scene不存在
            $ret = array(
                'code' => 202,
                'msg' => '授权失败,scene不存在'
            );
        }

        // 返回结果
        echo json_encode($ret, JSON_UNESCAPED_UNICODE);
    }
    public function cancelAuth(){
        header("content-type:application/json");
        isset($_GET['scene'])?$scene = $_GET['scene']:exit(json_encode(array('code'=>202,'msg'=>'取消失败,scene不存在!'),JSON_UNESCAPED_UNICODE));
        $redis = new Redis();
        $redisKey = 'qrcode_'.$scene;
        $cancelAuth = $redis->get($redisKey);
        if(!empty($cancelAuth)) {
            // 更新为取消授权且设置小程序码为过期
            $checkScene['status']=4;
            $redis->set($redisKey,$checkScene,300);
            $ret = array(
                'code' => 200,
                'msg' => '已取消授权'
            );
            $redis->rm($redisKey);
        }else {
            $ret = array(
                'code' => 202,
                'msg' => '取消失败,scene不存在'
            );
        }

        // 返回结果
        echo json_encode($ret, JSON_UNESCAPED_UNICODE);
    }
    public function checkScene(){
        header("Content-type:application/json");
        isset($_GET['scene'])?$scene = $_GET['scene']:exit(json_encode(array('code'=>202,'msg'=>'参数不能为空!'),JSON_UNESCAPED_UNICODE));
        $redis = new Redis();
        $redisKey = 'qrcode_'.$scene;
        $checkScene = $redis->get($redisKey);
        if(!empty($checkScene)){
            $result = array(
                'code' => 200,
                'msg' => '获取成功'
            );
        }else{
            $result = array(
                'code' => 204,
                'msg' => '参数错误'
            );
        }
            echo json_encode($result, JSON_UNESCAPED_UNICODE);
        }
            public function createQrcode(){
        $accessToken = $this->getAccessToken();
        $url="https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=$accessToken";
        $scene =ToolsModel::getMillisecond().rand(1000000,9999999);
        // 请求参数
        $data = array(
            "page" => "pages/authorize/index", // 小程序扫码页面的路径
            "scene" => $scene,
            "check_path" => false, // 是否验证你的路径是否正确
            "env_version" => "release" // 开发的时候这个参数是develop,小程序审核通过发布上线之后改为release
        );
        $dataReturn = ToolsModel::getCurl($url,json_encode($data));
        $redis = new Redis();
        $redisKey = 'qrcode_'.$scene;

// 向数据库插入一条生成小程序码的记录
        $dataArr = array(
            'scene' => $scene,
            'status' => 1,
            'scene' => $scene,
        );
        $redis->set($redisKey,$dataArr,600);
        $result = array(
            'code' => 200,
            'msg' => '创建成功',
            'scene' => $scene,
            'qrcode' => base64_encode($dataReturn)
        );
        echo json_encode($result, JSON_UNESCAPED_UNICODE);
    }
    private  function getAccessToken()
    {
        $redis= new Redis();
        $AccessTokenKey = 'access_token_'.Config('miniApp.APPID');
        if(!$redis->has($AccessTokenKey)){
            $times = 700;
            $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=".Config('miniApp.APPID')."&secret=".Config('miniApp.APPSECRET');
            $result =ToolsModel:: https_request($url);
            $jsoninfo = json_decode($result, true);
            $access_token = $jsoninfo['access_token'];
            if ($access_token)
            {
                $redis->set($AccessTokenKey,$access_token,$times);
            }
        }else
        {
            $access_token =  $redis->get($AccessTokenKey);
        }
        return $access_token;

    }

    public function checkScanStatus()
    {
        header("Content-type:application/json");
        isset($_GET['scene']) ? $scene = $_GET['scene'] : exit(json_encode(array('code' => 0, 'msg' => '参数不能为空!'), JSON_UNESCAPED_UNICODE));
        // 查看Scene的状态
        $redis = new Redis();
        $redisKey = 'qrcode_'.$scene;
        $checkScanStatus = $redis->get($redisKey);
        if (!empty($checkScanStatus)) {
            // 扫码状态
            $status = $checkScanStatus['status'];
            // openid
            $openid = $checkScanStatus['openid'];

            if ($status == 1) {

                // 未扫码
                $result = array(
                    'code' => 202,
                    'msg' => '请使用微信扫码'
                );

            } else if ($status == 2) {

                // 已扫码
                $result = array(
                    'code' => 203,
                    'msg' => '已扫码,请点击授权登录'
                );

            } else if ($status == 3 && $openid) {

                // 删除临时文件
                //  unlink('qrcode/' . $Scene . '.png');

                // 登录成功的处理
                // 例如存SESSION
                // 数据库操作等
                // -----------------------------------
                // 在这里编写你的逻辑
                $token = MD5($scene.Config('miniApp.APPSECRET').time());

                // 已登录
                $result = array(
                    'code' => 200,
                    'msg' => '登录成功',
                    'token' => $token
                );
                $redis->rm($redisKey);

            } else if ($status == 4) {

                // 已取消授权
                $result = array(
                    'code' => 204,
                    'msg' => '已取消授权'
                );

                // 删除临时文件
                // unlink('qrcode/' . $Scene . '.png');

            }
        } else {

            // 获取失败
            $result = array(
                'code' => 204,
                'msg' => '该二维码无法登录'
            );
        }
        echo json_encode($result, JSON_UNESCAPED_UNICODE);
    }

    public function index()
    {
        return view();
    }

小程序源码

存储位置 pages下面

file
index.js

// 获取应用实例
const app = getApp()

// 获取服务器域名和目录名
const domain = app.domain.url;
const dirName = app.dirName.dir;

Page({
    data: {
        scanStep: 1
    },

    // 获取扫码结果
    onLoad(options) {

        const that = this;
        if(options !== undefined) {
            if(options.scene) {

                // 获取scene
                let scene = decodeURIComponent(options.scene);

                wx.showLoading({
                    title: '加载中'
                })

                // 验证scene是否存在
                wx.request({
                    url: 'https://' + domain  + '/checkScene/?scene=' + scene,
                    header: {
                        'content-type': 'application/json'
                    },
                    success (res) {

                        // 输出验证结果
                        console.log(res.data)

                        // 存在
                        if(res.data.code == 200) {

                            // 微信登录
                            wx.login({
                                success (res) {
                                    if (res.code) {
                                        wx.request({
                                            url: 'https://' + domain  + '/getOpenid/?code=' + res.code + '&scene=' + scene,
                                            header: {
                                                "content-type": "application/json"
                                            },
                                            success (res) {

                                                // 成功获取到Openid
                                                if(res.data.code == 200) {

                                                    // 切换至授权界面
                                                    that.setData({
                                                        scanStep: 2,
                                                        sceneCode: scene
                                                    })
                                                }else {

                                                    // 获取失败
                                                    that.setData({
                                                        scanStep: 3,
                                                        loginSuccess: false,
                                                        errorMsg: res.data.msg
                                                    })
                                                }
                                            }
                                        });
                                    }
                                }
                            });
                        }
                        wx.hideLoading();
                    }
                })
            }
        }
    },

    // 点击授权登录
    loginAuth() {
        const that = this;
        wx.showNavigationBarLoading();
        wx.request({
            url: 'https://' + domain  + '/loginAuth/?scene=' + that.data.sceneCode,
            header: {
                'content-type': 'application/json'
            },
            success (res) {
                if(res.data.code == 200) {

                    // 切换至授权结果
                    that.setData({
                        scanStep: 3,
                        loginSuccess: true
                    })

                    wx.hideNavigationBarLoading();
                }else{

                    that.setData({
                        scanStep: 3,
                        loginSuccess: false,
                        errorMsg: res.data.msg
                    })
                }
            }
        })
    },

    // 取消授权
    cancelAuth() {
        const that = this;
        wx.request({
            url: 'https://' + domain  + '/cancelAuth/?scene=' + that.data.sceneCode,
            header: {
                'content-type': 'application/json'
            },
            success (res) {
                that.setData({
                    scanStep: 3,
                    loginSuccess: false,
                    errorMsg: res.data.msg
                })
            }
        })
    }
})

index.json

{
  "usingComponents": {}
}

index.wxml

<view class="container">

<!-- 扫描二维码 -->
<view class="scanQrcode" wx:if="{{scanStep == 1}}">

    <!-- 提醒logo -->
    <view class="tips-logo">
        <image src="../../images/tips.png"></image>
    </view>

    <!-- 扫码提示 -->
    <view class="scan-tips">请使用微信扫一扫</view>
</view>

<!-- 授权登录 -->
<view class="loginAuth" wx:if="{{scanStep == 2}}">

    <!-- 头像区域 -->
    <view class="avatar">
        <image src="../../images/warn.png"></image>
    </view>

    <!-- 昵称区域 -->
    <view class="nickname">使用微信授权登录</view>

    <!-- 授权按钮 -->
    <view class="button auth-login" bind:tap="loginAuth">授权登录</view>

    <!-- 取消授权 -->
    <view class="button cancel-login" bind:tap="cancelAuth">取消授权</view>

    <!-- 授权须知 -->
    <!--<view class="auth-know">
        <span>授权登录即同意</span>
        <span class="blue-font">xxx用户服务协议</span>和
        <span class="blue-font">xxx用户隐私协议</span>
        <span>,请阅读以上两项协议。</span>
    </view> -->
</view>

<!-- 登录结果 -->
<view class="loginResult" wx:if="{{scanStep == 3}}">

    <!-- 提醒logo -->
    <view class="tips-logo">
        <image src="../../images/success.png" wx:if="{{loginSuccess}}"></image>
        <image src="../../images/fail.png" wx:else></image>
    </view>

    <!-- 登录结果 -->
    <view class="login-success" wx:if="{{loginSuccess}}">登录成功</view>
    <view class="login-fail" wx:else>{{errorMsg}}</view>
</view>

</view>

index.wxss

.container {
    width: 93%;
    margin: 0 auto;
}

.loginAuth {
    width: 100%;
    margin: 30px auto 0;
    background: #fff;
    border-radius: 15px;
    overflow: hidden;
}
.loginAuth .avatar {
    width: 90px;
    height: 90px;
    margin: 50px auto 0;
}
.loginAuth .avatar image {
    width: 90px;
    height: 90px;
}
.loginAuth .nickname {
    text-align: center;
    color: #999;
    font-size: 33rpx;
    margin-top: 15px;
    margin-bottom: 50px;
}
.loginAuth .button {
    padding: 12px;
    width: 150px;
    margin: 0 auto;
    border-radius: 10px;
    text-align: center;
    font-size: 33rpx;
    font-weight: 400;
    margin-bottom: 10px;
}
.loginAuth .auth-login {
    background: #07c160;
    color: #fff;
}
.loginAuth .cancel-login {
    background: #eee;
    color: #666;
}

.auth-know {
    text-align: center;
    font-size: 28rpx;
    color: #999;
    width: 80%;
    margin: 35px auto 25px;
}
.auth-know .blue-font {
    color: #576b95;
    padding: 0 2px;
}

/* 扫码 */
.scanQrcode {
    width: 100%;
    margin: 30px auto 0;
    background: #fff;
    border-radius: 15px;
    overflow: hidden;
}
.scanQrcode .tips-logo {
    width: 90px;
    height: 90px;
    margin: 50px auto 0;
}
.scanQrcode .tips-logo image {
    width: 90px;
    height: 90px;
}
.scanQrcode .scan-tips {
    text-align: center;
    color: #999;
    font-size: 50rpx;
    margin-top: 15px;
    margin-bottom: 50px;
}
.scanQrcode .button {
    padding: 12px;
    width: 150px;
    margin: 0 auto;
    border-radius: 10px;
    text-align: center;
    font-size: 33rpx;
    font-weight: 400;
}
.scanQrcode .scan-qrcode {
    background: #07c160;
    color: #fff;
    margin-bottom: 25px;
}

.loginResult {
    width: 100%;
    margin: 30px auto 0;
    background: #fff;
    border-radius: 15px;
    overflow: hidden;
}
.loginResult .tips-logo {
    width: 90px;
    height: 90px;
    margin: 50px auto 0;
}
.loginResult .tips-logo image {
    width: 90px;
    height: 90px;
}
.loginResult .login-success {
    text-align: center;
    color: #333;
    font-size: 50rpx;
    margin-top: 15px;
    margin-bottom: 25px;
}
.loginResult .login-fail {
    text-align: center;
    color: #fa5151;
    font-size: 50rpx;
    margin-top: 15px;
    margin-bottom: 25px;
}

示例地址

https://soft.svip8.vip/login/index1

本文作者

Soujer