实现支持多公众号的微信公众号扫码登录服务

Posted 福禄网络研发团队

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实现支持多公众号的微信公众号扫码登录服务相关的知识,希望对你有一定的参考价值。

实现支持多公众号的微信公众号扫码登录服务

最近,在公司的通行证项目开发过程中,需求方提出了支持微信公众号扫码登录,并且可以支持多公众号接入的需求。研究了一下微信公众号的开发文档,实现微信公众号扫码登录并不难,但是要支持多公众号接入就得好好斟酌一下了。

理清思路,微信公众号扫码登录的实现关键就是appid、openid获取,appid用来识别公众号,openid用来识别用户,能理解这两点需求就应该不难实现了。

流程

我们先整理一下流程,用户在前端页面点击扫描登录,后端服务接收到前端页面请求之后调用微信官方api创建二维码,并将二维码的ticket和url返回给前端页面,前端页面展示二维码,然后用户用手机微信扫描二维码,微信官方后台监听到扫描事件,将事件推送给后端服务,后端服务缓存ticket和openid,前端页面轮询后端服务判断缓存中是否存在ticket对应的openid,有则表示扫描成功,如果openid没有绑定用户,则跳转至绑定页面,否则直接跳转到登录成功页面。

实战

我们主要有两个开发步骤:

  • 生成二维码
  • 扫码登录

1、生成二维码

参考微信官方文档生成带参数的二维码的说明。

  • 创建二维码ticket
    每次创建二维码ticket需要提供一个开发者自行设定的参数(scene_id),分别介绍临时二维码和永久二维码的创建二维码ticket过程。临时二维码请求说明
  • 临时二维码请求说明
    http请求方式: POST URL: https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN POST数据格式:json POST数据例子:{"expire_seconds": 604800, "action_name": "QR_SCENE", "action_info": {"scene": {"scene_id": 123}}} 或者也可以使用以下POST数据创建字符串形式的二维码参数:{"expire_seconds": 604800, "action_name": "QR_STR_SCENE", "action_info": {"scene": {"scene_str": "test"}}}
    废话不多说,直接撸代码。先获取AccessToken,再创建二维码。ticket是新生成的二维码的唯一标识,可以用来判断二维码是否被扫描。另外,缓存returnUrl用于登录成功后重定向。
        /// <summary>
        /// 生成二维码
        /// </summary>
        /// <param name="returnUrl"></param>
        /// <param name="appid"></param>
        /// <param name="secret"></param>
        /// <returns></returns>
        [HttpGet("/api/mpwechat/qrcode"), AllowAnonymous]
        public async Task<IActionResult> QrCodeAsync(string returnUrl, string appid, string secret)
        {
            var accessToken = await GetAccessTokenAsync(appid, secret);
            var jsonContent = await CreateQrCodeAsync(accessToken);

            var ticket = jsonContent["ticket"].Value<string>();
            // 缓存returnUrl
            var returnUrlCacheKey = MpwechatLoginReturnUrlCacheKey(ticket);
            if (!(await _cache.ExistsAsync(returnUrlCacheKey)))
                await _cache.AddAsync(returnUrlCacheKey, returnUrl, TimeSpan.FromMinutes(30));

            return Ok(ResponseResult.Execute(new { Url = $"https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={UrlEncoder.Default.Encode(ticket)}", Ticket = ticket }));
        }

        /// <summary>
        /// 获取AccessToken
        /// </summary>
        /// <param name="appid"></param>
        /// <param name="secret"></param>
        /// <returns></returns>
        private async Task<string> GetAccessTokenAsync(string appid, string secret)
        {
            // 从缓存获取AccessToken
            var cacheKey = $"mpwechat:{appid}";
            if ((await _cache.ExistsAsync(cacheKey)))
                return (await _cache.GetAsync(cacheKey)).ToString();

            var response = await _httpClient.GetAsync($"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}");
            response.EnsureSuccessStatusCode();
            var content = await response.Content.ReadAsStringAsync();
            var jsonContent = JObject.Parse(content);
            var accessToken = jsonContent["access_token"].Value<string>();
            var expiresIn = jsonContent["expires_in"].Value<int>();
            
            // 缓存AccessToken
            await _cache.AddAsync($"mpwechat:{appid}", accessToken, TimeSpan.FromSeconds(expiresIn - 60));
            return accessToken;
        }
        
         /// <summary>
        /// 创建二维码
        /// </summary>
        /// <param name="accessToken"></param>
        /// <param name="sceneStr"></param>
        /// <returns></returns>
        private async Task<JObject> CreateQrCodeAsync(string accessToken, string sceneStr = null)
        {
            var stringContent = new StringContent(JsonConvert.SerializeObject(
                new
                {
                    expire_seconds = 600, 
                    action_name = "QR_STR_SCENE",
                    action_info = new
                    {
                        scene = new
                        {
                            scene_str = sceneStr
                        }
                    }
                }), Encoding.UTF8, "application/json");
            var response = await _httpClient.PostAsync($"https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={accessToken}", stringContent);
            response.EnsureSuccessStatusCode();
            var content = await response.Content.ReadAsStringAsync();
            return JObject.Parse(content);
        }

        private string MpwechatLoginReturnUrlCacheKey(string ticket) => $"mpwechat_login_returnUrl:{ticket}";


2、扫码登录

微信公众号的扫码登录实现方式与微信的扫码登录实现方式不同,它是采用订阅通知的方式实现的。参考微信官方文档事件推送的说明。

  • 首先我们要准备两个RestApi,路由地址相同,一个Get方法,一个Post方法。Get方法用于微信官方检测服务器配置,Post方法用于接收事件推送。为了识别通知是从哪个微信公众号发送的,我们将Url定义为api/mpwechat/{appid},用动态路由接收appid。敲黑板,这里是关键。另外,如有事件需要其他处理(如自动回复),可转发事件到EventBus,其他应用可自行订阅EventBus的消息作处理。
         /// <summary>
        /// 验证微信公众号签名(微信公众号调用)
        /// </summary>
        [HttpGet("/api/mpwechat/{appid}"), AllowAnonymous]
        public Task<string> CheckMpwechatSignature(string appid, string signature, string timestamp, string nonce, string echostr)
        {
            if (!CheckMpwechatSignature(signature, timestamp, nonce))
                throw new Exception("签名验证不通过");
                
            return Task.FromResult(echostr);
        }

        /// <summary>
        /// 订阅微信公众号事件(微信公众号调用)
        /// </summary>
        [HttpPost("/api/mpwechat/{appid}"), AllowAnonymous]
        public async Task<IActionResult> SubscribeMpwechatEvent(string appid, string signature, string timestamp, string nonce)
        {
            if (!CheckMpwechatSignature(signature, timestamp, nonce))
                throw new Exception("签名验证不通过");

            using StreamReader sr = new(Request.Body, Encoding.UTF8);
            var data = await sr.ReadToEndAsync();
            var xmlDoc = new XmlDocument();
            xmlDoc.LoadXml(data);

            // 如果是密文则需要解密
            var encryptNode = xmlDoc.DocumentElement.SelectSingleNode("Encrypt");
            if (encryptNode != null)
            {
                var encrypt = encryptNode.InnerText;
                data = DecryptMpwechatMsg(appid, data, signature, timestamp, nonce);
                if (data == null)
                    throw new Exception("密文解密异常");

                xmlDoc.LoadXml(data);
            }
            // todo 推送消息到EventBus,如有事件需要其他处理(如自动回复),可订阅EventBus的消息。

            // 扫码登录
            var openId = xmlDoc.DocumentElement.SelectSingleNode("FromUserName").InnerText;
            var eventType = xmlDoc.DocumentElement.SelectSingleNode("Event").InnerText;
            var ticketNode = xmlDoc.DocumentElement.SelectSingleNode("Ticket");

            if (ticketNode != null)
            {
                var ticket = ticketNode.InnerText;
                if (eventType == "subscribe" || eventType == "SCAN")
                {
                    // 缓存openid,标记扫码登录
                    var cacheKey = MpwechatLoginOpenIdCacheKey(ticket);
                    if (!(await _cache.ExistsAsync(cacheKey)))
                        await _cache.AddAsync(cacheKey, openId, TimeSpan.FromMinutes(10));
                }
            }

            return Ok();
        }

        /// <summary>
        /// 轮询检查扫码状态(前端调用)
        /// </summary>
        /// <param name="ticket"></param>
        /// <returns></returns>
        [HttpGet("/api/mpwechat/checkscan"), AllowAnonymous]
        public async Task<IActionResult> MpwechatCheckscanAsync(string ticket)
        {
            var openIdCacheKey = MpwechatLoginOpenIdCacheKey(ticket);
            if ((await _cache.ExistsAsync(openIdCacheKey)))
            {
                var openId = (await _cache.GetAsync(openIdCacheKey)).ToString();
                
                var returnUrlCacheKey = MpwechatLoginReturnUrlCacheKey(ticket);
                var returnUrl = (await _cache.GetAsync(returnUrlCacheKey)).ToString();

                return Ok(ResponseResult.Execute(new { ReturnUrl = returnUrl, OpenId = openId }));
            }
            return Ok(ResponseResult.Execute("-1", "未扫码"));
        }

        /// <summary>
        /// 微信公众号基本设置中设置的Token
        /// </summary>
        private const string Token = "Token";

        /// <summary>
        /// 微信公众号基本设置中设置的EncodingAESKey
        /// </summary>
        private const string EncodingAESKey = "zJULaJfu8NVIXvmKVMYfvdM2inlh4YrKkO3BvCmDOt8";

        /// <summary>
        /// 验证微信公众号签名
        /// </summary>
        /// <param name="signature"></param>
        /// <param name="timestamp"></param>
        /// <param name="nonce"></param>
        /// <returns></returns>
        private bool CheckMpwechatSignature(string signature, string timestamp, string nonce)
        {
             // 拼接排序Sha1加密
            var orderJoinString = string.Join("", new string[] { Token, timestamp, nonce }.OrderBy(t => t));
            return signature == Encrypt.Sha1(orderJoinString);
        }

        /// <summary>
        /// 解密微信公众号内容
        /// </summary>
        /// <param name="appId"></param>
        /// <param name="data"></param>
        /// <param name="signature"></param>
        /// <param name="timestamp"></param>
        /// <param name="nonce"></param>
        /// <returns></returns>
        private string DecryptMpwechatMsg(string appId, string data, string signature, string timestamp, string nonce)
        {
             // 利用微信官方示例代码
            Tencent.WXBizMsgCrypt wxcpt = new(Token, EncodingAESKey, appId);
            var content = "";
            var ret = wxcpt.DecryptMsg(signature, timestamp, nonce, data, ref content);
            if (ret == 0)
                return content;
            return null;
        }
        
        private string MpwechatLoginOpenIdCacheKey(string ticket) => $"mpwechat_login_openId:{ticket}";
  • 在微信公众号的基本配置里面配置Url、Token、EncodingAESKey和消息加密方式。Token用来验证微信公众号签名,EncodingAESKey用来解密消息内容,配置必须与代码一致。

这样后端代码就完成了,前端代码请各位看官自行脑补!:)测试一下,完美通过!

最后

总体来说微信的官方文档和示例还是不错的,按照它一步步来很容易实现扫码登录功能。另外,由于时间仓促,写得不太细致,但是核心的思想和代码都在上面,希望可以给大家带来帮助!

福禄·研发中心 福小皮

微信公众号扫码登录—— 获取微信公众号二维码

引言

这几天在研究微信登录,今天解决了获取微信二维码问题;在这里总结一下

关于微信登录想说的话

第一次接触微信登录,开始就弄混了登录方式;简单来说,微信扫码登录分为两种,一种是微信公众平台,一种是微信开放平台,两者的开发文档也不一样,开始就是一直用的微信公众号的参数却使用的是微信开放平台提供的接口,找了半天问题;


总结一下:

微信公众号(公众平台) 和 微信开放平台 是两码事。

  • 微信公众平台是扫码通过微信公众号授权登录的,个人用户可以申请订阅号,但是没有一些接口调用权限,企业用户可以申请服务号,有许多接口权限;但是个人用户可以通过测试号获取一些权限进行学习测试;
  • 微信开放平台是微信为了接入更多第三方应用而开放的接口,对于web应用,可以申请web应用,从而获取权限,但是只能是企业用户才能申请;(个人学习很不方便)

两者开发文档是不同的,所以看网上教程一定要看清楚是公众平台还是开放平台,不要跟错教程了;最好就是看官方文档,这样就能避免踩坑;(我开始就是看不下去文档,因为微信是php示例代码,所以一直看网上教程,就被各种各样的教程弄晕了;最后还是乖乖看文档去了)


下面就进入正题

准备环境

首先申请了一个公众号,然后从开发者工具中进入测试号:

然后就是这个界面:

appID和appsecret都是操作所需参数;

接口配置下面介绍;

接口配置

接口配置后面会用到,所以需要先配置一下,其实看官方文档就能看懂,但是示例代码是php,所以这里我来演示一下我的操作;

官方文档:传送门

官方文档意思就是你得有一个域名,代码还得跑在该域名下的服务器上,然后你自己需要再代码中实现一个接口获取微信发来的信息进行处理;

如果你和我一样是个学生,开发都在本地127.0.0.1,或者没有域名,那该怎么办?因为微信不能直接调用本地ip,这就需要用到内网穿透;

简单来说就是微信想要向你填入的url发送请求数据,但是它不能直接向本地127.0.0.1发送,我们可以通过内网穿透获取一个域名,让该域名映射到本地127.0.0.1,然后微信向该域名发送数据,这样就把数据发送到了本地;(个人理解)


内网穿透

我通过ngrok进行的内网穿透,就花2块钱实名了一下,然后有一个免费的隧道可以使用:

然后进行配置,它就会給你分一个域名:

接下来下载ngrok客户端,启动隧道:

点击.bat文件,按要求输入隧道id:

链接成功,这时时就意味着访问本地127.0.0.1和访问生成的域名的效果是一样的;


访问流程就是:

文档:

微信向自己填的url发送请求;

实现接入代码

然后就是代码配置,就是官网文档的第二步,官方检验是一个php代码,下面是java代码:

controller接口:

/**
 *  接入微信接口
 */
@GetMapping("/callback")
@ResponseBody
public String checkSign (HttpServletRequest request) throws Exception 
    log.info("===========>checkSign");
    // 获取微信请求参数
    String signature = request.getParameter ("signature");
    String timestamp = request.getParameter ("timestamp");
    String nonce = request.getParameter ("nonce");
    String echostr = request.getParameter ("echostr");
    log.info("开始校验此次消息是否来自微信服务器,param->signature:,\\ntimestamp:,\\nnonce:,\\nechostr:",
            signature, timestamp, nonce, echostr);
    if (CheckWXTokenUtils.checkSignature(signature, timestamp, nonce)) 
        return echostr;
    
    return "";

校验工具类(直接cv)

import lombok.extern.log4j.Log4j2;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

/**
 * 和微信建立链接参数校验
 */
@Log4j2
public class CheckWXTokenUtils 
    private static final String TOKEN = "xxxxxx"; // 自定义的token

    /**
     * 校验微信服务器Token签名
     *
     * @param signature 微信加密签名
     * @param timestamp 时间戳
     * @param nonce     随机数
     * @return boolean
     */
    public static boolean checkSignature(String signature, String timestamp, String nonce) 
        String[] arr = TOKEN, timestamp, nonce;
        Arrays.sort(arr);
        StringBuilder stringBuilder = new StringBuilder();
        for (String param : arr) 
            stringBuilder.append(param);
        
        String hexString = SHA1(stringBuilder.toString());
        return signature.equals(hexString);
    

    private static String SHA1(String str) 
        MessageDigest md;
        try 
            md = MessageDigest.getInstance("SHA-1");
            byte[] digest = md.digest(str.getBytes());
            return toHexString(digest);
         catch (NoSuchAlgorithmException e) 
            log.info("校验令牌Token出现错误:", e.getMessage());
        
        return "";
    

    /**
     * 字节数组转化为十六进制
     *
     * @param digest 字节数组
     * @return String
     */
    private static String toHexString(byte[] digest) 
        StringBuilder hexString = new StringBuilder();
        for (byte b : digest) 
            String shaHex = Integer.toHexString(b & 0xff);
            if (shaHex.length() < 2) 
                hexString.append(0);
            
            hexString.append(shaHex);
        
        return hexString.toString();
    

几个校验参数官方文档也说了,自己对比着看就行了;

再次放入官方文档地址:接入概述


配置就是:

url是内网穿透域名+自己实现的接口

token也是自己写的;

然后在网页服务中:

点击修改:(还是内网穿透域名)

然后就配置好了;

下面就是正式开始二维码生成了;

生成二维码

先放出文档:

生成带参数的二维码

获取Access token

获取ticket

我们先看第一个文档,大致意思就是先发送一个获取二维码ticket的post请求,获取ticket:

整理一下:

url: post
https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN

参数:
param参数:access_token
json参数(两个必要的,其他可以自己看文档加): "expire_seconds": 604800, "action_name": "QR_SCENE"

结果:
"ticket":"gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm
3sUw==","expire_seconds":60,"url":"http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI"

那么就很简单了,就是调用就接口获取ticket,但是可以发现所需要的参数中的access_token我们并没有,所以调用该接口前需要先获取,同样也有官方文档,上面已经放出来了;


获取access_token

同样整理一下:

url: get
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

参数:
grant_type、appid、secret

结果:
"access_token":"ACCESS_TOKEN","expires_in":7200

其中grant_type就写client_credential就行了,固定的;

appid和secret就是测试号的:

所以很轻易就可以获取到access_token;

获取到access_token就可以获取ticket,到这里这两步就完成了;

接下来就是获取二维码了:

获取二维码

这就不过多介绍了,就加一个ticket参数即可;

测试流程

下面我使用postman测试一遍流程:

1,首先获取access_token:

https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=&secret=

2,然后获取ticket:

https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=ACCESS_TOKEN

3,最后获取二维码:

https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET

直接浏览器访问该链接,即可看到该二维码

整个流程就走通了;

下面就是代码实现该流程;

代码实现

这里思考一个问题,后端内部如何自己发送请求获取响应?

平时都是前端向后端发送请求,后端响应;

因为我们需要先发送请求获取access_token,再获取ticket,最后响应给前端的就是一个二维码url,所以后端需要自己发请求并获取响应结果;

这里可以使用httpclient,具体细节可以查资料,这里不过多介绍;

下面是实现代码:

httpclient依赖:

<dependency>
	<groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

httpclient工具类(直接cv):

import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;
 
import javax.net.ssl.SSLContext;
import java.io.File;
import java.io.FileInputStream;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
 
/**
 * HttpClient工具类
 */
public class HttpClientUtils 
 
    private static final CloseableHttpClient httpClient;
 
    // 采用静态代码块,初始化超时时间配置,再根据配置生成默认httpClient对象
    static 
        RequestConfig config = RequestConfig.custom().setConnectTimeout(30000).setSocketTimeout(15000).build();
        httpClient = HttpClientBuilder.create().setDefaultRequestConfig(config).build();
    
 
    /**
     * 发送 HTTP GET请求,不带请求参数和请求头
     * @param url 请求地址
     * @return
     * @throws Exception
     */
    public static String doGet(String url) throws Exception 
        HttpGet httpGet = new HttpGet(url);
        return doHttp(httpGet);
    
 
    /**
     * 发送 HTTP GET,请求带参数,不带请求头
     * @param url 请求地址
     * @param params 请求参数
     * @return
     * @throws Exception
     */
    public static String doGet(String url, Map<String, Object> params) throws Exception 
        // 转换请求参数
        List<NameValuePair> pairs = covertParamsToList(params);
        // 装载请求地址和参数
        URIBuilder ub = new URIBuilder();
        ub.setPath(url);
        ub.setParameters(pairs);
        HttpGet httpGet = new HttpGet(ub.build());
        return doHttp(httpGet);
    
 
    /**
     * 发送 HTTP GET请求,带请求参数和请求头
     * @param url 请求地址
     * @param headers 请求头
     * @param params 请求参数
     * @return
     * @throws Exception
     */
    public static String doGet(String url, Map<String, Object> headers, Map<String, Object> params) throws Exception 
        // 转换请求参数
        List<NameValuePair> pairs = covertParamsToList(params);
        // 装载请求地址和参数
        URIBuilder ub = new URIBuilder();
        ub.setPath(url);
        ub.setParameters(pairs);
 
        HttpGet httpGet = new HttpGet(ub.build());
        // 设置请求头
        for (Map.Entry<String, Object> param : headers.entrySet()) 
            httpGet.addHeader(param.getKey(), String.valueOf(param.getValue()));
        
        return doHttp(httpGet);
    
 
    /**
     * 发送 HTTP POST请求,不带请求参数和请求头
     *
     * @param url 请求地址
     * @return
     * @throws Exception
     */
    public static String doPost(String url) throws Exception 
        HttpPost httpPost = new HttpPost(url);
        return doHttp(httpPost);
    
 
    /**
     * 发送 HTTP POST请求,带请求参数,不带请求头
     *
     * @param url    请求地址
     * @param params 请求参数
     * @return
     * @throws Exception
     */
    public static String doPost(String url, Map<String, Object> params) throws Exception 
        // 转换请求参数
        List<NameValuePair> pairs = covertParamsToList(params);
        HttpPost httpPost = new HttpPost(url);
        // 设置请求参数
        httpPost.setEntity(new UrlEncodedFormEntity(pairs, StandardCharsets.UTF_8.name()));
 
        return doHttp(httpPost);
    
 
    /**
     * 发送 HTTP POST请求,带请求参数和请求头
     *
     * @param url     地址
     * @param headers 请求头
     * @param params  参数
     * @return
     * @throws Exception
     */
    public static String doPost(String url, Map<String, Object> headers, Map<String, Object> params) throws Exception 
        // 转换请求参数
        List<NameValuePair> pairs = covertParamsToList(params);
        HttpPost httpPost = new HttpPost(url);
        // 设置请求参数
        httpPost.setEntity(new UrlEncodedFormEntity(pairs, StandardCharsets.UTF_8.name()));
        // 设置请求头
        for (Map.Entry<String, Object> param : headers.entrySet()) 
            httpPost.addHeader(param.getKey(), String.valueOf(param.getValue()));
        
        return doHttp(httpPost);
    
 
    /**
     * 发送 HTTP POST请求,请求参数是JSON格式,数据编码是UTF-8
     *
     * @param url 请求地址
     * @param param 请求参数
     * @return
     * @throws Exception
     */
    public static String doPostJson(String url, String param) throws Exception 
        HttpPost httpPost = new HttpPost(url);
        // 设置请求头
        httpPost.addHeader("Content-Type", "application/json; charset=UTF-8");
        // 设置请求参数
        httpPost.setEntity(new StringEntity(param, StandardCharsets.UTF_8.name()));
        return doHttp(httpPost);
    
 
    /**
     * 发送 HTTP POST请求,请求参数是XML格式,数据编码是UTF-8
     *
     * @param url 请求地址
     * @param param 请求参数
     * @return
     * @throws Exception
     */
    public static String doPostXml(String url, String param) throws Exception 
        HttpPost httpPost = new HttpPost(url);
        // 设置请求头
        httpPost.addHeader("Content-Type", "application/xml; charset=UTF-8");
        // 设置请求参数
        httpPost.setEntity(new StringEntity(param, StandardCharsets.UTF_8.name()));
 
        return doHttp(httpPost);
    
 
    /**
     * 发送 HTTPS POST请求,使用指定的证书文件及密码,不带请求头信息<
     *
     * @param url 请求地址
     * @param param 请求参数
     * @param path 证书全路径
     * @param password 证书密码
     * @return
     * @throws Exception
     * @throws Exception
     */
    public static String doHttpsPost(String url, String param, String path, String password) throws Exception 
        HttpPost httpPost = new HttpPost(url);
        // 设置请求参数
        httpPost.setEntity(new StringEntity(param, StandardCharsets.UTF_8.name()));
 
        return doHttps(httpPost, path, password);
    
 
    /**
     * 发送 HTTPS POST请求,使用指定的证书文件及密码,请求头为“application/xml;charset=UTF-8”
     *
     * @param url 请求地址
     * @param param 请求参数
     * @param path 证书全路径
     * @param password 证书密码
     * @return
     * @throws Exception
     * @throws Exception
     */
    public static String doHttpsPostXml(String url, String param, String path, String password)<

以上是关于实现支持多公众号的微信公众号扫码登录服务的主要内容,如果未能解决你的问题,请参考以下文章

Swoole 的微信扫码登录

基于 Swoole 的微信扫码登录

从零玩转第三方登录之WeChat公众号扫码关注登陆 -wechatgzh

PHP开发微信公众号的问题

微信公众号开发

如何区分统计员工给公司的微信公众号拉多少人