1. 前言
微信小程序開發平臺,提供一類 API,可以讓開發者獲取到微信登錄用戶的個人數據。這類 API 統稱為開放接口。
Tip:微信小程序開發平臺,會把微信登錄用戶的個人信息分為明文數據和敏感數據。
明文數據也稱為公開數據,開發者可以直接獲取到,如登錄者的昵稱、頭像……
敏感數據如電話號碼、唯一標識符……等數據,只有高級認證開發者和經過登錄者授權后才能解密獲取到。
這一類 API較多,且 API之間功能有重疊之處,相互之間的區別較微小。有的適用于低版本,有的適用于高版本。
為了避免在使用時出現選擇混亂,本文將通過具體應用案例介紹幾個常用 API的使用。
2. 開放接口
開放接口是對一類 API的統稱,開發者可以通過調用這類接口得到微信登錄用戶的授權或獲取登錄者的個人數據。
開放接口又分成幾個子類 API :
- 登錄接口: 包括 wx.pluginLogin(Object args)、wx.login(Object object)、wx.checkSession(Object object) 幾 個 API。
- 賬號信息: 包括Object wx.getAccountInfoSync()此接口用來獲取開發者的賬號信息。
- 用戶信息: 包括 wx.getUserProfile(Object object)、wx.getUserInfo(Object object)、UserInfo。使用頻率非常高的接口,常用于小程序中獲取登錄者的個人公開數據。
- 授權接口:wx.authorizeForMiniProgram(Object object)、wx.authorize(Object object)
除上述列出的子類接口,還有收貨地址、生物認證……等諸多子類 API,有興趣者可以自行了解。
2.1 登錄接口
登錄接口中有 3 個 API,對于開發者來說,使用頻率較高的是 login接口,此環節將重點介紹此接口。
非本文特別關注的接口,會簡略帶過。
wx.pluginLogin(Object args):此接口只能在插件中可以調用,調用此接口獲得插件用戶的標志憑證code,插件可使用此憑證換取用于識別用戶的唯一標識 OpenpId。
用戶不同、宿主小程序不同或插件不同的情況下,該標識均不相同,即當且僅當同一個用戶在同一個宿主小程序中使用同一個插件時,OpenpId 才會相同。
對于一般開發者,此時 接口用的不是很多,具體使用細節在此處也不做過多復述。
什么是 OpenId?
當微信用戶登錄公眾號或小程序時,微信平臺為每一個微信登錄者分配的一個唯一標識符號。
2.1.1wx.login(Object object)
功能描述:
- 開發者使用此接口可以獲取到微信登錄者的登錄憑證(code)。
- 登錄憑證具有臨時性,也就是每次調用時都會不一樣,所以code 只能使用一次。
- 開發者可以通過臨時code,再向微信接口服務器索取登錄者的唯一標識符 OpenId、微信開發平臺賬號的唯一標識 UnionID(需要當前小程序已綁定到微信開放平臺帳號)、以及會話密鑰 session_key。
那么,獲取到的openId和session_key對于開發者而言,有什么實質性的意義?
- 根據 OpenId的唯一性特點,可以在微信用戶第一次登錄時,把OpenID保存在數據庫或緩存中,在后續登錄時,只需要檢查用戶的 OpenId是否存在于數據庫或緩存中,便能實現自動登錄功能。
- session_key 也稱會話密鑰,用來解密微信登錄者的敏感數據。
- 后文將詳細介紹。
如何獲取OpenId?
現在通過一個簡單案例,實現微信小程序端與開發者服務器之間的數據交互。以此了解開發者服務器如何通過微信小程序傳遞過來的用戶臨時 code換取到登錄者的更多信息。
實現之前,先通過一個簡易演示圖了解其過程。
簡單描述整個請求過程:
- 微信用戶打開微信小程序后,開發者在微信小程序中通過調用wx.login接口獲取到臨時登錄憑證 code。
- 在微信小程序中調用 wx.request 接口向開發者服務器發送 http 請求,需要把登錄憑證 code一并發送過去。
- 開發者服務器使用發送過來的 code 以及開發者憑證信息向微信接口服務器索取微信登錄者的 openId和session_key。
簡而言之,就是 3 者(微信小程序、開發者服務器、微信接口服務器)之間的一個擊鼓傳花游戲。
開發流程:
第一步:項目結構分析
完整的系統由 2 個部分組成:
- 微信小程序端 App。
- 如果對微信小程序開發不是很了解,請先閱讀官方提供的相關文檔。
- 服務器端應用程序。
- 本文的服務器端應用程序基于 Spring Boot開發平臺。
本項目結構是標準的前后端分離模式,微信小程序是前端應用,服務器端應用程序為后臺應用。
第二步:新建微信小程序(前端應用)
打開微信開發工具,新建一個名為 guokeai 的小程序項目 ,項目會初始化一個index 頁面。在 index.js中編寫如下代碼。
//index.js
const app = getApp()
const httpRequest = require("../../utils/request.js")
Page({
data: {
isHasUserInfo: null,
userInfo: null
},
//啟動時
onLoad: function () {
let this_ = this
/***
* 檢查微信用戶是否已經登錄到后臺服務器
* 已經登錄的標志,數據庫中存在 OPENID
*/
let code = null
//調用 login 接口
wx.login({
success: (res) => {
//得到登錄用戶的臨時 code
code = res.code
//向開發者服務器發送請求
let api = "wx/getLoginCertificate"
let config = {
url: api,
method: "GET",
data: {
code: code
}
}
let promise = httpRequest.wxRequest(config)
promise.then(res => {
let isHas = null
// 有沒有完整的微信登錄者信息
isHas = res.data == 0 ? false : true
app.globalData.isHasUserInfo = isHas
this_.setData({
isHasUserInfo: isHas
})
}).catch(res => {
console.log("fail", res)
});
}
})
}
})
代碼解釋:
- 一般會在微信小程序啟動時,也就是在頁面onload 函數中調用 wx.login接口,檢查用戶是否登錄過。
- http://127.0.0.1:8080/wx/getLoginCertificate 是開發者服務器提供的對外處理微信用戶信息的接口。
- 最后只是簡單地輸出開發者服務器端返回的數據。
- httpRequest.wxRequest(config)是自定義的封裝wx.request接口的請求組件。
function wxRequest(config) {
//返回的數據類型
let dataType = config.dataType == null ? "json" : config.dataType;
let responseType = config.responseType == null ? "text" : config.responseType;
//服務器基地址
let serverUrl = "http://127.0.0.1:8080/"
//超時
let timeout = config.timeout == null ? 50000 : config.timeout;
//目標地址,基地址+接口
let url = serverUrl + config.url;
//數據提交方式
let method = config.method == null ? "GET" : config.method;
//提交數據
let data = config.data == null ? null : config.data
//頭信息
let header = {
// 默認值
'content-type': 'application/json',
'x-requested-with': 'XMLHttpRequest'
}
let sessionId = wx.getStorageSync('sessionId')
if (sessionId) {
header["cookie"] = sessionId
}
return new Promise(function (resolve, reject) {
wx.request({
url: url,
data: data,
//返回的數據類型(json)
dataType: dataType,
enableCache: false,
enableHttp2: false,
enableQuic: false,
method: method,
header: header,
responseType: responseType,
timeout: timeout,
success: (res) => {
console.log("requestData", res)
if (res.cookies != null && res.cookies.length != 0)
wx.setStorageSync('sessionId', res.cookies[0])
resolve(res)
},
fail: (res) => {
console.log("requestException", res)
reject(res)
}
})
})
}
第三步:創建開發者服務器程序(后臺應用)
本文使用 spring boot快速搭建后臺應用程序。在項目的 pom.xml文件中除了必要的依賴包外,還需要添加以下 的依賴包。
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<dependency>
<groupId>org.Apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>MySQL</groupId>
<artifactId>mysql-connector-JAVA</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
- fastjson 是阿里云提供的開源 JSON解析框架。
- 微信小程序和開發者服務器構建的項目結構,是標準的前后端分離模式。
- 請求與響應時,數據交互常使用JSON格式。這時使用 fastjson 作為json解析器,當然,也可以選擇其它的類似解析器。
- httpclient 是一個http請求組件。
- mysql-connector-java 本文案例使用 MySQL數據庫,需要加載相應的驅動包。
- mybatis-plus-boot-starter,mybatis-plus 依賴包。
在后臺應用中編寫處理器(響應)組件:
@RestController
@RequestMapping("/wx")
public class WxAction {
@Autowired
private IWxService wxService;
/***
* 獲取到微信用戶的 OPENID
*/
@GetMapping("/getLoginCertificate")
public String getLoginCertificate(@RequestParam("code") String code) throws Exception {
WxUserInfo wxInfo = this.wxService.getLoginCertificate(code);
//用戶不存在,或者用戶的信息不全
return wxInfo==null || wxInfo.getNickName()==null?"0":"1";
}
代碼解釋:
- IWxService是處理器依賴的業務組件,提供有 getLoginCertificate()方法用來實現通過code向微信接口服務器換取微信登錄者的 openId和session_key。
編寫業務組件:
@Service
public class WxService implements IWxService {
@Override
public WxUserInfo getLoginCertificate(String code) throws Exception {
//請求地址
String requestUrl = WxUtil.getWxServerUrl(code);
// 發送請求
String response = HttpClientUtils.getRequest(requestUrl);
//格式化JSON數據
WxUserInfo wxUserInfo = JSONObject.parseobject(response, WxUserInfo.class);
//檢查數據庫中是否存在 OPENID
WxUserInfo wxUserInfo_ = this.wxUserMapper.selectById(wxUserInfo.getOpenId());
if (wxUserInfo_ == null) {
//數據庫中沒有用戶的 OPENID,添加到數據庫中
this.wxUserMapper.insert(wxUserInfo);
} else {
if (!wxUserInfo.getSessionKey().equals(wxUserInfo_.getSessionKey())) {
//如果數據庫保存的session_key和最新的session_key 不相同,則更新
wxUserInfo_.setSessionKey(wxUserInfo.getSessionKey());
this.wxUserMapper.updateById(wxUserInfo_);
}
}
return wxUserInfo_;
}
}
代碼解釋:
- WxUtil 是自定義的一個工具組件,用來構建請求微信接口服務器的 url。
- https://api.weixin.qq.com/sns/jscode2session是微信接口服務器對外提供的接口,請求此接口時,需要提供 4 個請求數據。
- appid:小程序 appId。
- secret:小程序 appSecret。
- js_code:獲取到的微信登錄者的臨時 code。
- grant_type:授權類型,此處只需填寫 authorization_code。
public class WxUtil {
private final static String APP_ID = "微信小程序開發者申請的 appid";
private final static String APP_SECRET = "微信小程序開發者申請的 APP_SECRET";
//
private final static String WX_LOGIN_SERVER_URL = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
public static String getWxServerUrl(String code) throws IOException {
String url = MessageFormat.format(WX_LOGIN_SERVER_URL, new String[]{APP_ID, APP_SECRET, code});
return url;
}
}
- HttpClientUtils也是一個自定義組件,用來向指定的服務器發送 http請求。
public class HttpClientUtils {
/**
* GET請求
*/
public static String getRequest(String url) throws Exception {
//HttpClient對象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
try {
HttpGet httpGet = new HttpGet(url);
response = httpClient.execute(httpGet);
//響應體
HttpEntity entity = response.getEntity();
if (entity != null) {
//格式化響應體
return EntityUtils.toString(entity);
}
} catch (ClientProtocolException e) {
throw e;
} catch (IOException e) {
throw e;
} finally {
response.close();
httpClient.close();
}
return null;
}
}
- WxUserInfo 是自定義的數據封裝類。微信接口服務器返回的數據是以JSON格式組裝的,這里需要格式成對象數據,便于在 java中處理。本文使用 MyBatisPlus操作數據庫,此類也對應數據庫中的gk_wx_user表。
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("gk_wx_user")
public class WxUserInfo {
//OPEN_id
@TableId(type = IdType.ASSIGN_ID, value = "open_id")
private String openId;
//會話密鑰
@TableField(value = "session_key")
private String sessionKey;
//頭像路徑
@TableField("avatar_url")
private String avatarUrl;
//城市
private String city;
//國家
private String country;
//性別
private String gender;
//語言
private String language;
//昵稱
@TableField("nick_name")
private String nickName;
//備注名或真實名
@TableField("real_name")
private String realName;
//省份
private String province;
//學生ID
@TableField("stu_id")
private Integer stuId;
}
MyBatis 數據庫映射組件:
@Repository
public interface WxUserMapper extends BaseMapper<WxUserInfo> {
}
第四步:測試。
先啟動后臺應用程序,再啟動微信小程序,可以在數據庫表中查看到如下信息。
微信用戶的openid和session_key已經保存到后臺的數據庫表中。
2.1.2wx.checkSession(Object object)
官方文檔中,有一段對 session_key的生命周期的描述。
- session_key的生命周期有不確定性,可以使用 wx.login接口刷新 session_key。為了避免頻繁調用 wx.login 接口,可以通過調用 wx.checkSession(Object object)接口判斷session_key是否已經過期。
- 當開發者在實現自定義登錄態時,可以考慮以 session_key 有效期作為自身登錄態有效期,也可以實現自定義的時效性策略。
wx.checkSession 的功能,可以使用此接口判斷session_key是否過期。
- 調用成功說明當前 session_key 未過期。
- 調用失敗說明 session_key 已過期。
2.2 用戶信息接口
wx.login接口僅能獲取到微信登錄者的有限數據,如果想要獲取到登錄者的更多個人信息,可以使用用戶信息接口中的相關API。
- wx.getUserProfile(Object object)。獲取用戶信息,頁面產生點擊事件(例如 button 上 bindtap 的回調中)后才可調用,每次請求都會彈出授權窗口,用戶同意后返回 userInfo。
- wx.getUserInfo(Object object) 。和 wx.getUserProfile的功能一樣,在基礎庫 2.10 的后續版本中,其功能已經被削弱。
- UserInfo是用戶信息封裝類。
getUserProfile是從 基礎庫2.10.4版本開始支持的接口,該接口用來替換 wx.getUserInfo,意味著官方不建議再使用getUserInfo接口獲取用戶的個人信息。
下圖是官方提供的 2 個接口的功能對比圖。
為了避免頻繁彈窗,可以在第一次獲取到用戶信息后保存在數據庫中以備以后所用。為了獲取到用戶的敏感數據,在后臺要通過getUserProfile接口所獲取的數據進行解密操作。
2.2.2wx.getUserProfile
下面通過具體代碼講解如何保存微信登錄者的個人數據。先了解一下整個數據獲取的流程,這里直接截取官方提供的一張流程圖。
獲取微信登錄者的個人信息,需要經過 2 個步驟。
簽名效驗:
- 通過調用wx.getUserProfile接口獲取數據時,接口會同時返回 rawData、signature,其中 signature = sha1( rawData + session_key )。
- 開發者將 signature、rawData 發送到開發者服務器進行校驗。服務器利用用戶對應的 session_key 使用相同的算法計算出簽名 signature2 ,比對signature 與 signature2 即可校驗數據的完整性。
解密加密數據:
- 對稱解密使用的算法為 AES-128-CBC,數據采用PKCS#7填充。
- 對稱解密的目標密文為 Base64_Decode(encryptedData)。
- 對稱解密秘鑰 aeskey = Base64_Decode(session_key), aeskey 是16字節。
- 對稱解密算法初始向量 為Base64_Decode(iv),其中iv由數據接口返回。
具體編寫實現。
第一步:在微信小程序端編碼。
在index.wxml頁面中添加一個按鈕,并注冊bindtap事件。
<view>
<button bindtap="getUserProfile">獲取用戶數據</button>
</view>
在index.js中添加一個名為getUserProfile的事件回調函數。為了避免不必要的彈窗,只有當后臺沒有獲取到個人數據時,才調用wx.getUserProfile接口。
getUserProfile: function (e) {
let this_ = this
if (!this.data.isHasUserInfo) {
//如果服務器端沒有保存完整的微信登錄者信息
wx.getUserProfile({
desc: '需要完善您的資料!',
success: (res) => {
this_.setData({
//小程序中用來顯示個人信息
userInfo: res.userInfo,
isHasUserInfo: true
})
//再次登錄,因為 session_key 有生命中周期
wx.login({
success(res_) {
//保存到服務器端
let config = {
url: "wx/wxLogin",
method: "GET",
data: {
code: res_.code,
//明文數據
rawData: res.rawData,
//加密數據
encryptedData: res.encryptedData,
iv: res.iv,
//數字簽名
signature: res.signature
}
}
let promise = httpRequest.wxRequest(config)
promise.then(res => {
//返回
console.log("wxLogin", res)
}).catch(res => {
console.log("fail", res)
});
}
})
}
})
}
}
服務器端代碼:
在pom.xml文件中添加如下依賴包,用來解密數據。
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
在處理器類WxAction中添加wxLogin響應方法。
@RestController
@RequestMapping("/wx")
public class WxAction {
@Autowired
private IWxService wxService;
/***
*
* @param code
* @param rawData
* @param encryptedData
* @param iv
* @param signature
* @return
* @throws Exception
*/
@GetMapping("/wxLogin")
public WxUserInfo wxLogin(@RequestParam("code") String code, @RequestParam("rawData") String rawData,
@RequestParam("encryptedData") String encryptedData, @RequestParam("iv") String iv,
@RequestParam("signature") String signature) throws Exception {
WxUserInfo wxInfo = this.wxService.getWxUserInfo(code, rawData, encryptedData, iv, signature);
return wxInfo;
}
}
業務代碼:
小程序中傳遞過來的數據是經過base64編碼以及加密的數據,需要使用 Base64解碼字符串,再使用解密算法解密數據。先提供一個解密方法。
public String decrypt(String session_key, String iv, String encryptData) {
String decryptString = "";
//解碼經過 base64 編碼的字符串
byte[] sessionKeyByte = Base64.getDecoder().decode(session_key);
byte[] ivByte = Base64.getDecoder().decode(iv);
byte[] encryptDataByte = Base64.getDecoder().decode(encryptData);
try {
Security.addProvider(new BouncyCastleProvider());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
//得到密鑰
Key key = new SecretKeySpec(sessionKeyByte, "AES");
//AES 加密算法
AlgorithmParameters algorithmParameters = AlgorithmParameters.getInstance("AES");
algorithmParameters.init(new IvParameterSpec(ivByte));
cipher.init(Cipher.DECRYPT_MODE, key, algorithmParameters);
byte[] bytes = cipher.doFinal(encryptDataByte);
decryptString = new String(bytes);
} catch (Exception e) {
e.printStackTrace();
}
return decryptString;
}
具體獲取數據的業務實現:
@Override
public WxUserInfo getWxUserInfo(@NotNull String code, @NotNull String rawData, @NotNull String encryptedData, @NotNull String iv, @NotNull String signature) throws Exception {
//會話密鑰
WxUserInfo wxUserInfo = this.getLoginCertificate(code);
String signature2 = DigestUtils.sha1Hex(rawData + wxUserInfo.getSessionKey());
if (!signature.equals(signature2)) {
throw new Exception("數字簽名驗證失敗");
}
//數字簽名驗證成功,解密
String infos = this.decrypt(wxUserInfo.getSessionKey(), iv, encryptedData);
//反序列化 JSON 數據
WxUserInfo wxUserInfo_ = JSONObject.parseObject(infos, WxUserInfo.class);
wxUserInfo_.setSessionKey(wxUserInfo.getSessionKey());
wxUserInfo_.setOpenId(wxUserInfo.getOpenId());
//更新數據庫
this.wxUserMapper.updateById(wxUserInfo_);
return wxUserInfo_;
}
測試,啟動微信小程序和后臺應用,在小程序中觸發按鈕事件。
在彈出的對話框中,選擇允許。
查看后臺數據庫表中的數據。
能夠獲取到的微信登錄者個人信息都保存到了數據庫表中。至于怎么使用這些數據,可以根據自己的業務需要定制。
3.總結
微信開發平臺,提供有諸多接口,可以幫助開發者獲取到有用的數據。本文主要介紹 wx.login和wx.getProfile接口,因篇幅所限,不能對其它接口做詳細介紹 ,有興趣者可以查閱官方文檔。
官方文檔只會對接口功能做些介紹 ,如要靈活運用這些接口,還需要結合實際需要演練一下,如此方能有切身體會。
如果本文對你有幫助,別忘記給我個3連 ,點贊,轉發,評論,,咱們下期見。
收藏 等于白嫖,點贊才是真情。
原文
https://www.cnblogs.com/guo-ke/p/16276843.html