( 十 )、SpringBoot整合微信小程序登錄
1. 微信小程序登錄流程
微信小程序登錄流程涉及到三個角色:小程序、開發者服務器、微信服務器
三者交互步驟如下:
第一步:
第二步:
第三步:
第四步:
第五步:
第六步:
第七步:
第八步:
第九步:
第十步:
小程序
通過wx.login()獲取code。
第二步:
小程序
通過wx.request()發送code到開發者服務器。
第三步:
開發者服務器
接收小程序發送的code,並攜帶appid、appsecret(這兩個需要到微信小程序后台查看)、code發送到微信服務器
。
第四步:
微信服務器
接收開發者服務器發送的appid、appsecret、code進行校驗。校驗通過后向開發者服務器
發送session_key、openid。
第五步:
開發者服務器
自己生成一個skey(自定義登錄狀態)與openid、session_key進行關聯,並存到數據庫中(mysql、redis等)。
第六步:
開發者服務器
返回生成skey(自定義登錄狀態)到小程序。
第七步:
小程序
存儲skey(自定義登錄狀態)到本地。
第八步:
小程序
通過wx.request()發起業務請求到開發者服務器
,同時攜帶skey(自定義登錄狀態)。
第九步:
開發者服務器
接收小程序
發送的skey(自定義登錄狀態),查詢skey在數據庫中是否有對應的openid、session_key。
第十步:
開發者服務器
返回業務數據到小程序
。

yml:
<!--hutool具包-->
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.4.0</version> </dependency>
<!--簡化代碼的工具包--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
<!-- mybatis-plus-spring-boot-starter-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
wx返回的用戶信息:
/** * @Author dw * @ClassName WeChatUserInfo * @Description 微信用戶信息 * @Date 2020/8/28 14:14 * @Version 1.0 */ @Data public class WeChatUserInfo { /** * 微信返回的code */ private String code; /** * 非敏感的用戶信息 */ private String rawData; /** * 簽名信息 */ private String signature; /** * 加密的數據 */ private String encrypteData; /** * 加密密鑰 */ private String iv; }
WeChatUtil工具:
/** * @Author dw * @ClassName WeChatUtil * @Description * @Date 2020/8/28 10:56 * @Version 1.0 */ public class WeChatUtil { public static JSONObject getSessionKeyOrOpenId(String code) { String requestUrl = "https://api.weixin.qq.com/sns/jscode2session"; HashMap<String, Object> requestUrlParam = new HashMap<>(); //小程序appId requestUrlParam.put("appid", "小程序appId"); //小程序secret requestUrlParam.put("secret", "小程序secret"); //小程序端返回的code requestUrlParam.put("js_code", code); //默認參數 requestUrlParam.put("grant_type", "authorization_code"); //發送post請求讀取調用微信接口獲取openid用戶唯一標識 String result = HttpUtil.get(requestUrl, requestUrlParam); JSONObject jsonObject = JSONUtil.parseObj(result); return jsonObject; } public static JSONObject getUserInfo(String encryptedData, String sessionKey, String iv) throws Base64DecodingException { // 被加密的數據 byte[] dataByte = Base64.decode(encryptedData); // 加密秘鑰 byte[] keyByte = Base64.decode(sessionKey); // 偏移量 byte[] ivByte = Base64.decode(iv); try { // 如果密鑰不足16位,那么就補足. 這個if 中的內容很重要 int base = 16; if (keyByte.length % base != 0) { int groups = keyByte.length / base + (keyByte.length % base != 0 ? 1 : 0); byte[] temp = new byte[groups * base]; Arrays.fill(temp, (byte) 0); System.arraycopy(keyByte, 0, temp, 0, keyByte.length); keyByte = temp; } // 初始化 Security.addProvider(new BouncyCastleProvider()); Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding", "BC"); SecretKeySpec spec = new SecretKeySpec(keyByte, "AES"); AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES"); parameters.init(new IvParameterSpec(ivByte)); // 初始化 cipher.init(Cipher.DECRYPT_MODE, spec, parameters); byte[] resultByte = cipher.doFinal(dataByte); if (null != resultByte && resultByte.length > 0) { String result = new String(resultByte, "UTF-8"); return JSONUtil.parseObj(result); } } catch (Exception e) { } return null; }
登錄controller:
/** * @Author dw * @ClassName WeChatUserLoginController * @Description * @Date 2020/8/28 14:12 * @Version 1.0 */ @RestController public class WeChatUserLoginController { @Resource private IUserService userService; /** * 微信用戶登錄詳情 */ @PostMapping("wx/login") public ResultInfo user_login(@RequestBody WeChatUserInfo weChatUserInfo) throws Base64DecodingException { // 2.開發者服務器 登錄憑證校驗接口 appId + appSecret + 接收小程序發送的code JSONObject SessionKeyOpenId = WeChatUtil.getSessionKeyOrOpenId(weChatUserInfo.getCode()); // 3.接收微信接口服務 獲取返回的參數 String openid = SessionKeyOpenId.get("openid", String.class); String sessionKey = SessionKeyOpenId.get("session_key", String.class); // 用戶非敏感信息:rawData // 簽名:signature JSONObject rawDataJson = JSONUtil.parseObj(weChatUserInfo.getRawData()); // 4.校驗簽名 小程序發送的簽名signature與服務器端生成的簽名signature2 = sha1(rawData + sessionKey) // String signature2 = DigestUtils.sha1Hex(weChatUserInfo.getRawData() + sessionKey); // if (!weChatUserInfo.getSignature().equals(signature2)) { // return ResultInfo.error( "簽名校驗失敗"); //} //encrypteData比rowData多了appid和openid JSONObject userInfo = WeChatUtil.getUserInfo(weChatUserInfo.getEncrypteData(), sessionKey, weChatUserInfo.getIv()); // 5.根據返回的User實體類,判斷用戶是否是新用戶,是的話,將用戶信息存到數據庫;不是的話,更新最新登錄時間 QueryWrapper<User> userQueryWrapper = new QueryWrapper<>(); userQueryWrapper.lambda().eq(User::getLoginName, openid); int userCount = userService.count(userQueryWrapper); // uuid生成唯一key,用於維護微信小程序用戶與服務端的會話(或者生成Token) String skey = UUID.randomUUID().toString(); if (userCount <= 0) { // 用戶信息入庫 String nickName = rawDataJson.get("nickName",String.class); String avatarUrl = rawDataJson.get("avatarUrl",String.class); String gender = rawDataJson.get("gender",String.class); String city = rawDataJson.get("city",String.class); String country = rawDataJson.get("country",String.class); String province = rawDataJson.get("province",String.class); // 新增用戶到數據庫 } else { // 已存在,更新用戶登錄時間 } //6. 把新的skey返回給小程序 return ResultInfo.success(); } }
全局返回結果:
public class ResultInfo { /** * 響應代碼 */ private String code; /** * 響應消息 */ private String message; /** * 響應結果 */ private Object result; public ResultInfo() { } public ResultInfo(BaseErrorInfoInterface errorInfo) { this.code = errorInfo.getResultCode(); this.message = errorInfo.getResultMsg(); } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Object getResult() { return result; } public void setResult(Object result) { this.result = result; } /** * 成功 * * @return */ public static ResultInfo success() { return success(null); } /** * 成功 * @param data * @return */ public static ResultInfo success(Object data) { ResultInfo rb = new ResultInfo(); rb.setCode(CommonEnum.SUCCESS.getResultCode()); rb.setMessage(CommonEnum.SUCCESS.getResultMsg()); rb.setResult(data); return rb; } /** * 失敗 */ public static ResultInfo error(BaseErrorInfoInterface errorInfo) { ResultInfo rb = new ResultInfo(); rb.setCode(errorInfo.getResultCode()); rb.setMessage(errorInfo.getResultMsg()); rb.setResult(null); return rb; } /** * 失敗 */ public static ResultInfo error(String code, String message) { ResultInfo rb = new ResultInfo(); rb.setCode(code); rb.setMessage(message); rb.setResult(null); return rb; } /** * 失敗 */ public static ResultInfo error(String message) { ResultInfo rb = new ResultInfo(); rb.setCode("-1"); rb.setMessage(message); rb.setResult(null); return rb; }
}
4. 微信小程序
4.1 初始配置

初始配置
4.2 me.wxml
<view class="container">
<!-- 登錄組件 https://developers.weixin.qq.com/miniprogram/dev/api/wx.getUserInfo.html -->
<button wx:if="{{!hasUserInfo}}" open-type="getUserInfo" bind:getuserinfo="onGetUserInfo">授權登錄</button>
<!-- 登錄后使用open-data -->
<view class="avatar-container avatar-position">
<image src="{{userInfo.avatarUrl}}" wx:if="{{hasUserInfo}}" class="avatar" />
<open-data wx:if="{{hasUserInfo}}" type="userNickName"></open-data>
</view>
</view>
4.3 me.wxss
無
4.4 me.json
{
}
4.5 me.js
// pages/me/me.js Page({ /** * 頁面的初始數據 */ data: { hasUserInfo: false, userInfo: null }, onLoad: function() { // 頁面加載時使用用戶授權邏輯,彈出確認的框 this.userAuthorized() }, userAuthorized() { wx.getSetting({ success: data => { if (data.authSetting['scope.userInfo']) { wx.getUserInfo({ success: data => { this.setData({ hasUserInfo: true, userInfo: data.userInfo }) } }) } else { this.setData({ hasUserInfo: false }) } } }) }, onGetUserInfo(e) { const userInfo = e.detail.userInfo if (userInfo) { // 1. 小程序通過wx.login()獲取code wx.login({ success: function(login_res) { //獲取用戶信息 wx.getUserInfo({ success: function(info_res) { // 2. 小程序通過wx.request()發送code到開發者服務器 wx.request({ url: 'http://localhost:8080/wx/login', method: 'POST', header: { 'content-type': 'application/json' }, data: { code: login_res.code, //臨時登錄憑證 rawData: info_res.rawData, //用戶非敏感信息 signature: info_res.signature, //簽名 encrypteData: info_res.encryptedData, //用戶敏感信息 iv: info_res.iv //解密算法的向量 }, success: function(res) { if (res.data.status == 200) { // 7.小程序存儲skey(自定義登錄狀態)到本地 wx.setStorageSync('userInfo', userInfo); wx.setStorageSync('skey', res.data.data); } else{ console.log('服務器異常'); } }, fail: function(error) { //調用服務端登錄接口失敗 console.log(error); } }) } }) } }) this.setData({ hasUserInfo: true, userInfo: userInfo }) } } })
4.6 app.json
設置app.json的pages { "pages":[ "pages/me/me" ], "window":{ "backgroundTextStyle":"light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "WeChat", "navigationBarTextStyle":"black" }, "debug":true }
5. 測試
啟動開發者服務器,啟動SpringBoot的main方法。
打開微信小程序開發者工具

清空緩存
點擊授權登錄,並允許。

授權登錄
登錄成功
查看數據庫,openid、skey以及用戶信息等存入了數據庫。

用戶信息入庫
同時微信小程序將skey等存儲到本地,每次發起請求時都可以攜帶上。