前言

各种官方网站通常都会有app、微信公众号等。比方央视网,银行等。

当咱们关注公众号或者app后,这些利用就能够在挪动端不便地将信息推送给用户。

对立各产品线的账号体系,实现一个账号处处应用的指标是十分有必要的。

于是有需要:能够实现微信扫码登陆。

微信对接

通常网站与集体微信的对接有两种形式:

第一种是Oauth2登陆:

用户通过扫描网站的二维码, 受权公开信息,如昵称、头像等。便能够胜利登陆网站。

例如在爱奇艺网站扫描二维码登陆后,受权给网站,之后胜利登陆爱奇艺。

登陆完之后,微信会提示信息。



第二种是关注微信公众号

网站本人弹出一个二维码,扫描二维码后弹出公众号的关注界面、只有一关注公众号网站主动登录、第二次扫描登录的时候网站间接登录。

例如这种,轻易找的网站。
扫码后跳到公众号,关注后主动登陆。


这两种扫码登陆办法,对应微信提供的两种开发方式

一种是基于微信公众平台的扫码登录,另一种是基于微信开放平台的扫码登录。

微信开放平台就是为了让第三方利用投入微信的怀抱而设计的,这第三方利用指的是比方android、ios、网站、零碎等;
援用
微信公众平台就是为了让程序员小伙伴利用微信自家技术(公众号、小程序)开发公众号、小程序而筹备的。

微信开放平台入口:https://open.weixin.qq.com/

微信公众平台入口:https://mp.weixin.qq.com/

两者应用微信扫码登录的区别:

微信开放平台须要开企业认证能力注册。比拟难申请

微信公众平台须要认证微信服务号,能力进行扫码登录的开发。只需申请一个公众号。

上面采纳第二种。

后盾与公众号配置

后盾应用的是spring boot。前台用的angular

这里引入github一个公众号开发工具包
github仓库

后盾pom.xml依赖:

       <dependency>            <groupId>com.github.binarywang</groupId>            <artifactId>weixin-java-mp</artifactId>            <version>4.4.0</version>        </dependency>        <dependency>            <groupId>com.github.binarywang</groupId>            <artifactId>wx-java-mp-spring-boot-starter</artifactId>            <version>4.4.0</version>        </dependency>

1.申请一个公众号。若无,可用测试公众号。

微信公众平台接口测试帐号申请

2.配置服务器信息

URL为开发者服务器的接口地址,微信服务器通过该接口与开发者服务器建设连贯。Token可由开发者能够任意填写,用作生成签名(该Token会和接口URL中蕴含的Token进行比对,从而验证安全性)
若是没有服务器,能够用ngfork进行内网穿透,收费好用。这里用的就是ngfork

3.配置后盾:

# 公众号配置(必填)wx:  mp:    appid: wxaf7fe05a8xxxxxxxxx    secret: 57b48fcec2d5db1axxxxxxxxxxx    token: yunzhi    aesKey: 123
@Servicepublic class WeChatMpService extends WxMpServiceImpl {  @Autowired  private WxMpConfig wxMpConfig;  @PostConstruct  public void init() {    final WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl();    // 设置微信公众号的appid    config.setAppId(this.wxMpConfig.getAppid());    // 设置微信公众号的app corpSecret    config.setSecret(this.wxMpConfig.getAppSecret());    // 设置微信公众号的token    config.setToken(this.wxMpConfig.getToken());    // 设置音讯加解密密钥    config.setAesKey(this.wxMpConfig.getAesKey());    super.setWxMpConfigStorage(config);  }}

4.验证服务器地址的有效性

开发者提交信息后,微信服务器将发送GET申请到方才填写的服务器地址URL上,

通过确认后,须要向微信服务器原样返回echostr参数内容,接入才失效,否则接入失败。

例如,微信服务器未正确收到返回信息,显示配置失败。




GET申请携带参数如下表所示:

参数形容
signature微信加密签名,signature联合了开发者填写的 token 参数和申请中的 timestamp 参数、nonce参数
timestamp工夫戳
nonce随机数
echostr随机字符串

验证签名的次要代码:

用的验证签名的函数是weixin-java-mp包提供的。

@RequestMapping("wechat")@RestControllerpublic class WechatController {   @Autowired  WeChatMpService weChatMpService;  /*   * @param signature 微信加密签名,signature联合了开发者填写的 token 参数和申请中的 timestamp 参数、nonce参数。   * @param timestamp 工夫戳   * @param nonce     这是个随机数   * @param echostr   随机字符串,验证胜利后原样返回   */  @GetMapping  public void get(@RequestParam(required = false) String signature,                  @RequestParam(required = false) String timestamp,                  @RequestParam(required = false) String nonce,                  @RequestParam(required = false) String echostr,                  HttpServletResponse response) throws IOException {    if (!this.weChatMpService.checkSignature(timestamp, nonce, signature)) {      this.logger.warn("接管到了未通过校验的微信音讯,这可能是token配置错了,或是接管了非微信官网的申请");      return;    }    response.setCharacterEncoding("UTF-8");    response.getWriter().write(echostr);    response.getWriter().flush();    response.getWriter().close();  }}

扫码登录的开发

先来看一下大抵流程

1.向微信服务器发送申请,获取access_token。


微信申请access_token文档

access_token是公众号的全局惟一接口调用凭据,公众号调用各接口时都需应用access_token。开发者须要进行妥善保留。access_token的存储至多要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,反复获取将导致上次获取的access_token生效。

url(1)申请示例地址: https://api.weixin.qq.com/cgi...

所以如果间接对微信文档开发的话,须要先申请access_token。

但用了weixin-java-mp包,会帮咱们做这件事。上面跳到下一步

2.申请获取ticket,用于换取登陆二维码。

微信获取二维码文档

获取带参数的二维码的过程包含两步:

  1. 首先创立二维码ticket
  2. 凭借 ticket 到指定 URL 换取二维码。

目前有2种类型的二维码:

  • 长期二维码,有过期工夫的,最长能够设置为在二维码生成后的30天后过期,可能生成较多数量。
  • 永恒二维码,无过期工夫的,数量较少(目前为最多10万个)。

长期二维码足以满足需要。

用包提供的办法获取ticket

String qrUuid = UUID.randomUUID().toString();WxMpQrCodeTicket wxMpQrCodeTicket = this.wxMpService.getQrcodeService().qrCodeCreateTmpTicket(qrUuid, 10 * 60);

每次创立二维码 ticket 须要提供一个开发者自行设定的参数(scene_id), 这里用了随机生成的uuid。

3.用ticket向微信申请二维码。前端显示

申请二维码文档

获取二维码 ticket 后,开发者可用 ticket 换取二维码图片。
HTTP GET申请(请应用 https 协定)https://mp.weixin.qq.com/cgi-...
揭示:TICKET记得进行UrlEncode

ticket正确状况下,http 返回码是200,是一张图片的url地址,能够间接展现或者下载。

应用包提供的办法进行申请获取

String qrCodeUrl = this.wxMpService.getQrcodeService().qrCodePictureUrl(wxMpQrCodeTicket.getTicket());

之后返回给前端,前端用<img>标签解决,作为src的值。

<img class="img-thumbnail" [src]="qrCodeUrl"/>

4.用户扫描二维码,微信进行事件推送

微信用户应用公共号时可能会产生很多事件,例如

  • 关注/勾销关注事件
  • 扫描带参数二维码事件
  • 自定义菜单事件
  • 点击菜单拉取音讯时的事件推送
  • 点击菜单跳转链接时的事件推送

而以后的场景就是用户扫描带参数二维码事件

用户扫描带场景值二维码时,可能推送以下两种事件:

  • 如果用户还未关注公众号,则用户能够关注公众号,关注后微信会将带场景值关注事件推送给开发者。
  • 如果用户曾经关注公众号,则微信会将带场景值扫描事件推送给开发者。

微信推送事件文档

承受微信的事件推送:

 /**   * 当设置完微信公众号的接口后,微信会把用户发送的音讯,扫码事件等推送过去   *   * @param signature 微信加密签名,signature联合了开发者填写的 token 参数和申请中的 timestamp 参数、nonce参数。   * @param encType 加密类型(暂未启用加密音讯)   * @param msgSignature 加密的音讯   * @param timestamp 工夫戳   * @param nonce 随机数   * @throws IOException   */  @PostMapping(produces = "text/xml; charset=UTF-8")  public void api(HttpServletRequest httpServletRequest,                  HttpServletResponse httpServletResponse,                  @RequestParam("signature") String signature,                  @RequestParam(name = "encrypt_type", required = false) String encType,                  @RequestParam(name = "msg_signature", required = false) String msgSignature,                  @RequestParam("timestamp") String timestamp,                  @RequestParam("nonce") String nonce) throws IOException {    if (!this.weChatMpService.checkSignature(timestamp, nonce, signature)) {      this.logger.warn("接管到了未通过校验的微信音讯,这可能是token配置错了,或是接管了非微信官网的申请");      return;    }    BufferedReader bufferedReader = httpServletRequest.getReader();    String str;    StringBuilder requestBodyBuilder = new StringBuilder();    while ((str = bufferedReader.readLine()) != null) {      requestBodyBuilder.append(str);    }    String requestBody = requestBodyBuilder.toString();    this.logger.info("\n接管微信申请:[signature=[{}], encType=[{}], msgSignature=[{}],"                    + " timestamp=[{}], nonce=[{}], requestBody=[\\n{}\\n]",            signature, encType, msgSignature, timestamp, nonce, requestBody);}

5. 后盾进行逻辑解决

微信推送给公众号的音讯类型很多,而公众号也须要针对用户不同的输出做出不同的反馈。

如果应用if ... else ...来实现的话十分难以保护,这时能够依据weixin-java-mp开发文档,应用 WxMpMessageRouter 来对音讯进行路由保护。

先写出几个事件的handler。比方扫码事件和关注事件。 源码在文末

/** * 解决关注事件 * 新用户走关注事件;老用户走扫码事件 * @author Binary Wang */@Componentpublic class SubscribeHandler extends AbstractHandler {  private final WeChatMpService weChatMpService;  private final WechatService wechatService;  private final Logger logger = LoggerFactory.getLogger(this.getClass());  public SubscribeHandler(WeChatMpService weChatMpService , WechatService wechatService) {    super(weChatMpService);    this.weChatMpService = weChatMpService;    this.wechatService = wechatService;  }  @Override  public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,                                  Map<String, Object> context,                                  WxMpService wxMpService,                                  WxSessionManager sessionManager) throws WxErrorException {    this.logger.info("新关注用户 OPENID: " + wxMessage.getFromUser());    if (this.logger.isDebugEnabled()) {      this.logger.info("新关注用户 OPENID: " + wxMessage.getFromUser());    }    WeChatUser weChatUser = this.wechatService.getOneByOpenidAndAppId(wxMessage.getFromUser(), wxMessage.getToUser());    if (wxMessage.getEventKey().startsWith("qrscene_")) {      String key = wxMessage.getEventKey().substring("qrscene_".length());      return this.handleByEventKey(key, weChatUser, wxMessage);    }    return new TextBuilder().build("感激关注,祝您生存欢快!",        wxMessage,        weChatMpService);  }}

对写的几个handler进行路由注册

private WxMpMessageRouter router; private void refreshRouter() {    final WxMpMessageRouter newRouter = new WxMpMessageRouter(this);    // 关注事件     newRouter.rule().async(false).msgType(EVENT).event(SUBSCRIBE).handler(this.subscribeHandler).end();    // 勾销关注事件     newRouter.rule().async(false).msgType(EVENT).event(UNSUBSCRIBE).handler(this.unsubscribeHandler).end();    // 扫码事件 newRouter.rule().async(false).msgType(EVENT).event(SCAN).handler(this.scanHandler).end();    // 默认    newRouter.rule().async(false).handler(this.msgHandler).end();    this.router = newRouter;  }/**   * 微信事件通过这个入口进来   * 依据不同事件,调用不同handler   */ public WxMpXmlOutMessage route(WxMpXmlMessage message) {    try {      return this.router.route(message);    } catch (Exception e) {      this.logger.error(e.getMessage(), e);    }    return null;  }

当初曾经配置好了,微信推送的扫码事件或者关注事件曾经被咱们定义的handler获取到了。

那么有下一个问题:如果有多个用户同时在应用微信登录,咱们怎么要推送给哪个客户端,让扫码胜利的用户胜利登陆呢?

这时就须要在返回用户登陆二维码前,

  • 记录该客户端的seesionId
  • 同时增加一个key为uuid,value为自定义handler到hashmap中。

这里的uuid为,后面获取ticket随机生成的uuid。

这样就能够在扫码事件handler中, 调用这个自定义的handler,实现向指定客户端发送信息。



在返回二维码前,定义自定义handler

 @Override  public String getLoginQrCode(String wsLoginToken, HttpSession httpSession) {    try {      if (this.logger.isDebugEnabled()) {        this.logger.info("1. 生成用于回调的uuid,请将推送给微信,微信当推送带有UUID的二维码,用户扫码后微信则会把带有uuid的信息回推过来");      }      String qrUuid = UUID.randomUUID().toString();      WxMpQrCodeTicket wxMpQrCodeTicket = this.wxMpService.getQrcodeService().qrCodeCreateTmpTicket(qrUuid, 10 * 60);      this.wxMpService.addHandler(qrUuid, new WeChatMpEventKeyHandler() {        long beginTime = System.currentTimeMillis();        private Logger logger = LoggerFactory.getLogger(this.getClass());        @Override        public boolean getExpired() {          return System.currentTimeMillis() - beginTime > 10 * 60 * 1000;        }        /**         * 扫码后调用该办法         * @param wxMpXmlMessage 扫码音讯         * @param weChatUser 扫码用户         * @return 输入音讯         */        @Override        public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, WeChatUser weChatUser) {          if (this.logger.isDebugEnabled()) {            this.logger.info("2. 用户扫描后触发该办法, 发送扫码胜利的同时,将wsUuid与微信用户绑定在一起,用前面应用wsU");          }          String openid = wxMpXmlMessage.getFromUser();          if (openid == null) {            this.logger.error("openid is null");          }          if (weChatUser.getUser() != null) {            String uuid = UUID.randomUUID().toString();            bindWsUuidToWeChatUser(uuid, weChatUser);            simpMessagingTemplate.convertAndSendToUser(wsLoginToken,                "/stomp/scanLoginQrCode",                uuid);            return new TextBuilder().build(String.format("登录胜利,登录的用户为: %s", weChatUser.getUser().getName()),                wxMpXmlMessage,                null);          } else {            simpMessagingTemplate.convertAndSendToUser(wsLoginToken,                "/stomp/scanLoginQrCode",                false);            return new TextBuilder().build(String.format("登录准则,起因:您尚未绑定微信用户"),                wxMpXmlMessage,                null);          }        }      });      return this.wxMpService.getQrcodeService().qrCodePictureUrl(wxMpQrCodeTicket.getTicket());    } catch (Exception e) {      this.logger.error("获取长期公众号图片时产生谬误:" + e.getMessage());    }    return "";  }

利用websocket向前端发送信息

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连贯上进行全双工通信的协定。
WebSocket 使得客户端和服务器之间的数据交换变得更加简略,容许服务端被动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只须要实现一次握手,两者之间就间接能够创立持久性的连贯,并进行双向数据传输。
 simpMessagingTemplate.convertAndSendToUser(wsLoginToken,                "/stomp/scanLoginQrCode",                uuid);

之后前端就能够利用这个身份信息loginUid登陆了。

前台向后盾发送登陆申请

/**   * 微信扫码登录   */  onWeChatLogin() {    this.userService.getLoginQrCode()      .subscribe(src => {        this.qrCodeSrc = src;        this.loginModel = 'wechat';        this.userService.onScanLoginQrCode$.pipe(first()).subscribe(data => {          const uuid = data.body;          this.login({username: uuid, password: uuid});        });      });  }

后盾再判断loginUid是否在map中,是则登陆胜利。

/**   * 校验微信扫码登录后的认证ID是否无效   * @param wsAuthUuid websocket认证ID   */  @Override  public boolean checkWeChatLoginUuidIsValid(String wsAuthUuid) {    return this.map.containsKey(wsAuthUuid);  }