关于微信开发:微信扫码登录

9次阅读

共计 14797 个字符,预计需要花费 37 分钟才能阅读完成。

微信扫码登录

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 接口平安域名

这个配置主要用途是解决 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.3 获取二维码 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.4 返回二维图片

获取二维码图片流代码如下

// 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.5 前端显示二维图片

前端能够应用 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.6 前端轮询扫码状况

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 获取.

正文完
 0