Wechat scan code login

1. Use context

Developing business systems today is not a separate system. They often need to call each other with multiple different systems, and sometimes they even need to connect with platforms such as wechat, Dingding and Feishu. At present, some of the business systems I have developed have been connected to the wechat public platform. As a summary of knowledge, next, we discuss a small part of the function of wechat public platform, wechat scan code login. The key point is to get the OpenID. I have carefully searched the development documents provided by wechat, which can be achieved in the following three ways.

  1. Generate two dimensions with parameters through wechat public platform
  2. Authorized login through wechat public platform wechat webpage
  3. Through wechat development platform wechat login function

2. Development environment construction

2.1 Intranet Penetration

Wechat all interface access, are required to use the domain name. However, most developers do not have domain names, causing problems for many developer testing. But here are two options to try:

  1. Using a company domain name, ask the company administrator to configure a subdomain name pointing to port 80 of an IP address on your company’s public network. Then use Nginx or the NAT command to locate the changed domain name to your development environment
  2. Using Intranet penetration tools, there are many free tunnels available in the market. However, it is unstable and does not support fixed subdomain names or access has been restricted by wechat. After I collect a large number of data, found nail nail development platform to provide Intranet penetration tools, quite good. It’s shameful for wechat to use Alibaba’s stuff to connect with wechat’s stuff. If you don’t provide convenience for developers, let rivals do it.

Then how does Nail nail’s Intranet penetration tool work?

First, use Git to download the internal network penetration tool. After downloading, find the windows_64 directory and create a new start.bat file with the content as

ding -config=ding.cfg -subdomain=pro 8080
Copy the code

In the command, -subdomain is used to generate subdomain name 8080, indicating that local port 8080 is insinuated. Double-click the start.bat file

In my tests, this is fairly stable and can specify static subdomains. It’s the conscience of the industry

2.2 Test environment of public account

Access the public platform test account system, you can log in through wechat, you can quickly get a test account. Then we need the following two configurations

  • Interface Configuration

When we click the submit button, the wechat server will verify whether the URL we configured is valid. This URL serves two purposes

  1. Verify that the address is valid by signing it
  2. Receive messages pushed by wechat, such as notifications after users scan codes

The signature generation logic uses the configured token combined with the timestamp and nonce sent back by wechat to form a new string through string array sorting, perform SHA signature, and then convert the signed binary array into hexadecimal string. The final content is the specific signature information. The corresponding Java code is as follows

// author: Herbert public id: 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("Failed to obtain signature information", e.getCause());
		}
		return "";
	}
Copy the code

The corresponding GET request code is as follows

// author: Herbert public id: 20210424
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		logger.info("Wechat passing authentication parameters in configuration server");
		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("Server generates signature information :{}", buildSign);
		if (buildSign.equals(signature)) {
			response.getWriter().write(echostr);
			logger.info("The signature generated by the service is equal to the signature generated by the wechat server, verification successful");
			return; }}Copy the code

The verification rule of wechat server is to return echostr as is. If it is troublesome to sign, it is also ok to return echostr directly.

  • JS Interface security domain name

The main purpose of this configuration is to solve the H5 and wechat JSSDK integration. Wechat must request the specified domain name in order to call JSSDK

3. Test project construction

To test the effect of scanning login, we need to build a simple Maven project. The specific files and directories in the project are as follows

The user scans the TWO-DIMENSIONAL code to obtain the corresponding OpenID, and then searches the corresponding user according to the OpenID in the userdata.json file. When found, the user information is written to the cache. If not, remind the user to bind the business account information. The front-end uses periodic polling to search for user information from the service cache

The contents of the userdata.json file are as follows

[{
	"userName": "Zhang"."password":"1234"."userId": "000001"."note": "Scan login"."openId": ""
}]
Copy the code

As you can see from the code, the back end provides five servlets whose roles are

  1. WeixinMsgEventServlet completes the verification of wechat server and receives wechat push messages.
  2. WeixinQrCodeServlet completes the generation of two-dimensional code with parameters, and completes the login polling interface
  3. WeixinBindServlet binds service information to user OpenID
  4. WeixinWebQrCodeServlet The TWO-DIMENSIONAL code for web page login is generated
  5. WeixinRedirectServlet completes the parameters of web page authorization receiving wechat redirection

The wechat interface information to be invoked is as follows

  // author: Herbert public number 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&sta te={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";
Copy the code

The three corresponding pages of the front end are respectively

  1. Login. HTML is used to display the TWO-DIMENSIONAL code of login and implement polling logic
  2. Index. HTML is used to display user information after successful login
  3. Weixinbind. HTML is used to bind service information

The final result is as follows

The home page is displayed if the OpenID is bound

Unbound users will receive the wechat binding link on their mobile phones

4. Login with qr code

The generation of two-dimensional code with parameters is mainly achieved through the following three steps

  1. Use APPID and APPSECRET for ACCESSTOKEN
  2. Use ACCESSTOKEN to exchange the TICKET with the corresponding QR code
  3. Use TICKET to get a specific 2d image and return it to the front end
4.1 Obtaining the public Account ACCESSTOKEN

Exchange ACCESSTOKEN code below

// author: Herbert public number 20210424
public static String getAccessToken(a) {
		if(ACCESSTOKEN ! =null) {
			logger.info("Get AccessToken from memory :{}", ACCESSTOKEN);
			return ACCESSTOKEN;
		}
		String access_token_url = MessageFormat.format(ACCESS_TOKEN_URL, APPID, APPSECRET);
		logger.info("Access_token_url Converted access address");
		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("Get corresponding text :{}", 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 succeeded with value: {}", accessToken);
				ACCESSTOKEN = accessToken;
			}

			return accessToken;
		} catch (IOException e) {
			logger.error("Error obtaining accessToken", e.getCause());
		}
		return null;
	}

Copy the code
4.2 Obtaining a QR Code TICKET

Obtain the TICKET code of the QR code based on ACCESSTOKEN

// author: Herbert public id: 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 converted access address");
		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("Submit content {}", 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("Get corresponding text :{}", 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 obtained successfully, value: {}", qrTicket);
			}
			return qrTicket;
		} catch (IOException e) {
			logger.error(Error obtaining QrCodeTicket, e.getCause());
		}
		return null;
	}

Copy the code
4.3 Return to the two-dimensional picture

To obtain the QR code picture stream code is as follows

// author: Herbert public id: 20210424
public static InputStream getQrCodeStream(String qrCodeTicket) {
		String qrcode_src_url = MessageFormat.format(QRCODE_SRC_URL, qrCodeTicket);
		logger.info("Qrcode_src_url converted access address");
		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("Error obtaining qrcode_src_URL", e.getCause());
		}
		return null;
	}
Copy the code

Finally, the TWO-DIMENSIONAL code picture is returned to the front end through the GET method in the servlet. What needs to be noted is to add a key for the current session to store the scan user information or OpenID

// author: Herbert public id: 20210424
protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
    accessToken = WeixinUtils.getAccessToken();
		String cacheKey = request.getParameter("key");
		logger.info("Current user cache 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();
	}
Copy the code
4.4 Display two-dimensional pictures in front

The front-end can use the image tag and SRC to point to the servlet address

<div class="loginPanel" style="margin-left: 25%;">
    <div class="title">Wechat Login (QR code in wechat scenario)</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>Please use wechat to scan the QR code to log in</p>
          <p>Scan code to Log in to test System</p>
        </div>
      </div>
    </div>
  </div>
Copy the code
4.5 Front-end polling and code scanning

When you access the LOGIN page from a PC, you need to enable periodic polling in addition to displaying the qr code. If the scan user information is found, the index page will be displayed. If the scan user information is not found, the query will continue at an interval of 2 seconds. The code for the poll is as follows

// author: Herbert public id: 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("Get bind user information")
          console.log(data.binduser)
          localStorage.setItem("loginuser".JSON.stringify(data.binduser));
          window.location.replace("index.html")}setTimeout(() = > {
          doPolling()
        }, 2000);
      })
    }
    doPolling()
Copy the code

You can see that the front end accesses a POST interface in the background. The corresponding background code is as follows

// author: Herbert public id: 20210424
	protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String cacheKey = request.getParameter("key");
		logger.info("Login polling read cache 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("Get request body");
		logger.info(rquestBody);
		logger.info("Scan successful :{}", 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("Cached data removed, key: {}", cacheKey);
			response.getWriter().write(ret.toJSONString());
			return;
		}
		ret.put("errcode".99);
		ret.put("errmsg"."User has not scanned the code yet");
		response.getWriter().write(ret.toJSONString());
	}

Copy the code

Through the above operation, the perfect solution to the two-dimensional display and polling function. But the user has scanned the QR code provided by us, how does our system know? Do you still remember the URL we configured initially? Wechat will send us the scanning information through POST. The received POST code is as follows

// author: Herbert public id: 20210424
protected void doPost(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		String rquestBody = WeixinUtils.InPutstream2String(request.getInputStream(), charSet);
		logger.info("Got the wechat push message body");
		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("Get event type {}", eventStr);
				if ("SCAN".equals(eventStr)) {
					NodeList eventKey = root.getElementsByTagName("EventKey");
					String eventKeyStr = eventKey.item(0).getTextContent();
					logger.info("Get scan scene value :{}", 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("User sends message :{}", contentStr); }}catch (Exception e) {
			logger.error("There is an error in the background of wechat service invocation", e.getCause()); }}Copy the code

The SCAN data we need is MsgType==”event” and event ==”SCAN”, find this data, parse out the key value we passed when generating the TWO-DIMENSIONAL code, and then write it into the cache. ScanLogin (openId, cacheKey) completes the service logic. If a user has been bound to a service account, a template message is sent to the user. Otherwise, a template message is sent to invite wechat to bind

// author: Herbert public id: 20210424
private void scanLogin(String openId, String cacheKey) throws IOException {
   JSONObject user = findUserByOpenId(openId);
   if (user == null) {
   // Send a message for the user to bind the account
   logger.info("The user has not been bound to wechat, and is sending an invitation to bind wechat message.");
   WeixinUtils.sendTempalteMsg(WeixinUtils.getAccessToken(), openId,
   		"LWP44mgp0rEGlb0pK6foatU0Q1tWhi5ELiAjsnwEZF4"."http://pro.vaiwan.com/weixin-server/weixinbind.html?key=" + cacheKey, null);
   WeixinCache.put(cacheKey, openId);
   return;
   }
   // Update the cache
   WeixinCache.put(cacheKey, user);
   WeixinCache.put(cacheKey + "_done".true);
   logger.info("Cache flag [key]:{} has been set to true", cacheKey + "_done");
   logger.info("Updated cache [key]:{}", cacheKey);
   logger.info("Wechat message of successful login has been sent");
   WeixinUtils.sendTempalteMsg(WeixinUtils.getAccessToken(), openId, "MpiOChWEygaviWsIB9dUJLFGXqsPvAAT2U5LcIZEf_o".null.null);
}
Copy the code

The above completes the logic of realizing wechat login through scene 2D

5. Authorize web login

The two-dimensional code of webpage authorization login needs us to build the specific content, and then use the two-dimensional code base to generate two-dimensional code

5.1 Generate the QR code for Web page authorization
// author: Herbert public id: 20210424
	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
	  String cacheKey = request.getParameter("key");
		logger.info("Current user cache 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(); }}Copy the code

As you can see, we cache the key value here and pass it to the wechat server in state mode. The wechat server will return the value to my redirect address as is, and attach the authorization code. We generate the TWO-DIMENSIONAL code through the two-dimensional code library, and then directly return the two-dimensional code map. The front end points directly at this address to display the image. The corresponding front-end code is as follows

  <div class="loginPanel">
    <div class="title">Wechat Login (wechat webpage authorization)</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>Please use wechat to scan the QR code to log in</p>
          <p>Scan code to Log in to test System</p>
        </div>
      </div>
    </div>
  </div>
Copy the code
5.2 Obtaining and Verifying the OpenID

After the user scans the QR code generated by us, the wechat server will send a GET request to the jump address configured by us. We complete the verification of OpenID and the operation of obtaining user information of the business system here, and the corresponding code is as follows

// author: Herbert public id: 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("Get it with Opeind.", openId);
			JSONObject user = findUserByOpenId(openId);
			if (user == null) {
				// The user is not bound. Store the openID in the cache for binding the user
				WeixinCache.put(state, openId);
				response.sendRedirect("weixinbind.html? key=" + state);
				return;
			}
			WeixinCache.put(state, user);
			WeixinCache.put(state + "_done".true);
			logger.info("Cache flag [key]:{} has been set to true", state + "_done");
			logger.info("Updated cache [key]:{}", state);

			response.setCharacterEncoding("GBK");
			response.getWriter().print("Code scan successful, login successfully."); }}Copy the code

After the user scans this QR code, the logic is the same as the scene QR code. If the user information is found, the user will be reminded that he has successfully logged in to the system. Otherwise, the user will jump to the wechat binding page

6. Log in to the development platform

Open platform login requires authentication before testing, and authentication requires payment. I’m sorry. I don’t deserve the test.

7. To summarize

The main logic of scanning login is to generate two dimensions with key values, and then poll the server to query the login status. Each of the above two methods has its advantages and disadvantages, and the main differences are as follows

  1. With parameters of the TWO-DIMENSIONAL code, wechat is responsible for generating two-dimensional. Web licensing requires us to generate our own two-dimensional
  2. If 2d scan code with parameters is successful or invitation binding adopts template message push, webpage authorization can be directly jumped, providing better experience
  3. Two-dimensional code with parameters more uses, such as ngork.cc website, to achieve attention to the public number to add tunnel function

The knowledge points involved here are

  1. Oauth certification process
  2. Two-dimensional code generation logic
  3. Intranet Penetration Principle
  4. Javaservlet development

During development, you need to consult the help documentation. The configuration of various environments in the development process is also not a small challenge for developers. Do wechat development for many years, from the enterprise wechat, to the public number, to small programs, to small games, there has been no summary. This time, I have made a wechat scanning code login topic. It took weeks to write the code and then the summary. If you feel good, also hope to pay attention to the support of the public account, your praise and look is the source of my writing power. If you have questions about wechat integration and enterprise wechat integration, you are welcome to reply on the public account. I will answer them as soon as I can when I see them. For the items mentioned in this article, please scan the qrcode below, follow the public account [Xiaoyuan No Small], reply to wxQrCode to obtain.