08.09 從小程序的安全說起

背景

第一個問題 小程序中可以使用session麼?

答案可能出乎大部分人意外

不可以!因為微信本身不是web方案,因此表現出來不會攜帶cookie 我們知道cookie和session的關係 Cookie,Session和Token的故事

分析

那麼我們如何來判斷用戶是哪個用戶呢???

從小程序的安全說起

小程序的官方實踐

現狀

首先我們來看看我們是怎麼做的?

getWxUserInfo (options) {

const url = `/weapp/code2session`;

wx.login({

success: res => {

if (res.code) {

//發起網絡請求,用code換sessionkey

requestUtil.wxRequestGet({

url,

data: {

code: res.code,

appid: config.appId,

},

success: result => {

this.saveUserInfo(options, result.data.session_key);

},

fail: result => {

wx.showToast({title: '小程序登錄失敗,請稍後再試。', icon: 'none'});

},

});

} else {

wx.showToast({title: '小程序登錄失敗,請稍後再試。', icon: 'none'});

}

},

fail(res) {

wx.showToast({title: '小程序登錄失敗,請稍後再試。', icon: 'none'});

}

});

},

saveUserInfo: (options, sessionKey) => {

options = options || {};

wx.getUserInfo({

lang: "zh_CN",

withCredentials: true,

success: res => {

requestUtil.wxRequestPostForm({

url: '/weapp/login',

data: {

appid: config.appId,

userInfo: JSON.stringify(res.userInfo),

encryptedData: res.encryptedData,

iv: res.iv,

sessionKey,

},

success: result => {

app.globalData.userInfo = result.data.user;

app.globalData.sessionKey = result.data.sessionKey;

if (options.success) {

options.success(result);

}

},

fail: result => {

wx.showToast({title: '獲取用戶信息,請稍後再試。', icon: 'none'});

},

});

},

fail: res => {

if (res.errMsg === 'getUserInfo:fail auth deny') {

wx.showModal({

title: '提示',

content: '小程序需要獲取您的公開信息,請在設置中打開。',

confirmText:'去設置',

success: res => {

if (res.confirm) {

wx.openSetting({

success: (res) => {

},

});

} else {

wx.showToast({title:'獲得授權失敗, 無法獲取用戶信息', icon: 'none'});

}

}

})

}

},

})

},

};

  1. wx.login接口返回code字段 該字段用來換取sessionkey
  2. 後端將sessionkey傳遞到了前端
  3. 調用wx.getUserInfo方法獲取用戶openid【傳遞sessionKey】
  4. 後端根據sessionKey對加密字段解密並存儲

上述步驟其實很明顯了 如果我是壞人 我不會把你的sessionkey傳給你 我可以自定義一個sessionKey【用該sessionKey和我想要的數據偽造出一個假的用戶】

這樣就會產生一個帶有openid的偽造的用戶了

通俗的說sessionkey相當於鑰匙 微信客戶端回傳加密信息 我們拼接該鑰匙進行解密 我們目前的做法是把鑰匙給了客戶然後客戶每次過來攜帶者鑰匙和加密數據給我來解密【掩耳盜鈴】

問題

再回到開始的問題 我們說小程序不支持cookie 那麼自然也不支持session 那麼我們要如何記住客戶的狀態呢???

回到cookie和session的起點【究竟啥時cookie 啥是session】 Cookie,Session和Token的故事

我們明白實質上cookie就是一個header 那麼如果我們在我們的請求中將對應的header設置到請求上去 那麼是不是就變相的可以使用session了呢???

答案是肯定的 因此聰明的小夥伴封裝了一把

const getHeader = (options)=> {

if (app.globalData.sessionKey) {

options.cookie = `${config.sessionKey}=${app.globalData.sessionKey}`;

return options;

}

return options;

};

那麼問題來了 session就有有效期 這個session多長時間超時呢???【這個session是真的session麼 session過期了應該怎麼辦呢?用戶有辦法取消session授權麼?】

來看一下我們如何check用戶

if (!app.globalData.userInfo) {

user.getWxUserInfo({

success: ()=> {

this.fetchData();

},

});

} else {

user.getUserInfo({

success: res => {

wx.stopPullDownRefresh();

this.setData({

userInfo: res.data.user,

showSelHistory: !!app.globalData.userInfo,

remainCountText: `剩餘 ${res.data.user.remainCount || 0 } 次/天`,

});

app.globalData.userInfo = res.data.user;

this.setPhoneNo(this.data.userInfo.cellPhone);

},

fail: res => {

wx.stopPullDownRefresh();

wx.showToast({title: res.data.msg, icon: 'none'});

},

});

}

當globalData中存在數據則直接將該數據設置到微信的page中===》如果客戶退出微信重新登陸了怎麼辦???

在看後端如何獲取sessionKey

@RequestMapping(value="/weapp/code2session")

@ResponseBody

public CarzoneJson weAppCode2Session(HttpServletRequest request,

String appid,

String code) throws Exception {

String jsonStr = HttpUtil.get("", String.format(WE_APP_CODE_2_SESSION_URL, appid, WE_APP_SECRET, code));

JSONObject jsonObject = new JSONObject(jsonStr);

String sessionKey = "";

try {

sessionKey = jsonObject.getString("session_key");

} catch (Exception e) {

log.error("小程序登錄失敗" + jsonStr);

return JsonMessage.getError();

}

CarzoneJson result = JsonMessage.getSuccess().set("session_key", sessionKey);

return result;

}

這邊需要注意一下和外部接口調用最好可以估計一下信息回放 這邊json數據沒有任何地方記錄

請求參數

appid 是 小程序唯一標識 secret 是 小程序的 app secret js_code 是 登錄時獲取的 code grant_type 是 填寫為 authorization_code 參數

必填

說明

在不滿足 UnionID 下發條件的情況下,返回參數

openid 用戶唯一標識 session_key 會話密鑰 參數

說明

在滿足 UnionID 下發條件的情況下,返回參數

openid 用戶唯一標識 session_key 會話密鑰 unionid 用戶在開放平臺的唯一標識符 參數

說明

很明顯這邊的openId是真實的openid 也是真正需要的openid【後續授權必須先經過這一步】

但是我們通過解析字段如何做的呢?

@RequestMapping(value="/weapp/login")

@ResponseBody

public CarzoneJson weAppLogin(HttpServletRequest request, String appid,

String userInfo, String encryptedData,

String sessionKey, String iv) throws Exception{

//保存或更新用戶信息

if (StringUtils.isNotEmpty(userInfo) && StringUtils.isNotEmpty(encryptedData)

&& StringUtils.isNotEmpty(sessionKey) && StringUtils.isNotEmpty(iv)) {

JSONObject userInfoObj = new JSONObject(userInfo);

com.alibaba.fastjson.JSONObject encryptedJson = WechatUtils.getUserInfo(sessionKey, encryptedData, iv);

String openId = encryptedJson.getString("openId");

String unionId = encryptedJson.getString("unionId");

GetUserInfoResponse userInfoModel = new GetUserInfoResponse();

userInfoModel.setNickname(userInfoObj.getString("nickName"));

userInfoModel.setCity(userInfoObj.getString("city"));

userInfoModel.setProvince(userInfoObj.getString("province"));

userInfoModel.setCountry(userInfoObj.getString("country"));

userInfoModel.setHeadimgurl(userInfoObj.getString("avatarUrl"));

userInfoModel.setLanguage(userInfoObj.getString("language"));

userInfoModel.setSex(userInfoObj.getInt("gender"));

userInfoModel.setOpenid(openId);

userInfoModel.setUnionid(unionId);

// 代表需要授權的f6用戶,1代表需要授權的零公里用戶,2代表需要授權的孚美用戶,3代表不需要授權的其他用戶

userService.addOrUpdateWxUser(userInfoModel, appid, CommonConstants.GB_WX_USER_LEVEL_10, 0);

CarzoneJson result = this.login(request, appid, openId, unionId);

result.set("sessionKey", request.getSession().getId());

return result;

} else {

throw new InvalidParameterException();

}

}

這邊OpenId如果不是上述的openid說明是偽造的噢~【由於sessionkey的流失那麼可以出現客戶偽造】===》如果我們前面記錄下了日誌我們可以根據openid查找不在名單內的用戶了~【最簡單放到redis中可以check是否存在】

實踐

按照微信的文檔所描述 一段時間類用code換取的sessionKey實際上是不變的【可能達到30天 但是隨著用戶操作頻率來說可能發生變化===》理解成我們的session 最長存在30天但是長時間不訪問session將自動過期 也就是說獲取wx用戶的api取不到數據了】

因此這邊存在問題 如何讓微信授權過後的用戶無縫訪問我們的受保護api呢???

按照官方推薦方案使用自己平臺的session【本質是還是驗證我是誰】

每次請求過來都會帶上用戶的sessionid 這樣我們根據sessionid解析出對應的session信息【由於session信息未暴露出去 自然提供了更好的安全性】

那麼值錢提的問題 如何保證session的過期【比如用戶切換了微信號】

會話密鑰 session_key 有效性

開發者如果遇到因為 session_key 不正確而校驗簽名失敗或解密失敗,請關注下面幾個與 session_key 有關的注意事項。

  1. wx.login() 調用時,用戶的 session_key 會被更新而致使舊 session_key 失效。開發者應該在明確需要重新登錄時才調用 wx.login(),及時通過登錄憑證校驗接口更新服務器存儲的 session_key。
  2. 微信不會把 session_key 的有效期告知開發者。我們會根據用戶使用小程序的行為對 session_key 進行續期。用戶越頻繁使用小程序,session_key 有效期越長。
  3. 開發者在 session_key 失效時,可以通過重新執行登錄流程獲取有效的 session_key。使用接口 wx.checkSession() 可以校驗 session_key 是否有效,從而避免小程序反覆執行登錄流程。
  4. 當開發者在實現自定義登錄態時,可以考慮以 session_key 有效期作為自身登錄態有效期,也可以實現自定義的時效性策略。

wx.checkSession(OBJECT)

校驗用戶當前 session_key 是否有效。

OBJECT 參數說明:

success Function 否 接口調用成功的回調函數,session_key 未過期 fail Function 否 接口調用失敗的回調函數,session_key 已過期 complete Function 否 接口調用結束的回調函數(調用成功、失敗都會執行) 參數名

類型

必填

說明

示例代碼:

wx.checkSession({

success: function( ){

//session_key 未過期,並且在本生命週期一直有效

},

fail: function( ){

// session_key 已經失效,需要重新執行登錄流程

wx.login() //重新登錄

....

}

})

在我們的系統中由於globalData一直存在用戶將不會每次觸發login 因此建議在小程序初始打開時調用checkSession方法

再思考

為何我們需要用戶手機號???

  1. 更好的安全性 用戶必須受到驗證碼
  2. 更好的全平臺統一打通【手機號可以唯一標緻客戶】
  3. 更好的融資【帶客戶手機號的更好賣吧 ^_^】
  4. 其他思考

那麼針對來說

  1. 手機短信平臺很常見 大概大家不知道人心險惡===》由於驗證碼被人刷的概率太多了
從小程序的安全說起


  1. 這個是必要的
  2. 這個也是必要的
  3. 但是由於引入手機號輸入勢必造成客戶手動輸入手機號~同時要防止惡意註冊等等
  4. 微信其實也提供了安全的手機號獲取接口 【很不巧 我們也有權限 ^_^】

getPhoneNumber(OBJECT)

  1. 說明
  2. 獲取微信用戶綁定的手機號,需先調用 login 接口。
  3. 因為需要用戶主動觸發才能發起獲取手機號接口,所以該功能不由 API 來調用,需用 <button> 組件的點擊來觸發。/<button>
  4. 注意:目前該接口針對非個人開發者,且完成了認證的小程序開放。需謹慎使用,若用戶舉報較多或被發現在不必要場景下使用,微信有權永久回收該小程序的該接口權限。
  5. 使用方法
  6. 需要將 <button> 組件 open-type 的值設置為 getPhoneNumber,當用戶點擊並同意之後,可以通過 bindgetphonenumber 事件回調獲取到微信服務器返回的加密數據, 然後在第三方服務端結合 session_key 以及 app_id 進行解密獲取手機號。/<button>
  7. 注意
  8. 在回調中調用 wx.login 登錄,可能會刷新登錄態。此時服務器使用 code 換取的 sessionKey 不是加密時使用的 sessionKey,導致解密失敗。建議開發者提前進行 login;或者在回調中先使用 checkSession 進行登錄態檢查,避免 login 刷新登錄態。
  9. 例子
  10. <button> /<button>
  11. Page({
  12. getPhoneNumber: function(e) {
  13. console.log(e.detail.errMsg)
  14. console.log(e.detail.iv)
  15. console.log(e.detail.encryptedData)
  16. }
  17. })
  18. 返回參數說明
  19. encryptedData String 包括敏感數據在內的完整用戶信息的加密數據,詳細見加密數據解密算法 iv String 加密算法的初始向量,詳細見加密數據解密算法 參數
  20. 類型
  21. 說明
  22. encryptedData 解密後為以下 json 結構,詳見加密數據解密算法
  23. {
  24. "phoneNumber": "13580006666",
  25. "purePhoneNumber": "13580006666",
  26. "countryCode": "86",
  27. "watermark":
  28. {
  29. "appid":"APPID",
  30. "timestamp":TIMESTAMP
  31. }
  32. }
  33. phoneNumber String 用戶綁定的手機號(國外手機號會有區號) purePhoneNumber String 沒有區號的手機號 countryCode String 區號 參數
  34. 類型
  35. 說明
  36. 比如某牛

本文由Mr_Qi 原創首發,© 著作權歸作者所有,如有侵權,請聯繫刪除。


分享到:


相關文章: