转载注明出处www.xdxxdxxdx.com,或者加入java学习群481845043。java
所谓的登陆态其实就是客户端发送请求的时候携带的token(一般叫作令牌),当用户输入帐号密码,验证成功以后,服务端生成一个token传递给客户端,客户端在后续的请求中携带这个token,服务器进行校验,校验成功则处理客户端的请求,校验失败则要求客户端从新去登录。web
在web项目中,咱们一般使用session来管理这一过程。数据库
客户端首次访问请求的时候,服务端返回一个sessionId做为cookie给客户端,日后客户端每次请求都带上这个cookie与服务端进行通讯,当执行完登录操做之后,服务端将用户数据存入到session中;随后的每次请求,服务端都从cookie中取出sessionId,利用sessionId去查询session,利用session中是否含有用户信息来判断用户是否有登录。json
关于cookie与session的关系,请先看笔者以前的一篇文章:浅谈cookie和session小程序
要明白小程序跟传统的web项目的不一样之处在于它不依托于浏览器,因此它没有cookie,天然没法用session来管理登陆态。这给咱们的编码形成了不小麻烦。可是其实咱们能够经过在请求头中加入键为JESSIONID(或者SESSION),值为sessionId的cookie来模拟这种操做。同时在服务端响应给小程序的时候,若sessionId有发生变化则再回传给客户端。api
还有一个要注意的是,小程序也有本身的登陆态,那就是session_key的生命周期,session_key是小程序中为了加密数据而提供的一个密钥,具备必定的生命周期。查看小程序官方文档,能够知道它是在服务端调用code2Session获取的。能够经过小程序的wx.checkSession()来校验小程序端的登陆态是否过时。浏览器
弄清楚了上述两点,咱们的要解决的问题包括。缓存
1.校验小程序的登陆态服务器
2.校验服务端的登陆态,便是否能从session中拿到用户数据。微信
3.任何一方的登陆态过时,都调用登录的相关代码,注意登录的相关代码包含小程序端和服务端。后续会说。
4.用户信息如何储存。在web项目里,咱们是将用户信息存放在session里,这样在服务端就能够直接用,而借助jsp的某些标签,在jsp页面咱们也能够直接从session中拿出用户数据。但如今是小程序,在服务端咱们依然能够从session中获取用户数据,可是在客户端,必须等待服务端的回传。这样每次请求都响应用户数据的作法显然不是很合理的,因此咱们能够将用户数据保存在微信的缓存里。
5.拦截器问题,在web项目中,咱们会在服务端给每一个controller写拦截器,拦截器通常是判断登陆态,判断成功则执行controller中的代码,失败的话,咱们通常会重定向到登录页面,或者执行完登录代码后重定向到某个特定页面(微信站中这样作的)。可是这种作法在小程序中是无效的,小程序是动静分离的,咱们不可能从服务端去重定向到小程序的特定页面,也不可能从服务端去调用小程序的wx.login()方法。因此,咱们把这种拦截校验的发起从服务端移到小程序端。让小程序主动发起这种校验,也就是第二点的检查服务端登陆态。
通过上面的分析,咱们整理出小程序登陆态的方案。
1.在须要用户登陆态的页面,首先从缓存中获取用户数据userInfo,若无数据,则跳4
2.调用wx.checkSession()检查小程序端的登陆态是否过时,若没过时,跳3,若过时,跳4
3.调用服务端的代码检查session是否过时(即检查服务端的登陆态),若没过时则拿到用户数据继续执行后续的操做。若过时,则跳4.
4.登陆操做,登陆操做分为以下几个步骤。
--a.小程序端调用wx.login()接口获得code。(code只能使用一次)
--b.服务端利用这个code访问code2Session接口获得session_key和open_id,并将session_key和open_id存入到session中。
--c.服务端执行登陆操做,主要是经过open_id去数据库中寻找用户数据,若无则新增用户到数据库,如有则取出用户数据。
--d.将用户数据userInfo,session_key,open_id等数据都存放到session中,方便服务端下次拿。
--e.将用户数据userInfo,连同session的sessionId一块儿响应给小程序端。
--f.小程序端获得用户数据和userInfo后更新缓存中的userInfo(包括JESSIONID的值sessionId)
上述过程能够用微信官方的这张图来表示。
这边的自定义登陆态就是sessionId,自定义登陆态与session_key,openid关联就是将session_key,openid存入到session中。
下面咱们来看具体的代码吧。
1.由于不少页面须要取到用户的数据才能继续操做,因此咱们在app.js里面写一个getUseInfo方法,供各子页面调用,方法以下。
//获取用户信息,传递的是一个回调函数,获取到用户信息后执行回调函数,传入的参数是userInfo
getUserInfo: function (cb) {
const
_this =
this
;
wx.checkSession({
success: function () {
let userInfo = wx.getStorageSync(
'userInfo'
);
//先从内存中获取userInfo
if
(userInfo.result ==
1
) {
_this.refreshSession(cb);
}
else
{
_this.userLogin(cb);
}
},
fail: function () {
_this.userLogin(cb);
}
})
},
|
上述方法的参数是一个回调函数,不一样的页面在获取了userInfo之后传入不一样的回调函数,回调函数的参数就是要获取的userInfo。
首先,调用wx.checkSession()方法断定小程序端登陆态是否失效,失效的话则去执行userLogin(cb)操做,未失效则从缓存中去拿userInfo数据。在userInfo中,咱们主要存放的是userName,userFace等用户数据和SESSION,还有一个标志位result,用于判断userInfo缓存数据是否失效。
而后,若是咱们能从缓存中拿到用户数据,就要 检验服务端的登陆态是否经过。访问refreshSession(cb)方法。代码以下
//检查服务端session是否过时
refreshSession:
function
(cb) {
const _this =
this
;
let userInfo = wx.getStorageSync(
'userInfo'
);
wx.request({
url: _this.domain + _this.api.xcxCheckSessionReq,
method:
'GET'
,
header: {
'Cookie'
:
'JSESSIONID='
+ userInfo.SESSION +
';SESSION='
+ userInfo.SESSION,
},
success:
function
(res) {
if
(res.data == 1) {
_this.globalData.userInfo = userInfo;
typeof
cb ==
"function"
&& cb(_this.globalData.userInfo);
}
else
{
wx.removeStorageSync(
'userInfo'
);
_this.userLogin(cb);
}
},
fail:
function
() {
wx.removeStorageSync(
'userInfo'
);
_this.userLogin(cb);
}
})
},
|
此处,调用服务端的接口来验证服务端的session是否已通过期,服务端的代码以下:
public String xcxCheckSession() {
Integer result;
HttpServletRequest req = ServletActionContext.getRequest();
HttpSession s = req.getSession();
if
(s.getAttribute(
"c_userId"
)!=
null
){
result=1;
}
else
{
result=0;
}
OutPutMsg.outPutMsg(result.toString());
return
null
;
}
|
其中OutPutMsg方法就是将结果响应给客户端。
上述代码根据小程序端传过来的JSESSIONID或者SESSION的值,利用servlet的特性,根据这个值去获取session,再判断session中是否有用户信息。从而完成服务端的登陆态校验。其实原理跟咱们在服务端使用拦截器校验session是否过时是同样的。
若服务端登陆态校验失败,则须要清空缓存中的userInfo信息,而后去执行userLogin(cb)方法,进行登陆。
2.登陆操做涉及到小程序端和服务端,小程序端的代码以下:
userLogin:
function
(cb) {
const _this =
this
;
wx.login({
success:
function
(res) {
//获取code而后去访问服务端登陆接口,code主要是为了换openId和session_key。
if
(res.code) {
wx.request({
url: _this.domain + _this.api.loginCheckReq,
method:
'POST'
,
header: {
'Content-Type'
: _this.globalData.postHeader
},
data: {
jsCode: res.code,
},
success:
function
(res) {
//登陆成功
if
(res.data.result == 1) {
wx.getUserInfo({
withCredentials:
true
,
success:
function
(result) {
res.data.wechatUserInfo = result.userInfo;
_this.globalData.userInfo = res.data;
_this.globalData.userInfo.face =
'/uploadFiles/'
+ res.data.userFace;
typeof
cb ==
"function"
&& cb(_this.globalData.userInfo)
wx.setStorageSync(
'userInfo'
, _this.globalData.userInfo);
//将用户数据存入内存
},
fail:
function
() {
_this.globalData.userInfo = res.data;
_this.globalData.userInfo.face = res.data.prefix +
'/uploadFiles/'
+ res.data.userFace;
typeof
cb ==
"function"
&& cb(_this.globalData.userInfo)
wx.setStorageSync(
'userInfo'
, _this.globalData.userInfo);
}
})
}
}
})
}
}
})
},
|
首先小程序端访问wx.login()接口获取code,而后调用服务端的登陆代码。服务端的登陆伪代码以下:
public
String xcxLogin(){
Integer result;
Map<String,Object>map=
new
HashMap<String, Object>();
try
{
HttpServletRequest req = ServletActionContext.getRequest();
String jsCode = req.getParameter(
"jsCode"
);
String url =
"https://api.weixin.qq.com/sns/jscode2session?appid="
+ ConfigUtil.XCX_APP_ID +
"&secret="
+ ConfigUtil.XCX_APP_SECRET +
"&js_code="
+ jsCode
+
"&grant_type=authorization_code"
;
String urlDetail = URLConnectionUtil.getUrlDetail(url);
//访问小程序接口,获取openId,session_key
JSONObject jsonObject = JSONObject.fromObject(urlDetail);
String openId=jsonObject.getString(
"openid"
);
String session_key=jsonObject.getString(
"session_key"
);
TUser user=getUserByOpenId(openId);
if
(user==
null
){
//新增用户,插入到数据库
TUser userTmp=
new
TUser();
user.setOpenId(openId);
addUser(userTmp);
user=userTmp;
}
session.put(
"user"
, user);
//将user信息放入session
session.put(
"session_key"
, session_key);
//将session_key放入session
map.put(
"user"
, user);
//将user信息响应给小程序端
map.put(
"SESSION"
, req.getSession().getId());
//将sessionId响应给小程序端
result=
1
;
//登陆操做成功的标志位
}
catch
(Exception e) {
e.printStackTrace();
}
map.put(
"result"
, result);
JSONObject resInfo=JsonUtil.mapToJsonObject(map);
OutPutMsg.outPutMsg(resInfo.toString());
//将数据响应给小程序端
return
null
;
}
|
先根据code去拿到openId和session_key,而后从数据库去查询是否有这个openId的客户,没有的话直接执行新增操做,而后将user信息(包含openId)和session_key信息存入session,方便服务端下次直接获取。再把user信息和sessionId回传给小程序端。
小程序端拿到这些信息,就能够把他们缓存起来,以备下次使用啦。
3.最后,凡事须要用户登陆才能进入的页面,咱们都让他调用getUserInfo(cb),并传入cb回调方法,好比。
onShow:
function
() {
const _this =
this
;
app.getUserInfo(
function
(userInfo) {
_this.setData({
userInfo: userInfo,
})
});
},
|
关于上述代码的userLogin()部分,目前主流的有两种。
1.使用wx.login()静默受权,获取用户的openId(),不要求用户绑定手机号,只在涉及到须要用户手机号的时候才让用户来绑定手机号。只须要在userInfo中预留一个标记用户是否有绑定手机号的字段便可。本文介绍的是采用这种登陆方式。
2.必需要用户登陆输入手机号及验证码才算登陆成功,则将userLogin处的逻辑改成跳转至登陆页面。而后服务端的判断逻辑则改成经过手机号和验证码来确认用户是否登陆成功。其余部分的逻辑不变,这也是目前比较主流的作法
3:能够简单的理解wx.login()接口是静默受权,它能获得用户的openId;而wx.getUserInfo()须要用户受权,能够获取到用户的头像,昵称等信息。还能够经过wx.getUserInfo()获取到unionId等私密信息,可是必须得在已经调用过wx.login()且登陆态还没有过时的前提下。
若是开发者拥有多个移动应用、网站应用、和公众账号(包括小程序),可经过 UnionID 来区分用户的惟一性,由于只要是同一个微信开放平台账号下的移动应用、网站应用和公众账号(包括小程序),用户的 UnionID 是惟一的。换句话说,同一用户,对同一个微信开放平台下的不一样应用,unionid是相同的。
绑定了开发者账号的小程序,能够经过下面 4 种途径获取 UnionID。
1.调用接口 wx.getUserInfo,从解密数据中获取 UnionID。注意本接口须要用户受权,请开发者妥善处理用户拒绝受权后的状况。
2.若是开发者账号下存在同主体的公众号,而且该用户已经关注了该公众号。开发者能够直接经过 wx.login + code2Session 获取到该用户 UnionID,无须用户再次受权。
3.若是开发者账号下存在同主体的公众号或移动应用,而且该用户已经受权登陆过该公众号或移动应用。开发者也能够直接经过 wx.login + code2Session 获取到该用户 UnionID ,无须用户再次受权。
4.小程序端调用云函数时,当知足 UnionID 获取条件时可在云函数中经过 cloud.getWXContext 获取 UnionID