微信掃碼登陸

微信掃碼登陸

1. 使用背景

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

  1. 經過微信公衆平臺生成帶參數的二維
  2. 經過微信公衆平臺微信網頁受權登陸
  3. 經過微信開發平臺微信登陸功能

2. 開發環境搭建

2.1 內網穿透

微信全部的接口訪問,都要求使用域名。但多數開發者是沒有域名,給不少開發者測試帶來了麻煩。不過有如下兩種方案能夠嘗試:html

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

那釘釘的內網穿透工具具體怎麼使用用的呢?前端

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

ding -config=ding.cfg -subdomain=pro 8080
複製代碼

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

釘釘內網穿透 通過我測試,這個至關穩定,而且能夠指定靜態子域名。簡直就是業界良心github

2.2 公衆號測試環境

訪問公衆平臺測試帳號系統,能夠經過微信登陸,可快速獲得一個測試帳號。而後咱們須要如下兩個配置web

  • 接口配置信息

接口配置信息

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

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

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

// 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獲取.

相關文章
相關標籤/搜索