目录
1 问题描述
小程序成功登陆后,安卓用户预约操作时,偶尔会出现登陆异常情况。
2 登陆实现方案
后端Cookie校验用户登陆状态
3 排查过程
1)后台日志排查,用户Cookie无效
2)微信小程序日志排查,安卓用户有问题
3)现象复现,安卓手机提示异常
4)微信小程序埋点输出日志,Cookie存取正常
5)正常与异常Cookie对比,Cookie顺序不对
6)异常Cookie排查,Cookie拼接不正确
4 为什么苹果手机、部分安卓手机没有问题?
5 问题原因
腾讯小程序Bug,Set-Cookie逗号拼接方式有问题
6 解决方法
Cookie正则分割,分号重新拼接
7 标准答案
8 硬广告
微信预约小程序,14天免费使用
1 问题描述
最近有用户反馈,小程序登陆有问题
小程序成功登陆后,安卓用户预约操作时,偶尔会出现登陆异常情况。
之前解决过shiro登陆失败的问题,《Shiro框架Given final block not properly padded问题解决》,但是没有彻底解决登陆失败的问题。
登陆失败现象极其诡异,问题难以复现。
1 用户10秒前刚登陆,用户后续操作马上提示“登陆异常”
2 部分安卓用户在登陆后,在后续操作提示“登陆异常”,这种现象是偶然发生的,安卓用户出现登陆异常概率大概是10分之1。
2 苹果用户从未没有出现”登陆异常“现象。
2 登陆实现方案
在介绍问题前,本文先简单描述下本文应用登陆实现方案。
本文的小程序应用是单独使用Cookie来维护登陆状态,登陆并未使用小程序的sessionKey来维护后台登陆状态。
后端Cookie校验用户登陆状态
为什么不使用小程序SessionKey维护登陆状态?
1 老应用有单独Cookie登陆方式。
2 后台支持多平台登陆(比如小程序、h5网页等),不完全依赖小程序SessionKey完成登陆 。
Shiro控制用户权限
1 获取微信小程序code值
2 code值以及appId换取用户openId
3 openId快捷登陆
4 小程序保存登陆Cookie
5 小程序携带Cookie请求后端应用
6 后端根据前端的Cookie校验用户的登陆情况
可惜小程序不支持Cookie,(浏览器一般会保存用户Cookie,方便后续浏览网页使用)
如何解决小程序支持Cookie登陆?
以下是网上常见的Cookie登陆解决方式。
// 登录
wx.login({
success: function (res) {
log.info(res)
//获取登录的临时凭证
var code = res.code;
//调用后端,获取微信的session_key,secret
wx.request({
url: domain + '/user/wxLogin',
header: {
"Content-Type": "application/x-www-form-urlencoded"
},
method: "POST",
data: util.json2Form({
code: code,
appId: that.globalData.appId
}),
success: function (result) {
// cookie存储起来
var cookie = result.header['Set-Cookie']
wx.setStorageSync('cookie', cookie)
},
fail: function (res) {
}
})
}
})
})
wx.request从res.header['Set-Cookie']中获取cookie信息,并使用wx.setStoargeSync将cookie信息同步写入小程序私有存储空间中
wx.request({
url: domain + '/api/xxx/xxx',
header: {
"Content-Type": "application/x-www-form-urlencoded",
'cookie': wx.getStorageSync("cookie")
},
method: 'POST',
data: util.json2Form({ xx1: xx1, xx2: xx2 }),
success: function (result) {
},
fail: function (res) {
log.info("服务器异常:" + res)
wx.showToast({
title: '服务器异常',
icon: 'none',
duration: 2000
})
}
})
wx.getStorageSync同步将cookie从小程序中取出,小程序携带该cookie请求后台服务,完成用户登陆状态校验。
3 排查过程
问题不可怕,可怕的是不能稳定地复现case。本人平时使用iphone手机,重复用户的操作,基本复现不出来“登陆异常”的现象,只能在服务后台以及小程序前端疯狂埋点打日志。
1)后台日志排查,用户Cookie无效
从后台日志可以看出,用户成功登陆,但是用户没有在后续的请求使用用有效cookie去请求后台服务,后台直接拒绝服务。
2)微信小程序日志排查,安卓用户有问题
通过后台和小程序日志发现,出现“登陆异常”的用户基本是安卓用户,苹果用户基本没有这个问题。
3)现象复现,安卓手机提示异常
目标锁定安卓手机,直接找了台安卓手机,重复登陆、预约操作,大概反复操作10次,终于复现了“登陆异常”的现象,喜大普奔啊。
4)微信小程序埋点输出日志,Cookie存取正常
起初怀疑wx.setStoargeSync有bug,未将cookie成功写入小程序。
验证方式:将res.header['Set-Cookie']、wx.getStorageSync日志输出。
如果res.header['Set-Cookie']输出日志为空,则定位是后端服务问题,未将cookie携带返回。
如果res.header['Set-Cookie']输出日志不为空,wx.getStorageSync('cookie')输出日志为空,则定位setStorageSync写入有bug
但是最终现象res.header['Set-Cookie']、wx.getStorageSync('cookie')输出日志均不为空,排查思路中断了。
5)正常与异常Cookie对比,Cookie顺序不对
对比正常请求的cookie与异常请求的cookie
发现cookie的顺序不一样
后台shiro做权限控制,返回3个Set-Cookie值,苹果用户的请求基本是以JESSIONID开头,而有问题的安卓用户请求是以remeberMe=Delete开关
6)异常Cookie排查,Cookie拼接不正确
将登陆异常请求的cookie按照正常请求cookie的顺序调整了下, 用户请求后端可以成功登陆。
使用错误顺序cookie请求后端,后端解析cookie并未获取到JESSIONID。
后端是如何解析Cookie,根据分号";"分割Cookie字符串,由于错误顺序Cookie是使用逗号做分割符,后端将Expires与JESSIONID视为一个整体,所以无法解析出JESSIONID
4 为什么苹果手机、部分安卓手机没有问题?
没有问题的Cookie,JESSIONID可以被后台正确分割出来。苹果手机获取Set-Cookie顺序是严格一致的,安卓手机Set-Cookie顺序是随机的。
5 问题原因
腾讯小程序Bug,Set-Cookie逗号拼接方式有问题
微信开放社区也有人反馈过这个bug,但是以微信社区人员解决问题的稀烂态度,至今未解决该bug。
解决bug只能靠自己去兼容这个bug
6 解决方法
当返回结果的header有多个Set-Cookier时,微信小程序获取res.header['Set-Cookie'],已经是将Set-Cookie用逗号拼接好的字符串。
如果使用简单的逗号分割,Expires时间也含有逗号,Cookie结果是错误的。
Cookie正则分割,分号重新拼接
见示例
简单的逗号无法分割,可以使用正则表达式去分割Cookie字符串,
需要被分割的逗号,后继字符串是含有=
7 标准答案
微信小程序使用Cookie登陆标准答案
// 登录
wx.login({
success: function (res) {
log.info(res)
//获取登录的临时凭证
var code = res.code;
//调用后端,获取微信的session_key,secret
wx.request({
url: domain + '/user/wxLogin',
header: {
"Content-Type": "application/x-www-form-urlencoded"
},
method: "POST",
data: util.json2Form({
code: code,
appId: that.globalData.appId
}),
success: function (result) {
// Set-Cookie字符串获取
var cookie = result.header['Set-Cookie']
// 字符串分割成数组
var cookieArray = cookie.split(/,(?=[^,]*=)/)
// 分号拼接数组
var newCookie = cookieArray.join(';')
// 存储拼接后的cookie
try {
wx.setStorageSync('cookie', newCookie)
} catch (error) {
log.error('setStorageSync cookie fail')
}
},
fail: function (res) {
}
})
}
})