Implementation of wechat code scanning login

1, Preparatory work

https://open.weixin.qq.com
1. Register
2. Mailbox activation
3. Improve Developer Information
4. Developer qualification certification
Prepare business license, approve within 1-2 working days, RMB 300
5. Create web app
Submit for review and approval within 7 working days
6. Familiar with wechat login process
Reference documents: https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=e547653f995d8f402704d5cb2945177dc8aa4e7e&lang=zh_CN
Get access_token sequence diagram

Note: individual developers cannot apply. They must have a business license. If they can't use it, consider using the wechat public platform. I use someone else's appid and key below.

2, Wechat login backend development

2.1 add configuration

application.properties configuration file:

# Wechat open platform appid
wx.open.app_id=Yours appid
# Wechat open platform appsecret
wx.open.app_secret=Yours appsecret
# Wechat open platform redirection url
wx.open.redirect_url=http://Your server name / api/ucenter/wx/callback

2.2 create constant class and constant propertiesutil.java constant class

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * Read the relevant configuration of wechat login
 */
@Component
public class ConstantWxUtils implements InitializingBean {
    @Value("${wx.open.app_id}")
    private String appId;

    @Value("${wx.open.app_secret}")
    private String appSecret;

    @Value("${wx.open.redirect_url}")
    private String redirectUrl;

    public static String WX_OPEN_APP_ID;
    public static String WX_OPEN_APP_SECRET;
    public static String WX_OPEN_REDIRECT_URL;

    @Override
    public void afterPropertiesSet() throws Exception {
        WX_OPEN_APP_ID = appId;
        WX_OPEN_APP_SECRET = appSecret;
        WX_OPEN_REDIRECT_URL = redirectUrl;
    }
}

2.3 create controller

WxApiController

import com.atguigu.commonutils.JwtUtils;
import com.atguigu.educenter.entity.UcenterMember;
import com.atguigu.educenter.service.UcenterMemberService;
import com.atguigu.educenter.utils.ConstantWxUtils;
import com.atguigu.educenter.utils.HttpClientUtils;
import com.atguigu.servicebase.exceptionhandler.GuliException;
import com.google.gson.Gson;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;

@Controller //Only the request address does not need to return data, and RestController is not required
@RequestMapping("/api/ucenter/wx")
@CrossOrigin
public class WxApiController {

    //1. Generate QR code scanned by wechat
    @GetMapping("login")
    public String getWxCode(){
        //Fixed address, back splicing parameters
        // Wechat open platform authorization baseurl% s is equivalent to? placeholder 
        String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
                "?appid=%s" +
                "&redirect_uri=%s" +
                "&response_type=code" +
                "&scope=snsapi_login" +
                "&state=%s" +
                "#wechat_redirect";
        //Yes, redirect_url encoded by URLEncoder
        String redirect_url=ConstantWxUtils.WX_OPEN_REDIRECT_URL;
        try {
            redirect_url = URLEncoder.encode(redirect_url, "utf-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        //Set the value in% s
        String url = String.format(baseUrl,
                ConstantWxUtils.WX_OPEN_APP_ID,
                redirect_url,
                "atguigu"
        );
        //Redirect to the requested wechat address
        return "redirect:"+url;
    }
}

Authorization url parameter description

parameterIs it necessaryexplain
appidyesApply unique identification
redirect_uriyesPlease use urlEncode to process the link
response_typeyesFill in code
scopeyesThe application authorization scope has multiple scopes separated by commas (,), and the web application only fills in snsapi at present_ login
stateyesIt is used to maintain the status of request and callback. After authorization request, it is brought back to the third party as is. This parameter can be used to prevent csrf attack (Cross Site Request Forgery Attack). It is recommended that the third party take this parameter, which can be set as a simple random number plus session for verification

2.4 testing

visit: http://localhost:8160/api/ucenter/wx/login

The interface after the mobile phone scans the QR code is as follows

It will also be displayed on the web page

3, Get the information of wechat scanner

3.1 test whether callback is available

The callback url has been set in the login controller, as shown in the following figure

After scanning, execute the local callback method, obtain two values in the callback, and pass them in the jump.
state: pass as is
Code: similar to mobile phone verification code, random unique value (temporary bill)

  //2. Obtain scanner information and add data
    @GetMapping("callback")
    public String callback(String code,String state){
		//Authorized temporary note code
    System.out.println("code = " + code);
    System.out.println("state = " + state);
}

3.2 adding dependencies

The reason why the version is not specified here is that I have specified it in the parent project. I develop a micro service project according to your own situation.

 <!--httpclient-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>
<!--commons-io-->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
</dependency>
<!--gson-->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>

3.3 add httpclient tool class

httpclient can achieve the same effect as browser access mainly through java code. We mainly use this to call the interface officially provided by wechat.

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 *  The dependent jar packages are: commons-lang-2.6.jar, httpclient-4.3.2.jar, httpcore-4.3.1.jar, commons-io-2.4.jar
 * @author zhaoyb
 *
 */
public class HttpClientUtils {

	public static final int connTimeout=10000;
	public static final int readTimeout=10000;
	public static final String charset="UTF-8";
	private static HttpClient client = null;

	static {
		PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
		cm.setMaxTotal(128);
		cm.setDefaultMaxPerRoute(128);
		client = HttpClients.custom().setConnectionManager(cm).build();
	}

	public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{
		return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
	}

	public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{
		return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
	}

	public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
			SocketTimeoutException, Exception {
		return postForm(url, params, null, connTimeout, readTimeout);
	}

	public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
			SocketTimeoutException, Exception {
		return postForm(url, params, null, connTimeout, readTimeout);
	}

	public static String get(String url) throws Exception {
		return get(url, charset, null, null);
	}

	public static String get(String url, String charset) throws Exception {
		return get(url, charset, connTimeout, readTimeout);
	}

	/**
	 * Send a Post request using the specified character set encoding
	 *
	 * @param url
	 * @param body RequestBody
	 * @param mimeType For example, application / XML "application / x-www-form-urlencoded" a = 1 & B = 2 & C = 3
	 * @param charset code
	 * @param connTimeout Link establishment timeout, Ms
	 * @param readTimeout Response timeout, Ms
	 * @return ResponseBody, Use the specified character set encoding
	 * @throws ConnectTimeoutException Link establishment timeout exception
	 * @throws SocketTimeoutException  Response timeout
	 * @throws Exception
	 */
	public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
			throws ConnectTimeoutException, SocketTimeoutException, Exception {
		HttpClient client = null;
		HttpPost post = new HttpPost(url);
		String result = "";
		try {
			if (StringUtils.isNotBlank(body)) {
				HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
				post.setEntity(entity);
			}
			// Set parameters
			Builder customReqConf = RequestConfig.custom();
			if (connTimeout != null) {
				customReqConf.setConnectTimeout(connTimeout);
			}
			if (readTimeout != null) {
				customReqConf.setSocketTimeout(readTimeout);
			}
			post.setConfig(customReqConf.build());

			HttpResponse res;
			if (url.startsWith("https")) {
				// Execute HTTP requests
				client = createSSLInsecureClient();
				res = client.execute(post);
			} else {
				// Execute Http request
				client = HttpClientUtils.client;
				res = client.execute(post);
			}
			result = IOUtils.toString(res.getEntity().getContent(), charset);
		} finally {
			post.releaseConnection();
			if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {
				((CloseableHttpClient) client).close();
			}
		}
		return result;
	}


	/**
	 * Submit form
	 *
	 * @param url
	 * @param params
	 * @param connTimeout
	 * @param readTimeout
	 * @return
	 * @throws ConnectTimeoutException
	 * @throws SocketTimeoutException
	 * @throws Exception
	 */
	public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
			SocketTimeoutException, Exception {

		HttpClient client = null;
		HttpPost post = new HttpPost(url);
		try {
			if (params != null && !params.isEmpty()) {
				List<NameValuePair> formParams = new ArrayList<NameValuePair>();
				Set<Entry<String, String>> entrySet = params.entrySet();
				for (Entry<String, String> entry : entrySet) {
					formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
				}
				UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
				post.setEntity(entity);
			}

			if (headers != null && !headers.isEmpty()) {
				for (Entry<String, String> entry : headers.entrySet()) {
					post.addHeader(entry.getKey(), entry.getValue());
				}
			}
			// Set parameters
			Builder customReqConf = RequestConfig.custom();
			if (connTimeout != null) {
				customReqConf.setConnectTimeout(connTimeout);
			}
			if (readTimeout != null) {
				customReqConf.setSocketTimeout(readTimeout);
			}
			post.setConfig(customReqConf.build());
			HttpResponse res = null;
			if (url.startsWith("https")) {
				// Execute HTTP requests
				client = createSSLInsecureClient();
				res = client.execute(post);
			} else {
				// Execute Http request
				client = HttpClientUtils.client;
				res = client.execute(post);
			}
			return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
		} finally {
			post.releaseConnection();
			if (url.startsWith("https") && client != null
					&& client instanceof CloseableHttpClient) {
				((CloseableHttpClient) client).close();
			}
		}
	}




	/**
	 * Send a GET request
	 *
	 * @param url
	 * @param charset
	 * @param connTimeout  Link establishment timeout, Ms
	 * @param readTimeout  Response timeout, Ms
	 * @return
	 * @throws ConnectTimeoutException   Link establishment timeout
	 * @throws SocketTimeoutException   Response timeout
	 * @throws Exception
	 */
	public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
			throws ConnectTimeoutException,SocketTimeoutException, Exception {

		HttpClient client = null;
		HttpGet get = new HttpGet(url);
		String result = "";
		try {
			// Set parameters
			Builder customReqConf = RequestConfig.custom();
			if (connTimeout != null) {
				customReqConf.setConnectTimeout(connTimeout);
			}
			if (readTimeout != null) {
				customReqConf.setSocketTimeout(readTimeout);
			}
			get.setConfig(customReqConf.build());

			HttpResponse res = null;

			if (url.startsWith("https")) {
				// Execute HTTP requests
				client = createSSLInsecureClient();
				res = client.execute(get);
			} else {
				// Execute Http request
				client = HttpClientUtils.client;
				res = client.execute(get);
			}

			result = IOUtils.toString(res.getEntity().getContent(), charset);
		} finally {
			get.releaseConnection();
			if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
				((CloseableHttpClient) client).close();
			}
		}
		return result;
	}


	/**
	 * Get charset from response
	 *
	 * @param ressponse
	 * @return
	 */
	@SuppressWarnings("unused")
	private static String getCharsetFromResponse(HttpResponse ressponse) {
		// Content-Type:text/html; charset=GBK
		if (ressponse.getEntity() != null  && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
			String contentType = ressponse.getEntity().getContentType().getValue();
			if (contentType.contains("charset=")) {
				return contentType.substring(contentType.indexOf("charset=") + 8);
			}
		}
		return null;
	}



	/**
	 * Create SSL connection
	 * @return
	 * @throws GeneralSecurityException
	 */
	private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
		try {
			SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
				public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {
					return true;
				}
			}).build();

			SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {

				@Override
				public boolean verify(String arg0, SSLSession arg1) {
					return true;
				}

				@Override
				public void verify(String host, SSLSocket ssl)
						throws IOException {
				}

				@Override
				public void verify(String host, X509Certificate cert)
						throws SSLException {
				}

				@Override
				public void verify(String host, String[] cns,
								   String[] subjectAlts) throws SSLException {
				}

			});

			return HttpClients.custom().setSSLSocketFactory(sslsf).build();

		} catch (GeneralSecurityException e) {
			throw e;
		}
	}

	public static void main(String[] args) {
		try {
			String str= post("https://localhost:443/ssl/test.shtml","name=12&page=34","application/x-www-form-urlencoded", "UTF-8", 10000, 10000);
			//String str= get("https://localhost:443/ssl/test.shtml?name=12&page=34","GBK");
            /*Map<String,String> map = new HashMap<String,String>();
            map.put("name", "111");
            map.put("page", "222");
            String str= postForm("https://localhost:443/ssl/test.shtml",map,null, 10000, 10000);*/
			System.out.println(str);
		} catch (ConnectTimeoutException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (SocketTimeoutException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

}

3.4 create callback controller method

1. The general idea of callback is to take the code value obtained by scanning login above and request the address provided by wechat https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_ type=authorization_ Code gets two values access_token and openid
For this, please refer to the wechat official website document https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html
2. Then take the access obtained above_ Token and openid, and then request the address provided by wechat https://api.weixin.qq.com/sns/userinfo?access_token =% S & openid =% s,% s is my placeholder. You can modify it according to your own situation. Here you can get the information of wechat scanner, such as wechat nickname, wechat avatar, openid, etc.
Add the following method in WxApiController.java

 @Autowired
    private UcenterMemberService memberService;

    //2. Obtain scanner information and add data
    @GetMapping("callback")
    public String callback(String code,String state){
        try {
            //1. Get code value, temporary bill, similar to verification code
            //2. Take the code to request the fixed address of wechat and get two values access_token and openid
            String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
                    "?appid=%s" +
                    "&secret=%s" +
                    "&code=%s" +
                    "&grant_type=authorization_code";
            //Splice three parameters: id key and code value
            String accessTokenUrl = String.format(baseAccessTokenUrl,
                    ConstantWxUtils.WX_OPEN_APP_ID,
                    ConstantWxUtils.WX_OPEN_APP_SECRET,
                    code
            );
            //Request the spliced address and get two values access_token and openid
            //Send the request using httpclient and get the returned result
            String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);
//            System.out.println("accessTokenInfo:"+accessTokenInfo);
            //Get access from the accessTokenInfo string_ Token and openid
            //Convert the accessTokenInfo string into a map set, and obtain the corresponding value according to the key in the map
            //Use the json conversion tool Gson
            Gson gson=new Gson();
            HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
            String access_token = (String) mapAccessToken.get("access_token");
            String openid = (String) mapAccessToken.get("openid");

            //Add the code scanner information to the database
            //Judge whether the same wechat information exists in the database according to openid
            UcenterMember member = memberService.getOpenIdMember(openid);
            if(member ==null){  //If member is empty, it means that there is no same wechat data in the table to add

                //3. Take the access you get_ Token and openid, and then request the fixed address provided by wechat to obtain the information of the code scanner
                //Access the wechat resource server to obtain user information
                String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
                        "?access_token=%s" +
                        "&openid=%s";
                String userInfoUrl= String.format(
                        baseUserInfoUrl,
                        access_token,
                        openid
                );
                //Send request
                String userInfo = HttpClientUtils.get(userInfoUrl);
//            System.out.println("userInfo:"+userInfo);
                //Get and return the user information in userInfo (code scanner information)
                HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class);
                String nickname = (String) userInfoMap.get("nickname"); //nickname
                String headimgurl = (String) userInfoMap.get("headimgurl"); //head portrait

                member=new UcenterMember();
                member.setOpenid(openid);
                member.setNickname(nickname);
                member.setAvatar(headimgurl);
                memberService.save(member); //add to
            }
            //Using jwt to generate token string from member object
            String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
            //Finally, return to the home page and pass the token string through the path
            return "redirect:http://localhost:3000?token="+jwtToken;
        } catch (Exception e) {
            throw new GuliException(20001,"Login failed");
        }
    }

   the above also makes a judgment. If it is the first login, the information will be added to the database.

3.5 front end display scanner information

   in fact, the work of the back-end developers has been basically completed here, but if we want to display it on the front end, we'd better use jwt according to the wechat information to generate the token string and pass the token string to the home page through the path.
The code in the previous step already contains this step.

  //Using jwt to generate token string from member object
            String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
            //Finally, return to the home page and pass the token string through the path
            return "redirect:http://localhost:3000?token="+jwtToken;

3.6 front end display effect after login

   I won't release the front-end code. You can achieve the desired effect according to your own needs. After I log in, I log in to the home page through callback, and display the information (avatar, nickname, etc.) in the upper right.

Tags: Spring Cloud

Posted on Tue, 16 Nov 2021 11:33:05 -0500 by kevingarnett2000