微信掃碼登錄


微信掃碼登錄

1. 使用背景

如今開發業務系統,已不是一個單獨的系統。往往需要同多個不同系統相互調用,甚至有時還需要跟微信,釘釘,飛書這樣平台對接。目前我開發的部分業務系統,已經完成微信公眾平台對接。作為知識總結,接下來,我們探討下對接微信公眾平台的一小部分功能,微信掃碼登錄。其中的關鍵點是獲取openid。我仔細查找了微信提供的開發文檔,主要有以下三個方式可實現。

  1. 通過微信公眾平台生成帶參數的二維
  2. 通過微信公眾平台微信網頁授權登錄
  3. 通過微信開發平台微信登錄功能

2. 開發環境搭建

2.1 內網穿透

微信所有的接口訪問,都要求使用域名。但多數開發者是沒有域名,給很多開發者測試帶來了麻煩。不過有以下兩種方案可以嘗試:

  1. 使用公司域名,讓公司管理員配置一個子域名指向你公司公網的一個ip的80端口。然后通過Nginx或者通過nat命令,將改域名定位到您的開發環境
  2. 使用內網穿透工具,目前市面有很多都可以使用免費的隧道。不過就是不穩定,不支持指定固定子域名或者已經被微信限制訪問。經過我大量收集資料,發現釘釘開發平台提供的內網穿透工具,比較不錯。用阿里的東西來對接微信東西,想想都為微信感到恥辱。你微信不為開發者提供便利,就讓對手來實現。

那釘釘的內網穿透工具具體怎么使用用的呢?

首先使用git下載釘釘內網穿透工具,下載好后找到windows_64目錄,在這里新建一個start.bat文件,內容為

ding -config=ding.cfg -subdomain=pro 8080

其中-subdomain 是用來生成子域名8080表示隱射本地8080端口
雙擊start.bat文件,最終啟動成功界面如下

釘釘內網穿透

經過我測試,這個相當穩定,並且可以指定靜態子域名。簡直就是業界良心

2.2 公眾號測試環境

訪問公眾平台測試賬號系統,可以通過微信登錄,可快速得到一個測試賬號。然后我們需要以下兩個配置

  • 接口配置信息

接口配置信息

在點擊提交按鈕時,微信服務器會驗證我們配置的這個URL是否有效。這個URL有兩個用途

  1. 通過簽名驗證地址是否有效
  2. 接收微信推送的信息,比如用戶掃碼后通知

簽名生成邏輯是用配置的token結合微信回傳的timestamp,nonce,通過字符串數組排序形成新的字符串,做SHA簽名,再將簽名后的二進制數組轉換成十六進制字符串。最終的內容就是具體的簽名信息。對應的java代碼如下

// author: herbert 公眾號:小院不小 20210424
	public static String getSignature(String token, String timestamp, String nonce) {
		String[] array = new String[] { token, timestamp, nonce };
		Arrays.sort(array);
		StringBuffer sb = new StringBuffer();
		for (String str : array) {
			sb.append(str);
		}
		try {
			MessageDigest md = MessageDigest.getInstance("SHA-1");
			md.update(sb.toString().getBytes());
			byte[] digest = md.digest();
			StringBuffer hexStr = new StringBuffer();
			String shaHex = "";
			for (int i = 0; i < digest.length; i++) {
				shaHex = Integer.toHexString(digest[i] & 0xFF);
				if (shaHex.length() < 2) {
					hexStr.append(0);
				}
				hexStr.append(shaHex);
			}
			return hexStr.toString();

		} catch (NoSuchAlgorithmException e) {
			logger.error("獲取簽名信息失敗", e.getCause());
		}
		return "";
	}

對應GET請求代碼如下

// author: herbert 公眾號:小院不小 20210424
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		logger.info("微信在配置服務器傳遞驗證參數");
		Map<String, String[]> reqParam = request.getParameterMap();
		for (String key : reqParam.keySet()) {
			logger.info(" {} = {}", key, reqParam.get(key));
		}

		String signature = request.getParameter("signature");
		String echostr = request.getParameter("echostr");
		String timestamp = request.getParameter("timestamp");
		String nonce = request.getParameter("nonce");

		String buildSign = WeixinUtils.getSignature(TOKEN, timestamp, nonce);

		logger.info("服務器生成簽名信息:{}", buildSign);
		if (buildSign.equals(signature)) {
			response.getWriter().write(echostr);
			logger.info("服務生成簽名與微信服務器生成簽名相等,驗證成功");
			return;
		}
	}

微信服務器驗證規則是原樣返回echostr,如果覺得簽名麻煩,直接返回echostr也是可以的。

  • JS接口安全域名

JS接口安全域名

這個配置主要用途是解決H5與微信JSSDK集成。微信必須要求指定的域名下,才能調用JSSDK

3. 測試項目搭建

為了測試掃碼登錄效果,我們需要搭建一個簡單的maven工程。工程中具體文件目錄如下

工程目錄

用戶掃描二維碼得到對應的openid,然后在userdata.json文件中,根據openid查找對應的用戶。找到了,就把用戶信息寫入緩存。沒找到,就提醒用戶綁定業務賬號信息。前端通過定時輪詢,從服務緩存中查找對應掃碼的用戶信息

userdata.json文件中的內容如下

[{
	"userName": "張三",
	"password":"1234",
	"userId": "000001",
	"note": "掃碼登錄",
	"openId": ""
}]

從代碼可以知道,后端提供了5個Servlet,其作用分別是

  1. WeixinMsgEventServlet 完成微信服務器驗證,接收微信推送消息。
  2. WeixinQrCodeServlet 完成帶參數二維碼生成,以及完成登錄輪詢接口
  3. WeixinBindServlet 完成業務信息與用戶openid綁定操作
  4. WeixinWebQrCodeServlet 完成網頁授權登錄的二維碼生成
  5. WeixinRedirectServlet 完成網頁授權接收微信重定向回傳參數

需要調用微信接口信息如下

  // author: herbert 公眾號小院不小 20210424
	private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
	private static final String QRCODE_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={0}";
	private static final String QRCODE_SRC_URL = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={0}";
	private static final String STENDTEMPLATE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={0}";
	private static final String WEB_AUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid={0}&redirect_uri={1}&response_type=code&scope=snsapi_base&state={2}#wechat_redirect";
	private static final String WEB_AUTH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type=authorization_code";

前端對應的三個頁面分別是

  1. login.html 用於展現登錄的二維碼,以及實現輪詢邏輯
  2. index.html 用於登錄成功后,顯示用戶信息
  3. weixinbind.html 用於用戶綁定業務信息

最終實現的效果如下

登錄頁面

已綁定openid直接跳轉到首頁

登錄成功頁面

未綁定用戶,在手機到會收到邀請微信綁定鏈接

微信綁定頁面

4. 帶參數二維碼登錄

生成帶參數的二維碼主要通過以下個步驟來實現

  1. 使用APPID和APPSECRET換取ACCESSTOKEN
  2. 使用ACCESSTOKEN換取對應二維碼的TICKET
  3. 使用TICKET獲取具體的二維圖片返回給前端
4.1 獲取公眾號ACCESSTOKEN

換取ACCESSTOKEN 代碼如下

// author: herbert 公眾號小院不小 20210424
public static String getAccessToken() {
		if (ACCESSTOKEN != null) {
			logger.info("從內存中獲取到AccessToken:{}", ACCESSTOKEN);
			return ACCESSTOKEN;
		}
		String access_token_url = MessageFormat.format(ACCESS_TOKEN_URL, APPID, APPSECRET);
		logger.info("access_token_url轉換后的訪問地址");
		logger.info(access_token_url);
		Request request = new Request.Builder().url(access_token_url).build();
		OkHttpClient httpClient = new OkHttpClient();
		Call call = httpClient.newCall(request);
		try {
			Response response = call.execute();
			String resBody = response.body().string();
			logger.info("獲取到相應正文:{}", resBody);
			JSONObject jo = JSONObject.parseObject(resBody);
			String accessToken = jo.getString("access_token");
			String errCode = jo.getString("errcode");
			if (StringUtils.isBlank(errCode)) {
				errCode = "0";
			}
			if ("0".equals(errCode)) {
				logger.info("獲取accessToken成功,值為:{}", accessToken);
				ACCESSTOKEN = accessToken;
			}

			return accessToken;
		} catch (IOException e) {
			logger.error("獲取accessToken出現錯誤", e.getCause());
		}
		return null;
	}

4.2 獲取二維碼TICKET

根據ACCESSTOKEN獲取二維碼TICKET代碼如下

// author: herbert 公眾號:小院不小 20210424
public static String getQrCodeTiket(String accessToken, String qeCodeType, String qrCodeValue) {
		String qrcode_ticket_url = MessageFormat.format(QRCODE_TICKET_URL, accessToken);
		logger.info("qrcode_ticket_url轉換后的訪問地址");
		logger.info(qrcode_ticket_url);

		JSONObject pd = new JSONObject();
		pd.put("expire_seconds", 604800);
		pd.put("action_name", "QR_STR_SCENE");
		JSONObject sence = new JSONObject();
		sence.put("scene", JSONObject
				.parseObject("{\"scene_str\":\"" + MessageFormat.format("{0}#{1}", qeCodeType, qrCodeValue) + "\"}"));
		pd.put("action_info", sence);
		logger.info("提交內容{}", pd.toJSONString());
		RequestBody body = RequestBody.create(JSON, pd.toJSONString());

		Request request = new Request.Builder().url(qrcode_ticket_url).post(body).build();
		OkHttpClient httpClient = new OkHttpClient();
		Call call = httpClient.newCall(request);
		try {
			Response response = call.execute();
			String resBody = response.body().string();
			logger.info("獲取到相應正文:{}", resBody);
			JSONObject jo = JSONObject.parseObject(resBody);
			String qrTicket = jo.getString("ticket");
			String errCode = jo.getString("errcode");
			if (StringUtils.isBlank(errCode)) {
				errCode = "0";
			}
			if ("0".equals(jo.getString(errCode))) {
				logger.info("獲取QrCodeTicket成功,值為:{}", qrTicket);
			}
			return qrTicket;
		} catch (IOException e) {
			logger.error("獲取QrCodeTicket出現錯誤", e.getCause());
		}
		return null;
	}

4.3 返回二維圖片

獲取二維碼圖片流代碼如下

// author: herbert 公眾號:小院不小 20210424
public static InputStream getQrCodeStream(String qrCodeTicket) {
		String qrcode_src_url = MessageFormat.format(QRCODE_SRC_URL, qrCodeTicket);
		logger.info("qrcode_src_url轉換后的訪問地址");
		logger.info(qrcode_src_url);
		Request request = new Request.Builder().url(qrcode_src_url).get().build();
		OkHttpClient httpClient = new OkHttpClient();
		Call call = httpClient.newCall(request);
		try {
			Response response = call.execute();
			return response.body().byteStream();
		} catch (IOException e) {
			logger.error("獲取qrcode_src_url出現錯誤", e.getCause());
		}
		return null;
	}

最終二維碼圖片通過servlet中的get方法返回到前端,需要注意的地方就是為當前session添加key用於存儲掃碼用戶信息或openid

// author: herbert 公眾號:小院不小 20210424
protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
    accessToken = WeixinUtils.getAccessToken();
		String cacheKey = request.getParameter("key");
		logger.info("當前用戶緩存key:{}", cacheKey);
		WeixinCache.put(cacheKey, "none");
		WeixinCache.put(cacheKey + "_done", false);
		if (qrCodeTicket == null) {
			qrCodeTicket = WeixinUtils.getQrCodeTiket(accessToken, QRCODETYPE, cacheKey);
		}
		InputStream in = WeixinUtils.getQrCodeStream(qrCodeTicket);
		response.setContentType("image/jpeg; charset=utf-8");
		OutputStream os = null;
		os = response.getOutputStream();
		byte[] buffer = new byte[1024];
		int len = 0;
		while ((len = in.read(buffer)) != -1) {
			os.write(buffer, 0, len);
		}
		os.flush();
	}
4.4 前端顯示二維圖片

前端可以使用image標簽,src指向這個servlet地址就可以了

<div class="loginPanel" style="margin-left: 25%;">
    <div class="title">微信登錄(微信場景二維碼)</div>
    <div class="panelContent">
      <div class="wrp_code"><img class="qrcode lightBorder" src="/weixin-server/weixinqrcode?key=herbert_test_key"></div>
      <div class="info">
        <div id="wx_default_tip">
          <p>請使用微信掃描二維碼登錄</p>
          <p>“掃碼登錄測試系統”</p>
        </div>
      </div>
    </div>
  </div>
4.5 前端輪詢掃碼情況

pc端訪問login頁面時,除了顯示對應的二維碼,也需要開啟定時輪詢操作。查詢到掃碼用戶信息就跳轉到index頁面,沒有就間隔2秒繼續查詢。輪詢的代碼如下

// author: herbert 公眾號:小院不小 20210424
  function doPolling() {
      fetch("/weixin-server/weixinqrcode?key=herbert_test_key", { method: 'POST' }).then(resp => resp.json()).then(data => {
        if (data.errcode == 0) {
          console.log("獲取到綁定用戶信息")
          console.log(data.binduser)
          localStorage.setItem("loginuser", JSON.stringify(data.binduser));
          window.location.replace("index.html")
        }
        setTimeout(() => {
          doPolling()
        }, 2000);
      })
    }
    doPolling()

可以看到前端訪問了后台一個POST接口,這個對應的后台代碼如下

// author: herbert 公眾號:小院不小 20210424
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String cacheKey = request.getParameter("key");
		logger.info("登錄輪詢讀取緩存key:{}", cacheKey);
		Boolean cacheDone = (Boolean) WeixinCache.get(cacheKey + "_done");
		response.setContentType("application/json;charset=utf-8");
		String rquestBody = WeixinUtils.InPutstream2String(request.getInputStream(), charSet);
		logger.info("獲取到請求正文");
		logger.info(rquestBody);
		logger.info("是否掃碼成功:{}", cacheDone);
		JSONObject ret = new JSONObject();
		if (cacheDone != null && cacheDone) {
			JSONObject bindUser = (JSONObject) WeixinCache.get(cacheKey);
			ret.put("binduser", bindUser);
			ret.put("errcode", 0);
			ret.put("errmsg", "ok");
			WeixinCache.remove(cacheKey);
			WeixinCache.remove(cacheKey + "_done");
			logger.info("已移除緩存數據,key:{}", cacheKey);
			response.getWriter().write(ret.toJSONString());
			return;
		}
		ret.put("errcode", 99);
		ret.put("errmsg", "用戶還未掃碼");
		response.getWriter().write(ret.toJSONString());
	}

通過以上的操作,完美解決了二維顯示和輪詢功能。但用戶掃描了我們提供二維碼,我們系統怎么知道呢?還記得我們最初配置的URL么,微信會把掃描情況通過POST的方式發送給我們。對應接收的POST代碼如下

// author: herbert 公眾號:小院不小 20210424
protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String rquestBody = WeixinUtils.InPutstream2String(request.getInputStream(), charSet);
		logger.info("獲取到微信推送消息正文");
		logger.info(rquestBody);
		try {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
			dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
			dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
			dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
			dbf.setXIncludeAware(false);
			dbf.setExpandEntityReferences(false);
			DocumentBuilder db = dbf.newDocumentBuilder();
			StringReader sr = new StringReader(rquestBody);
			InputSource is = new InputSource(sr);
			Document document = db.parse(is);
			Element root = document.getDocumentElement();
			NodeList fromUserName = document.getElementsByTagName("FromUserName");
			String openId = fromUserName.item(0).getTextContent();
			logger.info("獲取到掃碼用戶openid:{}", openId);
			NodeList msgType = root.getElementsByTagName("MsgType");
			String msgTypeStr = msgType.item(0).getTextContent();
			if ("event".equals(msgTypeStr)) {
				NodeList event = root.getElementsByTagName("Event");
				String eventStr = event.item(0).getTextContent();
				logger.info("獲取到event類型:{}", eventStr);
				if ("SCAN".equals(eventStr)) {
					NodeList eventKey = root.getElementsByTagName("EventKey");
					String eventKeyStr = eventKey.item(0).getTextContent();
					logger.info("獲取到掃碼場景值:{}", eventKeyStr);

					if (eventKeyStr.indexOf("QRCODE_LOGIN") == 0) {
						String cacheKey = eventKeyStr.split("#")[1];
						scanLogin(openId, cacheKey);
					}
				}
			}
			if ("text".equals(msgTypeStr)) {
				NodeList content = root.getElementsByTagName("Content");
				String contentStr = content.item(0).getTextContent();
				logger.info("用戶發送信息:{}", contentStr);
			}
		} catch (Exception e) {
			logger.error("微信調用服務后台出現錯誤", e.getCause());
		}
	}

我們需要的掃碼數據是 MsgType=="event" and Event=="SCAN" ,找到這條數據,解析出我們在生成二維碼時傳遞的key值,再寫入緩存即可。代碼中的 scanLogin(openId, cacheKey)完成具體業務邏輯,如果用戶已經綁定業務賬號,則直接發送模板消息登錄成功,否則發送模板消息邀請微信綁定,對應的代碼邏輯如下

// author: herbert 公眾號:小院不小 20210424
private void scanLogin(String openId, String cacheKey) throws IOException {
   JSONObject user = findUserByOpenId(openId);
   if (user == null) {
   // 發送消息讓用戶綁定賬號
   logger.info("用戶還未綁定微信,正在發送邀請綁定微信消息");
   WeixinUtils.sendTempalteMsg(WeixinUtils.getAccessToken(), openId,
   		"LWP44mgp0rEGlb0pK6foatU0Q1tWhi5ELiAjsnwEZF4",
   		"http://pro.vaiwan.com/weixin-server/weixinbind.html?key=" + cacheKey, null);
   WeixinCache.put(cacheKey, openId);
   return;
   }
   // 更新緩存
   WeixinCache.put(cacheKey, user);
   WeixinCache.put(cacheKey + "_done", true);
   logger.info("已將緩存標志[key]:{}設置為true", cacheKey + "_done");
   logger.info("已更新緩存[key]:{}", cacheKey);
   logger.info("已發送登錄成功微信消息");
   WeixinUtils.sendTempalteMsg(WeixinUtils.getAccessToken(), openId, "MpiOChWEygaviWsIB9dUJLFGXqsPvAAT2U5LcIZEf_o",
   	null, null);
}

以上就完成了通過場景二維實現微信登錄的邏輯

5. 網頁授權登錄

網頁授權登錄的二維碼需要我們構建好具體的內容,然后使用二維碼代碼庫生成二維碼

5.1 生成網頁授權二維碼
// author: herbert 公眾號:小院不小 20210424
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
	  String cacheKey = request.getParameter("key");
		logger.info("當前用戶緩存key:{}", cacheKey);
		BufferedImage bImg = WeixinUtils.buildWebAuthUrlQrCode("http://pro.vaiwan.com/weixin-server/weixinredirect",
				cacheKey);
		if (bImg != null) {
			response.setContentType("image/png; charset=utf-8");
			OutputStream os = null;
			os = response.getOutputStream();
			ImageIO.write(bImg, "png", os);
			os.flush();
		}
	}

可以看到,我們這里緩存key值,通過state方式傳遞給微信服務器。微信服務器會將該值原樣返回給我我們的跳轉地址,並且附帶上授權碼。我們通過二維碼庫生成二維碼,然后直接返回二維碼圖。前端直接指向這個地址就可顯示圖片了。對應前端代碼如下

  <div class="loginPanel">
    <div class="title">微信登錄(微信網頁授權)</div>
    <div class="panelContent">
      <div class="wrp_code"><img class="qrcode lightBorder" src="/weixin-server/weixinwebqrcode?key=herbert_test_key"></div>
      <div class="info">
        <div id="wx_default_tip">
          <p>請使用微信掃描二維碼登錄</p>
          <p>“掃碼登錄測試系統”</p>
        </div>
      </div>
    </div>
  </div>
5.2 獲取openid並驗證

用戶掃描我們生成的二維碼以后,微信服務器會發送一個GET請求到我們配置的跳轉地址,我們在這里完成openid的驗證和業務系統用戶信息獲取操作,對應代碼如下

// author: herbert 公眾號:小院不小 20210424
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String code = request.getParameter("code");
		String state = request.getParameter("state");
		logger.info("獲取到微信回傳參數code:{},state:{}", code, state);
		JSONObject webTokenInfo = WeixinUtils.getWebAuthTokenInfo(code);
		if (webTokenInfo != null && !webTokenInfo.containsKey("errcode")) {
			String openId = webTokenInfo.getString("openid");
			logger.info("獲取到用opeind", openId);
			JSONObject user = findUserByOpenId(openId);
			if (user == null) {
				//用戶未綁定 將openid存入緩存方便下一步綁定用戶
				WeixinCache.put(state, openId);
				response.sendRedirect("weixinbind.html?key=" + state);
				return;
			}
			WeixinCache.put(state, user);
			WeixinCache.put(state + "_done", true);
			logger.info("已將緩存標志[key]:{}設置為true", state + "_done");
			logger.info("已更新緩存[key]:{}", state);

			response.setCharacterEncoding("GBK");
			response.getWriter().print("掃碼成功,已成功登錄系統");
		}
	}

用戶掃描這個二維碼后,邏輯跟場景二維碼一樣,找到用戶信息就提示用戶已成功登陸系統,否則就跳轉到微信綁定頁面

6. 開發平台登錄

開放平台登錄需要認證過后才能測試,認證需要交錢。對不起,我不配測試。

7. 總結

掃描登錄主要邏輯是生成帶key值二維,然后一直輪詢服務器查詢登錄狀態。以上兩個方式各有優劣,主要區別如下

  1. 帶參數二維碼方式,微信負責生成二維。網頁授權需要我們自己生成二維
  2. 帶參數二維掃碼成功或邀請綁定采用模板消息推送,網頁授權可以直接跳轉,體驗更好
  3. 帶參數二維碼用途更多,比如像ngork.cc網站,實現關注了公眾號才能加隧道功能

這里涉及到的知識點有

  1. Oauth認證流程
  2. 二維碼生成邏輯
  3. 內網穿透原理
  4. Javaservlet開發

開發過程中,需要多查幫助文檔。開發過程中的各種環境配置,對開發者來說,也是不小的挑戰。做微信開發也有好多年,從企業微信,到公眾號,到小程序,到小游戲,一直沒有總結。這次專門做了一個微信掃碼登錄專題。先寫代碼,再寫總結也花費了數周時間。如果覺得好,還望關注公眾號支持下,您的點贊在看是我寫作力量的源泉。對微信集成和企業微信集成方面有問題的,也歡迎在公眾號回復,我看到了會第一時間力所能及的為您解答。需要文中提及的項目,請掃描下方的二維碼,關注公眾號[小院不小],回復wxqrcode獲取.

公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM