轉載注明出處www.xdxxdxxdx.com,或者加入java學習群481845043。
所謂的登錄態其實就是客戶端發送請求的時候攜帶的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?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 是唯一的。換句話說,同一用戶,對同一個微信開放平台下的不同應用,unionid是相同的。
綁定了開發者帳號的小程序,可以通過下面 4 種途徑獲取 UnionID。
1.調用接口 wx.getUserInfo,從解密數據中獲取 UnionID。注意本接口需要用戶授權,請開發者妥善處理用戶拒絕授權后的情況。
2.如果開發者帳號下存在同主體的公眾號,並且該用戶已經關注了該公眾號。開發者可以直接通過 wx.login + code2Session 獲取到該用戶 UnionID,無須用戶再次授權。
3.如果開發者帳號下存在同主體的公眾號或移動應用,並且該用戶已經授權登錄過該公眾號或移動應用。開發者也可以直接通過 wx.login + code2Session 獲取到該用戶 UnionID ,無須用戶再次授權。
4.小程序端調用雲函數時,當滿足 UnionID 獲取條件時可在雲函數中通過 cloud.getWXContext 獲取 UnionID