微信小程序一键登录可行性方案
背景说明
微信小程序的用户信息获取接口经过多次迭代后,目前已无法直接通过 API 获取用户的真实头像、昵称等信息。微信官方推荐的最佳实践是引导用户在个人中心或设置页面主动完善资料,但许多产品希望支持用户点击授权按钮即可完成登录。为此,我们可以利用微信公众号网页接口限制较宽松的特点,设计如下实现流程。
方案难点
- 微信服务号 / 公众号需配置服务器,用于接收并处理微信回调到网页的请求;
- 需要实现 webview 内网页与小程序之间的通信。
具体实现流程
1. 微信小程序端处理
小程序需包含一个专门用于加载网页的 webview 路由,核心是拼接微信授权网页的 URL 并跳转至该 webview。代码示例如下:
// 微信公众号/服务号的 appid,可登录微信公众平台 -> 设置与开发 -> 开发接口管理
const appId = 'wx1234'
/*
* 这里需要填写公众号/服务号配置的域名(此域名也需要配置到小程序的安全域名里面)
* PS. 推荐使用 https , 因为小程序安全域名只支持 https
*/
const redirectUri = 'https://xxxx.site/wechat/callback'
const encodedRedirectUri = encodeURIComponent(redirectUri)
const scope = 'snsapi_userinfo' // 获取用户信息
// 这里的 crypto 由 crypto-js 提供,高版本使用此代码会报错,建议固定版本为:3.3.0
const state = crypto.enc.Hex.stringify(crypto.lib.WordArray.random(16))
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${encodedRedirectUri}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`
uni.redirectTo({
// 这里的 url 用小程序配置的 web-view 路由
url: `/pages/webview/index?url=${encodeURIComponent(url)}`,
})
公众号/服务号配置
用户点击授权后,微信会将请求跳转至redirectUri
对应的地址,因此需提前在公众号后台配置该地址。以公众号为例,配置路径为:设置与开发 → 开发接口管理 → 服务器配置。
配置时需在服务器上部署验证接口,用于完成微信的服务器验证。代码示例(基于 Express)如下:
const express = require("express");
const crypto = require("crypto-js");
const app = express();
const port = 3000;
app.get("/", (req, res) => {
res.send("Hello World!");
});
// 将在 服务号/公众号 后台配置的 Token 值填入这里
const WECHAT_TOKEN = 'wechat'
/**
* 专门用于微信服务号/公众号注册服务器时验证 Token 时启动
*/
app.get('/wechat/register', (req, res) => {
try {
// 从请求参数中获取微信服务器发送的验证信息
const { signature, timestamp, nonce, echostr } = req.query;
// 验证参数是否完整
if (!signature || !timestamp || !nonce || !echostr) {
return res.status(400).send("缺少必要参数");
}
// 将token、timestamp、nonce按字典序排序
const sorted = [WECHAT_TOKEN, timestamp, nonce].sort();
// 拼接并进行SHA1加密
const hashcode = crypto.SHA1(sorted.join("")).toString(crypto.enc.Hex);
// 打印调试信息
console.log(
`验证信息 - 计算的hash: ${hashcode}, 接收的signature: ${signature}`
);
// 验证通过则返回echostr,否则返回空
if (hashcode === signature) {
res.status(200).send(echostr);
} else {
res.send("");
}
} catch (error) {
console.error('验证出错', error.message)
res.send('')
}
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});
配置完成后,在公众号后台点击 “提交” 即可生效(注意:服务器地址必须使用 HTTPS 协议,因小程序安全域名仅支持 HTTPS)。
3. 编写微信授权回调接口与页面
用户确认授权后,微信会跳转至redirectUri
并携带code
参数。服务器需通过该code
获取用户信息,具体流程如下:
-
获取访问令牌:使用
code
、公众号appid
和secret
调用微信接口换取access_token
; -
获取用户信息:使用
access_token
和openid
调用微信接口获取用户详情; - 传递信息至小程序:将用户信息注入 HTML 页面,通过微信 JS-SDK 跳转回小程序并携带信息。
后端接口代码示例:
// 处理微信授权回调
async function handleWechatCallback(code) {
try {
const appId = process.env.WECHAT_APP_ID;
const appSecret = process.env.WECHAT_APP_SECRET;
console.log('开始处理微信授权回调');
console.log('参数信息:', {
appId,
code,
redirectUri: process.env.WECHAT_REDIRECT_URI
});
// 获取访问令牌
console.log('正在请求访问令牌...');
const tokenResponse = await axios.get(
`https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code`
);
console.log('访问令牌响应:', {
status: tokenResponse.status,
statusText: tokenResponse.statusText,
data: tokenResponse.data
});
const { access_token, openid } = tokenResponse.data;
// 获取用户信息
console.log('正在获取用户信息...');
const userInfoResponse = await axios.get(
`https://api.weixin.qq.com/sns/userinfo?access_token=${access_token}&openid=${openid}&lang=zh_CN`
);
console.log('用户信息响应:', {
status: userInfoResponse.status,
statusText: userInfoResponse.statusText,
data: userInfoResponse.data
});
return userInfoResponse.data;
} catch (error) {
console.error('微信授权回调处理失败:', {
message: error.message,
response: error.response ? {
status: error.response.status,
statusText: error.response.statusText,
data: error.response.data
} : 'No response',
stack: error.stack
});
throw error;
}
}
app.get('/wechat/callback', async (req, res) => {
console.log('收到微信回调请求:', {
query: req.query,
headers: req.headers,
ip: req.ip
});
try {
const { code } = req.query;
if (!code) {
console.error('缺少授权码');
return res.status(400).json({
success: false,
error: 'Missing authorization code'
});
}
console.log('开始处理授权码:', code);
const userInfo = await handleWechatCallback(code);
console.log('成功获取用户信息,准备返回响应');
const templatePath = path.join(__dirname, 'public', 'callback.html');
fs.readFile(templatePath, 'utf8', (err, data) => {
if (err) {
console.error('Error reading template:', err);
return res.status(500).send('Error loading page');
}
console.log('userInfo', userInfo);
// 替换所有出现的 {{userInfo}}
const html = data.replace(/\{\{userInfo\}\}/g, JSON.stringify(userInfo));
res.send(html);
});
} catch (error) {
console.error('处理微信回调失败:', {
error: error.message,
stack: error.stack
});
res.status(500).json({
success: false,
error: error.message
});
}
});
用于传递信息的 HTML 页面代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登陆</title>
<!-- 注意:这里必须是 https 地址,因为这个这个网页的地址是 https 不能引用 http 资源 -->
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
<script>
function objectToQueryString(obj, prefix = '') {
if (obj === null || typeof obj !== 'object') {
return encodeURIComponent(prefix) + '=' + encodeURIComponent(obj);
}
const queryParts = [];
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
const fullKey = prefix ? `${prefix}[${key}]` : key;
if (value !== null && typeof value === 'object') {
if (Array.isArray(value)) {
// 处理数组
value.forEach((item, index) => {
const arrayKey = `${fullKey}[${index}]`;
if (typeof item === 'object' && item !== null) {
queryParts.push(objectToQueryString(item, arrayKey));
} else {
queryParts.push(encodeURIComponent(arrayKey) + '=' + encodeURIComponent(item));
}
});
} else {
// 处理嵌套对象
queryParts.push(objectToQueryString(value, fullKey));
}
} else {
// 处理基本类型值
queryParts.push(encodeURIComponent(fullKey) + '=' + encodeURIComponent(value));
}
}
}
return queryParts.filter(part => part.length > 0).join('&');
}
if(wx.miniProgram){
const userInfo = {{userInfo}}
wx.miniProgram.redirectTo({url:"/pages/callback/index?" + objectToQueryString(userInfo)})
wx.miniProgram.postMessage({ data: userInfo });
}
</script>
</body>
</html>
4. 小程序回调页处理
在小程序中创建/pages/callback/index
页面,在onLoad
生命周期中接收 URL 参数中的用户信息,即可完成登录逻辑处理:
<script setup>
import { onLoad } from '@dcloudio/uni-app'
onLoad((params) => {
if (!params) {
return toast('获取用户信息失败', 'error')
}
handleLogin(
decodeURIComponent(params.nickname),
decodeURIComponent(params.headimgurl),
)
})
handleLogin(nickname, headimgurl) {
// 登录逻辑实现
console.log('用户信息', nickname, headimgurl);
// ...
}
</script>
总结
该方案通过借助微信公众号网页接口的特性,间接实现了小程序一键授权登录的需求,核心在于公众号服务器配置与 webview 和小程序的跨端通信。需注意的是,这一方案属于基于当前平台规则的灵活实现,可能会受微信生态政策调整的影响。关键技术点包括公众号服务器验证、OAuth2.0 授权流程及 webview 与小程序的通信机制。