1. 程式人生 > >微信小程式java登入授權解密獲取unionId(填坑)

微信小程式java登入授權解密獲取unionId(填坑)

官方流程圖:

第一步:獲取code

說明:

  1. 小程式呼叫wx.login() 獲取 臨時登入憑證code ,並回傳到開發者伺服器。

  2. 開發者伺服器以code換取 使用者唯一標識openid 和 會話金鑰session_key

之後開發者伺服器可以根據使用者標識來生成自定義登入態,用於後續業務邏輯中前後端互動時識別使用者身份。

//app.js
App({
  onLaunch: function() {
    wx.login({
      success: function(res) {
        if (res.code) {
          //發起網路請求
          wx.request({
            url: 'https://test.com/onLogin',
            data: {
              code: res.code
            }
          })
        } else {
          console.log('登入失敗!' + res.errMsg)
        }
      }
    });
  }
})

關於unionId,這裡需要說明一下,如果應用只限於小程式內則不需要unionId,直接通過openId可以確定使用者身份,但是如果需要跨應用 如:網頁應用,app應用時則需要使用到unionId作為身份標識

UnionID獲取途徑

綁定了開發者帳號的小程式,可以通過下面3種途徑獲取UnionID。

  1. 呼叫介面wx.getUserInfo,從解密資料中獲取UnionID。注意本介面需要使用者授權,請開發者妥善處理使用者拒絕授權後的情況。

  2. 如果開發者帳號下存在同主體的公眾號,並且該使用者已經關注了該公眾號。開發者可以直接通過wx.login獲取到該使用者UnionID,無須使用者再次授權。

  3. 如果開發者帳號下存在同主體的公眾號或移動應用,並且該使用者已經授權登入過該公眾號或移動應用。開發者也可以直接通過wx.login獲取到該使用者UnionID,無須使用者再次授權。

說明完了,我們繼續下一步,主要需要區分獲得授權,和未獲得授權情況

第二步:通過code換取sessionKey等個人資訊

1、未獲得授權

      注意:這裡unionId如果滿足上面2、3所說獲取條件,則會在這一步裡返回,如果不滿足則需要呼叫wx.getUserInfo來獲取,這個方法需要獲得使用者授權。

public class Test {

	private static final String APPID = "";// 微信應用唯一標識
	private static final String SECRET = "";

	public void main(String code) {

		JSONObject jsonObject = code2sessionKey(code);

		String openId = jsonObject.getString("openid");// 使用者唯一標識

		String session_key = jsonObject.getString("session_key");// 金鑰

		// 滿足UnionID下發條件的情況下,返回
		String unionId = jsonObject.getString("unionid");

	}

	/**
	 * 傳送請求用code換取sessionKey和相關資訊
	 * 
	 * @param code
	 * @return
	 */
	public static JSONObject code2sessionKey(String code) {
		String stringToken = String.format(
				"https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
				APPID, SECRET, code);
		String response = HttpUtils.httpsRequestToString(stringToken, "GET", null);
		return JSON.parseObject(response);
	}

	/**
	 * 傳送https請求
	 * 
	 * @param path
	 * @param method
	 * @param body
	 * @return
	 */
	public static String httpsRequestToString(String path, String method, String body) {
		if (path == null || method == null) {
			return null;
		}
		String response = null;
		InputStream inputStream = null;
		InputStreamReader inputStreamReader = null;
		BufferedReader bufferedReader = null;
		HttpsURLConnection conn = null;
		try {
			// 建立SSLConrext物件,並使用我們指定的信任管理器初始化
			SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
			TrustManager[] tm = { new X509TrustManager() {
				@Override
				public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
				}

				@Override
				public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
				}

				@Override
				public X509Certificate[] getAcceptedIssuers() {
					return null;
				}

			} };
			sslContext.init(null, tm, new java.security.SecureRandom());

			// 從上面物件中得到SSLSocketFactory
			SSLSocketFactory ssf = sslContext.getSocketFactory();

			URL url = new URL(path);
			conn = (HttpsURLConnection) url.openConnection();
			conn.setSSLSocketFactory(ssf);

			conn.setDoOutput(true);
			conn.setDoInput(true);
			conn.setUseCaches(false);

			// 設定請求方式(get|post)
			conn.setRequestMethod(method);

			// 有資料提交時
			if (null != body) {
				OutputStream outputStream = conn.getOutputStream();
				outputStream.write(body.getBytes("UTF-8"));
				outputStream.close();
			}

			// 將返回的輸入流轉換成字串
			inputStream = conn.getInputStream();
			inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
			bufferedReader = new BufferedReader(inputStreamReader);
			String str = null;
			StringBuffer buffer = new StringBuffer();
			while ((str = bufferedReader.readLine()) != null) {
				buffer.append(str);
			}

			response = buffer.toString();
		} catch (Exception e) {

		} finally {
			if (conn != null) {
				conn.disconnect();
			}
			try {
				bufferedReader.close();
				inputStreamReader.close();
				inputStream.close();
			} catch (IOException execption) {

			}
		}
		return response;
	}
}

2、獲得授權情況下

當前臺獲得了使用者的授權後,我們就可以獲得使用者的個人資訊以及unionId(不管有沒有關注公眾號)

前臺介面:

wx.getUserInfo(OBJECT)

  1. 當用戶未授權過,呼叫該介面將直接報錯
  2. 當用戶授權過,可以使用該介面獲取使用者資訊

OBJECT引數說明:

引數名 型別 必填 說明 最低版本
withCredentials Boolean 是否帶上登入態資訊 1.1.0
lang String 指定返回使用者資訊的語言,zh_CN 簡體中文,zh_TW 繁體中文,en 英文。預設為en。 1.3.0
timeout Number 超時時間,單位 ms
success Function 介面呼叫成功的回撥函式
fail Function 介面呼叫失敗的回撥函式
complete Function 介面呼叫結束的回撥函式(呼叫成功、失敗都會執行)

注:當 withCredentials 為 true 時,要求此前有呼叫過 wx.login 且登入態尚未過期,此時返回的資料會包含 encryptedData, iv 等敏感資訊;當 withCredentials 為 false 時,不要求有登入態,返回的資料不包含 encryptedData, iv 等敏感資訊。

success返回引數說明:

引數 型別 說明
userInfo OBJECT 使用者資訊物件,不包含 openid 等敏感資訊
rawData String 不包括敏感資訊的原始資料字串,用於計算簽名。
signature String 使用 sha1( rawData + sessionkey ) 得到字串,用於校驗使用者資訊,參考文件 signature
encryptedData String 包括敏感資料在內的完整使用者資訊的加密資料,詳細見加密資料解密演算法
iv String 加密演算法的初始向量,詳細見加密資料解密演算法

示例程式碼:

<!--wxml-->
<!-- 如果只是展示使用者頭像暱稱,可以使用 <open-data /> 元件 -->
<open-data type="userAvatarUrl"></open-data>
<open-data type="userNickName"></open-data>
<!-- 需要使用 button 來授權登入 -->
<button wx:if="{{canIUse}}" open-type="getUserInfo" bindgetuserinfo="bindGetUserInfo">授權登入</button>
<view wx:else>請升級微信版本</view>
//js
Page({
  data: {
    canIUse: wx.canIUse('button.open-type.getUserInfo')
  },
  onLoad: function() {
    // 檢視是否授權
    wx.getSetting({
      success: function(res){
        if (res.authSetting['scope.userInfo']) {
          // 已經授權,可以直接呼叫 getUserInfo 獲取頭像暱稱
          wx.getUserInfo({
            success: function(res) {
              console.log(res.userInfo)
            }
          })
        }
      }
    })
  },
  bindGetUserInfo: function(e) {
    console.log(e.detail.userInfo)
  }
})

前臺呼叫getUserInfo後,可以將獲得的code值,encryptedData加密資料包,iv初始向量提示發給後臺

後臺程式碼如下:

public class Test {

	private static final String APPID = "";// 微信應用唯一標識
	private static final String SECRET = "";

	public void main(String code,String encryptedData,String iv) {

		JSONObject jsonObject = code2sessionKey(code);

		String openId = jsonObject.getString("openid");// 使用者唯一標識

		String session_key = jsonObject.getString("session_key");// 金鑰

		// 解密encryptedData,獲取unionId相關資訊
		JSONObject json = decryptionUserInfo(encryptedData, session_key, iv);
	}

	/**
	 * 傳送請求用code換取sessionKey和相關資訊
	 * 
	 * @param code
	 * @return
	 */
	public static JSONObject code2sessionKey(String code) {
		String stringToken = String.format(
				"https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
				APPID, SECRET, code);
		String response = HttpUtils.httpsRequestToString(stringToken, "GET", null);
		return JSON.parseObject(response);
	}
	/**
	 * 小程式解密使用者資料
	 * 
	 * @param encryptedData
	 * @param sessionKey
	 * @param iv
	 * @return
	 */
	public static JSONObject decryptionUserInfo(String encryptedData, String sessionKey, String iv) {
		// 被加密的資料
		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 JSONObject.parseObject(result);
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	/**
	 * 傳送https請求
	 * 
	 * @param path
	 * @param method
	 * @param body
	 * @return
	 */
	public static String httpsRequestToString(String path, String method, String body) {
		if (path == null || method == null) {
			return null;
		}
		String response = null;
		InputStream inputStream = null;
		InputStreamReader inputStreamReader = null;
		BufferedReader bufferedReader = null;
		HttpsURLConnection conn = null;
		try {
			// 建立SSLConrext物件,並使用我們指定的信任管理器初始化
			SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
			TrustManager[] tm = { new X509TrustManager() {
				@Override
				public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
				}

				@Override
				public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
				}

				@Override
				public X509Certificate[] getAcceptedIssuers() {
					return null;
				}

			} };
			sslContext.init(null, tm, new java.security.SecureRandom());

			// 從上面物件中得到SSLSocketFactory
			SSLSocketFactory ssf = sslContext.getSocketFactory();

			URL url = new URL(path);
			conn = (HttpsURLConnection) url.openConnection();
			conn.setSSLSocketFactory(ssf);

			conn.setDoOutput(true);
			conn.setDoInput(true);
			conn.setUseCaches(false);

			// 設定請求方式(get|post)
			conn.setRequestMethod(method);

			// 有資料提交時
			if (null != body) {
				OutputStream outputStream = conn.getOutputStream();
				outputStream.write(body.getBytes("UTF-8"));
				outputStream.close();
			}

			// 將返回的輸入流轉換成字串
			inputStream = conn.getInputStream();
			inputStreamReader = new InputStreamReader(inputStream, "UTF-8");
			bufferedReader = new BufferedReader(inputStreamReader);
			String str = null;
			StringBuffer buffer = new StringBuffer();
			while ((str = bufferedReader.readLine()) != null) {
				buffer.append(str);
			}

			response = buffer.toString();
		} catch (Exception e) {

		} finally {
			if (conn != null) {
				conn.disconnect();
			}
			try {
				bufferedReader.close();
				inputStreamReader.close();
				inputStream.close();
			} catch (IOException execption) {

			}
		}
		return response;
	}
}

與未授權情況下,多了一步解密資料包的動作,解密後就能獲得我們需要的資料啦,所以在處理登入時一定要考慮好使用者授權情況。

附:encryptedData 解密後 json 結構

{
    "openId": "OPENID",
    "nickName": "NICKNAME",
    "gender": GENDER,
    "city": "CITY",
    "province": "PROVINCE",
    "country": "COUNTRY",
    "avatarUrl": "AVATARURL",
    "unionId": "UNIONID",
    "watermark":
    {
    	"appid":"APPID",
	"timestamp":TIMESTAMP
    }
}

(填坑..) 注意:上述返回的結果中如果還是沒有unionId,這個時候需要檢查是否完成開發者資質認證...

微信開放平臺繫結小程式流程

前提:微信開放平臺帳號必須已完成開發者資質認證

開發者資質認證流程:

登入微信開放平臺(open.weixin.qq.com) – 帳號中心 – 開發者資質認證

img

繫結流程:

登入微信開放平臺(open.weixin.qq.com)—管理中心—公眾帳號—繫結公眾帳號

img