所谓的登录态其实就是客户端发送请求的时候携带的token(通常叫做令牌),当用户输入账号密码,验证成功之后,服务端生成一个token传递给客户端,客户端在后续的请求中携带这个token,服务器进行校验,校验成功则处理客户端的请求,校验失败则要求客户端重新去登陆。
在web项目中,我们通常使用session来管理这一过程。
客户端首次访问请求的时候,服务端返回一个sessionId作为cookie给客户端,往后客户端每次请求都带上这个cookie与服务端进行通信,当执行完登陆操作以后,服务端将用户数据存入到session中;随后的每次请求,服务端都从cookie中取出sessionId,利用sessionId去查询session,利用session中是否含有用户信息来判断用户是否有登陆。
关于cookie与session的关系,请先看笔者之前的一篇文章:浅谈cookie和session
一.小程序的登录态
要明白小程序跟传统的web项目的不同之处在于它不依托于浏览器,所以它没有cookie,自然无法用session来管理登录态。这给我们的编码造成了不小麻烦。但是其实我们可以通过在请求头中加入键为JESSIONID(或者SESSION),值为sessionId的cookie来模拟这种操作。同时在服务端响应给小程序的时候,若sessionId有发生变化则再回传给客户端。
还有一个要注意的是,小程序也有自己的登录态,那就是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" + 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 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。
绑定了开发者帐号的小程序,可以通过下面 4 种途径获取 UnionID。
1.调用接口 wx.getUserInfo,从解密数据中获取 UnionID。注意本接口需要用户授权,请开发者妥善处理用户拒绝授权后的情况。
2.如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号。开发者可以直接通过 wx.login + code2Session 获取到该用户 UnionID,无须用户再次授权。
3.如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用。开发者也可以直接通过 wx.login + code2Session 获取到该用户 UnionID ,无须用户再次授权。
4.小程序端调用云函数时,当满足 UnionID 获取条件时可在云函数中通过 cloud.getWXContext 获取 UnionID
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。