【愚公系列】2022年09月 微信小程序-微信小程序实现网页一键登录功能
【摘要】 前言如果微信小程序要获取微信登录的用户信息,需要拿到code去后台换取用户信息,具体步骤又如下:使用微信开放功能button按钮绑定点击事件为获取用户授权授权成功调用微信登录接口获取code用获取到的code去调用后台接口获取到用户的openidcode+openid去调用后台写的小程序自动登录接口获取到access_tokenaccess_token拿到就可以去查询用户信息了 一、微信小...
前言
如果微信小程序要获取微信登录的用户信息,需要拿到code去后台换取用户信息,具体步骤又如下:
- 使用微信开放功能button按钮绑定点击事件为获取用户授权
- 授权成功调用微信登录接口获取code
- 用获取到的code去调用后台接口获取到用户的openid
- code+openid去调用后台写的小程序自动登录接口获取到access_token
- access_token拿到就可以去查询用户信息了
一、微信小程序实现网页一键登录功能
首先服务端先安装两个包
npm i koa-weixin-auth --save
npm i koa-body --save
1.旧版登录方法
小程序端
<button bindgetuserinfo="login" open-type="getUserInfo" type="primary">登陆</button>
login(e) {
console.log(e);
let {userInfo,
encryptedData,
iv} = e.detail
wx.login({
success(res0) {
if (res0.code) {
//发起网络请求
wx.request({
url: 'http://localhost:3000/user/wexin-login',
method: 'POST',
header: {
'content-type': 'application/json'
},
data: {
code: res0.code,
userInfo,
encryptedData,
iv
},
success(res) {
console.log('请求成功', res.data)
getApp().globalData.token = res.data.data.authorizationToken
console.log('authorization', getApp().globalData.token)
},
fail(err) {
console.log('请求异常', err)
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
},
服务端
const WeixinAuth = require("../lib/koa2-weixin-auth")
const WXBizDataCrypt = require('../lib/WXBizDataCrypt')
// 小程序的机要信息
const miniProgramAppId = '自己的appId'
const miniProgramAppSecret = '自己的appSecret'
const weixinAuth = new WeixinAuth(config.miniProgram.appId, config.miniProgram.appSecret);
// 这是第一次小程序登陆法
router.post("/wexin-login", async (ctx) => {
let { code } = ctx.request.body
const token = await weixinAuth.getAccessToken(code);
// const accessToken = token.data.access_token;
const openid = token.data.openid;
// const userinfo = await weixinAuth.getUser(openid)
// 这个地方有一个错误,invalid credential, access_token is invalid or not latest
// 拉取不到userInfo
ctx.status = 200
ctx.body = {
code: 200,
msg: 'ok',
data: openid
}
})
2.新版登录方法
小程序端
<button bindgetuserinfo="login" open-type="getUserInfo" type="primary">登陆</button>
login(e) {
console.log(e);
let {
userInfo,
encryptedData,
iv
} = e.detail
console.log('userInfo', userInfo);
const requestLoginApi = (code)=>{
//发起网络请求
wx.request({
url: 'http://localhost:3000/user/wexin-login2',
method: 'POST',
header: {
'content-type': 'application/json'
},
data: {
code: code,
userInfo,
encryptedData,
iv
},
success(res) {
console.log('请求成功', res.data)
let token = res.data.data.authorizationToken
wx.setStorageSync('token', token)
onUserLogin(token)
console.log('authorization', token)
},
fail(err) {
console.log('请求异常', err)
}
})
}
const onUserLogin = (token)=>{
getApp().globalData.token = token
wx.showToast({
title: '登陆成功了',
})
}
wx.checkSession({
success () {
//session_key 未过期,并且在本生命周期一直有效
console.log('在登陆中');
let token = wx.getStorageSync('token')
if (token) onUserLogin(token)
},
fail () {
// session_key 已经失效,需要重新执行登录流程
wx.login({
success(res0) {
if (res0.code) {
requestLoginApi(res0.code)
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
}
})
},
const WeixinAuth = require("../lib/koa2-weixin-auth")
const WXBizDataCrypt = require('../lib/WXBizDataCrypt')
// 小程序的机要信息
const miniProgramAppId = '自己的appId'
const miniProgramAppSecret = '自己的appSecret'
router.post("/wexin-login2", async (ctx) => {
console.log('request.body', ctx.request.body);
let { code,
userInfo,
encryptedData,
iv,
sessionKeyIsValid } = ctx.request.body
console.log("sessionKeyIsValid", sessionKeyIsValid);
let sessionKey
// 如果客户端有token,则传来,解析
if (sessionKeyIsValid) {
let token = ctx.request.header.authorization;
token = token.split(' ')[1]
// token有可能是空的
if (token) {
let payload = await util.promisify(jsonwebtoken.verify)(token, config.jwtSecret).catch(err => {
console.log('err', err);
})
console.log('payload', payload);
if (payload) sessionKey = payload.sessionKey
}
}
// 除了尝试从token中获取sessionKey,还可以从数据库中或服务器redis缓存中获取
// 如果在db或redis中存储,可以与cookie结合起来使用,
// 目前没有这样做,sessionKey仍然存在丢失的时候,又缺少一个wx.clearSession方法
//
console.log("ctx.session.sessionKeyRecordId", ctx.session.sessionKeyRecordId);
if (sessionKeyIsValid && !sessionKey && ctx.session.sessionKeyRecordId) {
let sessionKeyRecordId = ctx.session.sessionKeyRecordId
console.log("sessionKeyRecordId", sessionKeyRecordId);
// 如果还不有找到历史上有效的sessionKey,从db中取一下
let sesskonKeyRecordOld = await SessionKey.findOne({
where: {
id: ctx.session.sessionKeyRecordId
}
})
if (sesskonKeyRecordOld) sessionKey = sesskonKeyRecordOld.sessionKey
console.log("从db中查找sessionKey3", sessionKey);
}
// 如果从token中没有取到,则从服务器上取一次
if (!sessionKey) {
const token = await weixinAuth.getAccessToken(code)
// 目前微信的 session_key, 有效期3天
sessionKey = token.data.session_key;
console.log('sessionKey2', sessionKey);
}
let decryptedUserInfo
var pc = new WXBizDataCrypt(config.miniProgram.appId, sessionKey)
// 有可能因为sessionKey不与code匹配,而出错
// 通过错误,通知前端再重新拉取code
decryptedUserInfo = pc.decryptData(encryptedData, iv)
console.log('解密后 decryptedUserInfo.openId: ', decryptedUserInfo.openId)
let user = await User.findOne({ where: { openId: decryptedUserInfo.openId } })
if (!user) {//如果用户没有查到,则创建
let createRes = await User.create(decryptedUserInfo)
console.log("createRes", createRes);
if (createRes) user = createRes.dataValues
}
let sessionKeyRecord = await SessionKey.findOne({ where: { uid: user.id } })
if (sessionKeyRecord) {
await sessionKeyRecord.update({
sessionKey: sessionKey
})
} else {
let sessionKeyRecordCreateRes = await SessionKey.create({
uid: user.id,
sessionKey: sessionKey
})
sessionKeyRecord = sessionKeyRecordCreateRes.dataValues
console.log("created record", sessionKeyRecord);
}
// ctx.cookies.set("sessionKeyRecordId", sessionKeyRecord.id)
ctx.session.sessionKeyRecordId = sessionKeyRecord.id
console.log("sessionKeyRecordId", sessionKeyRecord.id);
// 添加上openId与sessionKey
let authorizationToken = jsonwebtoken.sign({
uid: user.id,
nickName: decryptedUserInfo.nickName,
avatarUrl: decryptedUserInfo.avatarUrl,
openId: decryptedUserInfo.openId,
sessionKey: sessionKey
},
config.jwtSecret,
{ expiresIn: '3d' }//修改为3天,这是sessionKey的有效时间
)
Object.assign(decryptedUserInfo, { authorizationToken })
ctx.status = 200
ctx.body = {
code: 200,
msg: 'ok',
data: decryptedUserInfo
}
})
二、相关第三方包源码
koa2-weixin-auth.js
const querystring = require("querystring");
const request = require("request");
const AccessToken = function(data){
if(!(this instanceof AccessToken)){
return new AccessToken(data);
}
this.data = data;
}
/*!
* 检查AccessToken是否有效,检查规则为当前时间和过期时间进行对比
*
* Examples:
* ```
* token.isValid();
* ```
*/
AccessToken.prototype.isValid = function() {
return !!this.data.session_key && (new Date().getTime()) < (this.data.create_at + this.data.expires_in * 1000);
}
/**
* 根据appid和appsecret创建OAuth接口的构造函数
* 如需跨进程跨机器进行操作,access token需要进行全局维护
* 使用使用token的优先级是:
*
* 1. 使用当前缓存的token对象
* 2. 调用开发传入的获取token的异步方法,获得token之后使用(并缓存它)。
* Examples:
* ```
* var OAuth = require('oauth');
* var api = new OAuth('appid', 'secret');
* ```
* @param {String} appid 在公众平台上申请得到的appid
* @param {String} appsecret 在公众平台上申请得到的app secret
*/
const Auth = function (appid, appsecret) {
this.appid = appid;
this.appsecret = appsecret;
this.store = {};
this.getToken = function (openid) {
return this.store[openid];
};
this.saveToken = function (openid, token) {
this.store[openid] = token;
};
}
/**
* 获取授权页面的URL地址
* @param {String} redirect 授权后要跳转的地址
* @param {String} state 开发者可提供的数据
* @param {String} scope 作用范围,值为snsapi_userinfo和snsapi_base,前者用于弹出,后者用于跳转
*/
Auth.prototype.getAuthorizeURL = function(redirect_uri, scope, state) {
return new Promise((resolve, reject) => {
const url = "https://open.weixin.qq.com/connect/oauth2/authorize";
let info = {
appid: this.appid,
redirect_uri: redirect_uri,
scope: scope || 'snsapi_base',
state: state || '',
response_type: 'code'
}
resolve(url + '?' + querystring.stringify(info) + '#wechat_redirect')
})
}
/*!
* 处理token,更新过期时间
*/
Auth.prototype.processToken = function(data){
data.create_at = new Date().getTime();
// 存储token
this.saveToken(data.openid, data);
return AccessToken(data);
}
/**
* 根据授权获取到的code,换取access token和openid
* 获取openid之后,可以调用`wechat.API`来获取更多信息
* Examples:
* ```
* api.getAccessToken(code);
* ```
* Exception:
*
* - `err`, 获取access token出现异常时的异常对象
*
* 返回值:
* ```
* {
* data: {
* "access_token": "ACCESS_TOKEN",
* "expires_in": 7200,
* "refresh_token": "REFRESH_TOKEN",
* "openid": "OPENID",
* "scope": "SCOPE"
* }
* }
* ```
* @param {String} code 授权获取到的code
*/
Auth.prototype.getAccessToken = function(code){
return new Promise((resolve, reject) => {
const url = "https://api.weixin.qq.com/sns/jscode2session";
// const url = "https://api.weixin.qq.com/sns/oauth2/access_token";
const info = {
appid: this.appid,
secret: this.appsecret,
js_code: code,
grant_type: 'authorization_code'
}
request.post(url,{form:info},(err, res, body) => {
if(err){
reject(err)
}else{
const data = JSON.parse(body);
resolve(this.processToken(data))
}
})
})
}
/**
* 根据refresh token,刷新access token,调用getAccessToken后才有效
* Examples:
* ```
* api.refreshAccessToken(refreshToken);
* ```
* Exception:
*
* - `err`, 刷新access token出现异常时的异常对象
*
* Return:
* ```
* {
* data: {
* "access_token": "ACCESS_TOKEN",
* "expires_in": 7200,
* "refresh_token": "REFRESH_TOKEN",
* "openid": "OPENID",
* "scope": "SCOPE"
* }
* }
* ```
* @param {String} refreshToken refreshToken
*/
Auth.prototype.refreshAccessToken = function(refreshToken){
return new Promise((resolve, reject) => {
const url = 'https://api.weixin.qq.com/sns/oauth2/refresh_token';
var info = {
appid: this.appid,
grant_type: 'refresh_token',
refresh_token: refreshToken
};
request.post(url,{form:info},(err, res, body) => {
if(err){
reject(err)
}else{
const data = JSON.parse(body);
resolve(this.processToken(data))
}
})
})
}
/**
* 根据openid,获取用户信息。
* 当access token无效时,自动通过refresh token获取新的access token。然后再获取用户信息
* Examples:
* ```
* api.getUser(options);
* ```
*
* Options:
* ```
* openId
* // 或
* {
* "openId": "the open Id", // 必须
* "lang": "the lang code" // zh_CN 简体,zh_TW 繁体,en 英语
* }
* ```
* Callback:
*
* - `err`, 获取用户信息出现异常时的异常对象
*
* Result:
* ```
* {
* "openid": "OPENID",
* "nickname": "NICKNAME",
* "sex": "1",
* "province": "PROVINCE"
* "city": "CITY",
* "country": "COUNTRY",
* "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
* "privilege": [
* "PRIVILEGE1"
* "PRIVILEGE2"
* ]
* }
* ```
* @param {Object|String} options 传入openid或者参见Options
*/
Auth.prototype.getUser = async function(openid){
const data = this.getToken(openid);
console.log("getUser",data);
if(!data){
var error = new Error('No token for ' + options.openid + ', please authorize first.');
error.name = 'NoOAuthTokenError';
throw error;
}
const token = AccessToken(data);
var accessToken;
if(token.isValid()){
accessToken = token.data.session_key;
}else{
var newToken = await this.refreshAccessToken(token.data.refresh_token);
accessToken = newToken.data.session_key
}
console.log('accessToken',accessToken)
return await this._getUser(openid,accessToken);
}
Auth.prototype._getUser = function(openid, accessToken,lang){
return new Promise((resolve, reject) => {
const url = "https://api.weixin.qq.com/sns/userinfo";
const info = {
access_token:accessToken,
openid:openid,
lang:lang||'zh_CN'
}
request.post(url,{form:info},(err, res, body) => {
if(err){
reject(err)
}else{
resolve(JSON.parse(body));
}
})
})
}
/**
* 根据code,获取用户信息。
* Examples:
* ```
* var user = yield api.getUserByCode(code);
* ```
* Exception:
*
* - `err`, 获取用户信息出现异常时的异常对象
*
* Result:
* ```
* {
* "openid": "OPENID",
* "nickname": "NICKNAME",
* "sex": "1",
* "province": "PROVINCE"
* "city": "CITY",
* "country": "COUNTRY",
* "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
* "privilege": [
* "PRIVILEGE1"
* "PRIVILEGE2"
* ]
* }
* ```
* @param {String} code 授权获取到的code
*/
Auth.prototype.getUserByCode = async function(code){
const token = await this.getAccessToken(code);
return await this.getUser(token.data.openid);
}
module.exports = Auth;
WXBizDataCrypt .js
var crypto = require('crypto')
function WXBizDataCrypt(appId, sessionKey) {
this.appId = appId
this.sessionKey = sessionKey
}
WXBizDataCrypt.prototype.decryptData = function (encryptedData, iv) {
// base64 decode
var sessionKey = new Buffer.from(this.sessionKey, 'base64')
encryptedData = new Buffer.from(encryptedData, 'base64')
iv = new Buffer.from(iv, 'base64')
try {
// 解密
var decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, iv)
// 设置自动 padding 为 true,删除填充补位
decipher.setAutoPadding(true)
// 问题是cipher.update(data, 'binary')输出一个缓冲区,该缓冲区自动字符串化为十六进制编码的字符串
var decoded = decipher.update(encryptedData,'binary',"utf8")
// 这里有一个错误发生:
// error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt
// 本质是由于sessionKey与code不匹配造成的
decoded += decipher.final('utf8')
decoded = JSON.parse(decoded)
} catch (err) {
console.log('err',err);
throw new Error('Illegal Buffer')
}
if (decoded.watermark.appid !== this.appId) {
throw new Error('Illegal Buffer')
}
return decoded
}
module.exports = WXBizDataCrypt
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)