小程式登入態管理
轉載註明出處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