【愚公系列】2022年09月 微信小程序-微信小程序实现网页一键登录功能

举报
愚公搬代码 发表于 2022/09/30 22:31:01 2022/09/30
【摘要】 前言如果微信小程序要获取微信登录的用户信息,需要拿到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

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。