Spring Boot+微信小程式_儲存微信登入者的個人資訊

一枚大果殼發表於2022-05-16

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 登入介面

登入介面中有 3API,對於開發者來說,使用頻率較高的是 login介面,此環節將重點介紹此介面。

非本文特別關注的介面,會簡略帶過。

wx.pluginLogin(Object args):此介面只能在外掛中可以呼叫,呼叫此介面獲得外掛使用者的標誌憑證code,外掛可使用此憑證換取用於識別使用者的唯一標識 OpenpId

使用者不同、宿主小程式不同或外掛不同的情況下,該標識均不相同,即當且僅當同一個使用者在同一個宿主小程式中使用同一個外掛時,OpenpId 才會相同。

對於一般開發者,此 介面用的不是很多,具體使用細節在此處也不做過多複述。

什麼是 OpenId?

當微信使用者登入公眾號或小程式時,微信平臺為每一個微信登入者分配的一個唯一識別符號號。

2.1.1 wx.login(Object object)

功能描述:

  • 開發者使用此介面可以獲取到微信登入者登入憑證(code)

    登入憑證具有臨時性,也就是每次呼叫時都會不一樣,所以code 只能使用一次。

  • 開發者可以通過臨時code,再向微信介面伺服器索取登入者的唯一識別符號 OpenId、微信開發平臺賬號的唯一標識 UnionID(需要當前小程式已繫結到微信開放平臺帳號)、以及會話金鑰 session_key

那麼,獲取到的openIdsession_key對於開發者而言,有什麼實質性的意義?

  • 根據 OpenId的唯一性特點,可以在微信使用者第一次登入時,把OpenID儲存在資料庫或快取中,在後續登入時,只需要檢查使用者的 OpenId是否存在於資料庫或快取中,便能實現自動登入功能。

  • session_key 也稱會話金鑰,用來解密微信登入者的敏感資料。

    後文將詳細介紹。

如何獲取OpenId

現通過一個簡單案例,實現微信小程式端與開發者伺服器之間的資料互動。以此瞭解開發者伺服器如何通過微信小程式傳遞過來的使用者臨時 code換取到登入者的更多資訊。

實現之前,先通過一個簡易演示圖瞭解其過程。

wx01.png

簡單描述整個請求過程:

  • 微信使用者開啟微信小程式後,開發者在微信小程式中通過呼叫wx.login介面獲取到臨時登入憑證 code
  • 在微信小程式中呼叫 wx.request 介面向開發者伺服器傳送 http 請求,需要把登入憑證 code一併傳送過去。
  • 開發者伺服器使用傳送過來的 code 以及開發者憑證資訊向微信介面伺服器索取微信登入者的 openIdsession_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-startermybatis-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微信介面伺服器換取微信登入者的 openIdsession_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> {

}

第四步:測試。

先啟動後臺應用程式,再啟動微信小程式,可以在資料庫表中檢視到如下資訊。

資料庫.png

微信使用者的openidsession_key已經儲存到後臺的資料庫表中。

2.1.2 wx.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)。獲取使用者資訊,頁面產生點選事件(例如 buttonbindtap 的回撥中)後才可呼叫,每次請求都會彈出授權視窗,使用者同意後返回 userInfo
  • wx.getUserInfo(Object object) 。和 wx.getUserProfile的功能一樣,在基礎庫 2.10 的後續版本中,其功能已經被削弱。
  • UserInfo是使用者資訊封裝類。

getUserProfile是從 基礎庫2.10.4版本開始支援的介面,該介面用來替換 wx.getUserInfo,意味著官方不建議再使用getUserInfo介面獲取使用者的個人資訊。

下圖是官方提供的 2 個介面的功能對比圖。

介面調整.png

為了避免頻繁彈窗,可以在第一次獲取到使用者資訊後儲存在資料庫中以備以後所用。為了獲取到使用者的敏感資料,在後臺要通過getUserProfile介面所獲取的資料進行解密操作。

2.2.2 wx.getUserProfile

下面通過具體程式碼講解如何儲存微信登入者的個人資料。先了解一下整個資料獲取的流程,這裡直接擷取官方提供的一張流程圖。

解密碼.jpg

獲取微信登入者的個人資訊,需要經過 2 個步驟。

簽名效驗:

  • 通過呼叫wx.getUserProfile介面獲取資料時,介面會同時返回 rawDatasignature,其中 signature = sha1( rawData + session_key )
  • 開發者將 signaturerawData 傳送到開發者伺服器進行校驗。伺服器利用使用者對應的 session_key 使用相同的演算法計算出簽名 signature2 ,比對signaturesignature2 即可校驗資料的完整性。

解密加密資料:

  • 對稱解密使用的演算法為 AES-128-CBC,資料採用PKCS#7填充。
  • 對稱解密的目標密文為 Base64_Decode(encryptedData)
  • 對稱解密祕鑰 aeskey = Base64_Decode(session_key), aeskey16位元組。
  • 對稱解密演算法初始向量 為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_;
}

測試,啟動微信小程式和後臺應用,在小程式中觸發按鈕事件。

wx03.png

在彈出的對話方塊中,選擇允許

wx04.png

檢視後臺資料庫表中的資料。

wx05.png

能夠獲取到的微信登入者個人資訊都儲存到了資料庫表中。至於怎麼使用這些資料,可以根據自己的業務需要定製。

3.總結

微信開發平臺,提供有諸多介面,可以幫助開發者獲取到有用的資料。本文主要介紹 wx.loginwx.getProfile介面,因篇幅所限,不能對其它介面做詳細介紹 ,有興趣者可以查閱官方文件。

官方文件只會對介面功能做些介紹 ,如要靈活運用這些介面,還需要結合實際需要演練一下,如此方能有切身體會。

相關文章