在如今移动互联网的浪潮中,微信已成为人们生活中不可或缺的社交工具。对于开发者而言,借助微信强大的用户基础,实现微信网页授权登录功能,能有效提升应用的用户体验和用户获取效率。
微信网页授权登录原理
微信网页授权登录基于 OAuth 2.0
协议,整个流程大致分为以下几个步骤:
引导用户授权:应用将用户引导至微信授权页面,用户在该页面确认是否授权应用获取其信息。
获取授权码:若用户同意授权,微信会回调应用事先配置好的重定向 URL,并附带一个临时的授权码(code)。
换取用户信息:应用使用获取到的授权码,通过向微信服务器发送请求,换取用户的 access_token 和 openid。
拉取用户信息:凭借 access_token 和 openid,应用可以进一步获取用户的详细信息,如头像、昵称等。
前端授权流程
利用这个流程,我们可以实现一个扫码登录的功能
准备工作
微信公众平台测试号为开发者提供了一个便捷的测试环境,无需复杂的公众号资质申请流程,通过简单注册即可获取测试号,拥有和正式公众号类似的接口权限,非常适合用于开发初期的功能调试与验证,包括网页授权登录功能的实现。
微信公众平台测试号配置
访问微信公众平台测试号 申请页面 ,使用个人微信号扫码登录,即可获取一个测试号。
在测试号管理页面,记录下
AppID
和AppSecret
,这两个参数在后续代码中会频繁使用。往下滑动网页,在体验接口权限表中找到
网页服务 -> 网页帐号 -> 网页授权获取用户基本信息
点击
修改
,填入授权回调页面域名:127.0.0.1
(注意:此处只填写ip即可,不需要加上http或https)
Nodejs项目环境搭建
新建一个 Nodejs 项目目录,打开终端,执行npm init -y命令,快速生成package.json文件,用于管理项目依赖。
安装axios用于发送 HTTP 请求,执行npm install axios;若使用express框架搭建 Web 服务(也可选择其他框架或原生 Nodejs 实现),执行npm install express。
具体实现步骤
本文主要介绍基于 nvpress
实现微信授权登录的方案。
二维码生成
要扫码登陆,首先就要生成二维码。二维码本质上就是存储数据的图片介质,其中的数据可以是 URL 也可以是文本等。通过二维码扫码工具就能扫出其中的数据。比如扫描上面设计图中的二维码,你就可以得到一段文本。所以基于此原理,我们就可以把 URL 存储在二维码中,微信扫码之后,会自动跳转到该 URL。
上面还提到,微信可以通过授权 URL 获取到用户的 openid,而第三步需要该 openid,所以我们的 URL 需要是一个授权 URL。
在 node.js 中生成授权 URL 可以借助于 wechat-oauth
这个包。详细步骤如下:
const OAuth = require('wechat-oauth');
const uuid = require('uuid');
const client = new OAuth('wechat_appid', 'wechat_appsecret');
/**
* 获取微信授权码
* @param {*} data
* @param {*} req
* @param {*} res
* @returns
*/
register_rest_route('wechat', 'get-code', {
methods: 'get',
callback(data, req, res) {
const url = client.getAuthorizeURL('http://127.0.0.1', '123123', 'snsapi_userinfo')
// 创建一个唯一的token,用于轮询
const wechat_token = uuid.v1()
return {
url,
token: wechat_token,
}
},
})
其中 redirectUrl
是网页授权回调域名(需要填写与测试号中配置的信息一致),scope 是网页授权的方式,有 snsapi_base
和 snsapi_userinfo
。state 是我们自定义的一个参数,重定向后会带上该参数,所以一般可以用该参数来表示不同的业务。更详细的信息可以参考 微信网页授权。
这样用户访问该 URL 之后就会被重定向到上面设置的 redirectUrl
,并带上 code
参数。在第三步的时候,就可以根据 code 来获取用户的 openid。
然后由于还需要确定是哪个用户在进行登陆,即将微信和电脑浏览器对应起来,所以还需要一个唯一字符串来标识。即在 URL 中加上一个唯一 token
,这样微信就能根据该 token 知道是哪一个客户端(浏览器,也就是用户)在进行登陆了。
所以我们可以生成一个 uuid
作为 token。生成 uuid 可以使用 uuid
这个包。然后我们可以将 uuid 作为 state 参数来生成授权 URL。就像下面这样:
const url = client.getAuthorizeURL('http://127.0.0.1', '985123a0-7e4f-11e7-9022-fb7190c856e4', 'snsapi_userinfo');
console.log(url);
// https://open.weixin.qq.com/connect/oauth2/authorize?appid=APP_ID&redirect_uri=http%3A%2F%2F127.0.0.1&response_type=code&scope=snsapi_userinfo&state=985123a0-7e4f-11e7-9022-fb7190c856e4#wechat_redirect
最后根据这个带有 token 的 URL 生成一个二维码。生成二维码的步骤可以放在前端也可以放在后端,方式有很多种。
同时还需要做的事情是,将生成二维码的 token 也返回给客户端,因为后面还会用到该 token。可以将其放在 cookie 里面,也可以放在隐藏表单。
扫码获取token
当用户用微信扫描登陆页面的二维码时,就会自动跳转到二维码对应的 URL 上。
用户同意授权之后,接下来微信浏览器还将继续跳转回调域名,并带上 code 参数,可能跳转之后的页面就是
http://127.0.0.1/?code=CODE&state=985123a0-7e4f-11e7-9022-fb7190c856e4。
于是我们就可以自己的后端通过 state 参数中取得 token。
获取用户信息
在之前我们生成的二维码链接中,回调域名是 127.0.0.1
,所以我们在对应的路由界面中进行判断是否有 code
这一参数
onMounted(() => {
const searchParams = new URLSearchParams(window.location.search)
const code = searchParams.get('code')
if (code) {
wxLogin(code)
}
})
同时我们需要实现的后端接口是根据 code 进行下面的步骤,最终获取到用户的信息。
/**
* 获取用户信息,并更新用户信息
* @param {*} data
* @param {*} req
* @param {*} res
* @returns
*/
register_rest_route('wechat', 'get-user-info', {
methods: 'post',
callback: async (data, req, res) => {
const { code } = data
const { data: tokenData } = await axios.get('https://api.weixin.qq.com/sns/oauth2/access_token?appid=' + AppID + '&secret=' + AppSecret + '&code=' + code + '&grant_type=authorization_code')
let access_token = tokenData.access_token
let openid = tokenData.openid
const { data: userinfo } = await axios.get('https://api.weixin.qq.com/sns/userinfo?access_token=' + access_token + '&openid=' + openid + '&lang=zh_CN')
const user = get_user_by_openid(userinfo.openid)
let nvnonce = ''
// 用户未注册则根据微信返回的信息创建一个新用户
if (!user) {
const user_id = nv_insert_user({...})
// 设置cookie
nvnonce = nv_create_nonce(user_id)
} else {
// 用户已存在则直接设置cookie
nvnonce = nv_create_nonce(user.id)
}
// 记住,那就1年,反正nonce都是24小时过期,除非一直使用才能续期。否则就是会话
res.cookie(
'nvnonce',
nvnonce,
apply_filters('nv_user_login_cookie_options', {
path: '/',
maxAge: 24 * 60 * 60 * 1000,
sameSite: 'Lax',
})
)
return userinfo
},
})
轮询
轮询可以循环发送 HTTP
请求,也可以使用 Web Socket
。
当服务端发现用户已经扫码之后,就可以将登陆状态设置为已登陆,如设置 session,然后返回给客户端。客户端发现已登陆成功,则跳转到登陆后的页面。
这里我们以轮询发送http请求为例:
/**
* 检查用户是否登录
* @param {*} data
* @param {*} req
* @param {*} res
* @returns
*/
register_rest_route('wechat', 'check-login', {
methods: 'post',
callback: async (data, req, res) => {
const { token } = data
// 根据token判断用户是否已经注册成功
const user = get_user_by_token(token)
if (user) {
// 注册成功后的操作,可以在此处创建nvnonce,同时前端调用/nv/check-nonce去检查用户信息
return user
} else {
return {
code: 0,
msg: '用户不存在',
}
}
},
})
改进
上面的设计方案存在的主要题是,二维码是一直有效的。如果考虑到二维码失效怎么处理?这个时候就可以简单改变一下思路。
前面是扫码的时候,将 token
和 openid
存储到数据库(或别的存储),客户端根据 token
轮询判断是否有数据。
考虑到二维码的实效性,则生成二维码的时候,就先将 token 存储到数据库,并设置一个token的 过期时间
。
当用户使用微信扫码的时候,获取到 token 和 openid。首先根据 token 判断一下数据库中是否有该 token 对应的数据,如果没有则不存储;如果有,则判断是否过期;如果有且 token 未过期,则更新 该 token 对应的 openid。这样就能达到二维码实效性的问题。
总结
其实文中大部分内容描述的都不是最优的解决方案。是因为最开始思考的不够,没想到那么全。当然,改进部分描述的可能也不是最好的方案。但如果没有之前想到的那些,可能更不会想到可以改进的地方,索性就这么在本文中记录了一下。