前言
晓杰网站增加了多个登录方案包括公众号扫码登录,公众号快捷登录,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下面
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。