微信授权—网页+APP


目录
  • 前言
    • 官方文档
  • H5网页授权
  • APP授权
  • 小程序授权
  • JS-SDK说明文档
  • 准备工作
    • 测试:
      • 申请测试账号
      • 账号验证(接口配置)
    • 正式:
      • 申请账号
      • 账号认证
      • 接口配置
  • 流程讲解
    • 总体流程
    • 网页授权
      • 功能
      • 注意
      • 流程
    • APP授权
    • 小程序授权
    • JS-SDK接口调用
      • 功能
      • 注意
      • 流程
      • 生成JS-SDK权限验证的签名
  • 功能实现
    • 属性配置
    • 授权
      • 获取 code (拉起授权页面)
      • 获取access_token
      • 通过openID + access_token获取用户信息
      • access_token过期刷新
    • JS-SDK生成signature
  • 错误处理
    • invalid signature签名错误。建议按如下顺序检查:

前言

微信网页登录授权、APP登录授权、JS-SDK接口调用

温馨提示:实践某项功能请至少读完准备工作,可以避免很多的坑

官方文档

H5网页授权

https://developers.weixin.qq.com

APP授权

https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/Development_Guide.html

小程序授权

[https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/authorize.html)

JS-SDK说明文档

https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.htmlhttps://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html)

准备工作

  • 测试:

    • natapp 内网穿透工具

      开发阶段可用此工具获取域名

    • 申请测试账号

      测试账号拥有微信几乎所有接口的能力,可以先通过测试账号提前验证

    • 获取测试号信息

      image-20211023123859614

    • 账号验证(接口配置)

      填写接口配置信息,此信息需要你有自己的服务器资源,填写的URL需要正确响应微信发送的Token验证,请阅读消息接口使用指南。

      字段 说明
      URL 开发者用来接收微信消息和事件的接口URL(可以外网访问自己服务器IP:端口号也可以)
      TOKEN 自己随意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)

      验证接口实现:

      @ApiOperation("微信公众号认证入口")
      @GetMapping(value = "/wechat/api/wxServerValdation")
      public String wxServerValdation(String signature, String timestamp, String nonce, String echostr){
          if (Objects.isNull(signature)|| Objects.isNull(timestamp) || Objects.isNull(nonce) || Objects.isNull(echostr)){
              return "fail";
          }
          ArrayList list= new ArrayList<>();
          list.add(nonce);
          list.add(timestamp);
          //这是第5步中你设置的Token
          list.add(WxMpConfig.token);
          Collections.sort(list);
          String sha1Singnature = DigestUtils.sha1Hex(list.get(0)+list.get(1)+list.get(2));
          if (sha1Singnature.equals(signature)){
              return echostr;
          }else {
              return "fail";
          }
      }
      

      此时的URL可以设置为 :http://IP:端口号/wechat/api/wxServerValdation

      也可以填写为域名+/wechat/api/wxServerValdation

      验证字段说明

      参数 描述
      signature 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
      timestamp 时间戳
      nonce 随机数
      echostr 随机字符串

      开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:

      1)将token、timestamp、nonce三个参数进行字典序排序

      2)将三个参数字符串拼接成一个字符串进行sha1加密

      3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信

      验证工具:

      http://mp.weixin.qq.com/debug/

    • JS接口安全域名

      设置JS接口安全域后,通过关注该测试号,开发者即可在该域名下调用微信开放的JS接口,请阅读微信JSSDK开发文档。

      请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;

    • 网页授权获取用户基本信息

      微信根据此域名检查回调地址域名

      请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;

  • 正式:

    • 申请账号
      • 正式的微信账号分为订阅号、服务号、企业号,认证都需要300元,但是使用授权和接口功能必须认证。

      • 但是订阅号不能直接获取到用户的OpenID,所以尽量申请一个服务号。(这点微信也是不会提醒用户的,但是当你看了一下测试账号是订阅号,就直接申请了一个订阅号结果就不言而喻了,接口处就会提醒用户,只有服务号才能使用授权接口。)

      • 不过订阅号也是有一个漏洞的,如果你是在公众号内使用,是可以迂回获取到用户信OpenID的。可以参考下面的文章:https://blog.csdn.net/vbirdbest/article/details/51217478

    • 账号认证
    • 接口配置

      接口配置主要有一下三点,其中IP白名单要加入所有访问微信接口服务器的IP

      • 基本配置

      • 服务器配置

      • 功能设置

流程讲解

  • 总体流程

    接入微信公众平台开发,开发者需要按照如下步骤完成:

    1、填写服务器配置

    2、验证服务器地址的有效性

    3、依据接口文档实现业务逻辑

  • 网页授权

    • 功能

      网页授权获取用户基本信息:通过该接口,可以获取用户的基本信息(获取用户的OpenID是无需用户同意的,获取用户的基本信息则需用户同意)

    • 注意
      • 关于网页授权access_token和普通access_token的区别

        1、微信网页授权是通过OAuth2.0机制实现的,在用户授权给公众号后,公众号可以获取到一个网页授权特有的接口调用凭证(网页授权access_token),通过网页授权access_token可以进行授权后接口调用,如获取用户基本信息;

        2、其他微信接口,需要通过基础支持中的“获取access_token”接口来获取到的普通access_token调用。

    • 流程

      网页授权流程分为四步:

      1、引导用户进入授权页面同意授权,获取code(拉起授权页面)

      2、通过code换取网页授权access_token(与基础支持中的access_token不同)

      3、如果需要,开发者可以刷新网页授权access_token,避免过期

      4、通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)

      5 附:检验授权凭证(access_token)是否有效

  • APP授权

    • 第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据 code 参数;
    • 通过 code 参数加上 AppID 和 AppSecret 等,通过 API 换取 access_token;
    • 通过 access_token 进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
  • 小程序授权

    • 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。

    • 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key

    • 之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份

  • JS-SDK接口调用

    • 功能

      微信JS-SDK:是开发者在网页上通过JavaScript代码使用微信原生功能的工具包,开发者可以使用它在网页上录制和播放微信语音、监听微信分享、上传手机本地图片、拍照等许多能力。

    • 注意

      这块可能会出现前端调用失败的问题,或者分享的时候微信不能分享的问题,这个很有可能时版本的问题,请先倒回之前的版本,再进行测试。

      1. 签名用的noncestr和timestamp必须与wx.config中的nonceStr和timestamp相同。
      2. 签名用的url必须是调用JS接口页面的完整URL。
    • 流程
      • 获取Access token

        官方接口测试工具

        https://mp.weixin.qq.com/debug/cgi-bin/apiinfo?t=index&type=基础支持&form=获取access_token接口 /token

      • 获得jsapi_ticket

        (有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi

      • 生成JS-SDK权限验证的签名

        https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx6fb2b539e0663d3b&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_re

        获取code 需要先设置REDIRECT_URI 回调路径

        如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。

        code说明 : code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。

        • 获取access_token

        获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

        参数 是否必须 说明
        appid 公众号的唯一标识
        secret 公众号的appsecret
        code 填写第一步获取的code参数
        grant_type 填写为authorization_code
        • 通过openID + access_token获取用户信息

        需要用缓存存储access_token

        https://api.weixin.qq.com/sns/userinfo?access_token=" + ACCESS_TOKEN + "&openid=" + OPEN_ID + "&lang=zh_CN";
        
        • access_token过期刷新

        检查token是否失效

      @ApiOperation("网页拉起微信授权页面,返回微信校验信息")
          @GetMapping("/auth")
          public R callBack(@ApiParam(value = "*code",required = true)String code) throws IOException {
              /**
               * 获取到授权标志code(用户同意微信授权之后产生的code)
               */
              log.info("进入微信回调code={}",code);
              String url = WxAuthUtil.H5_ACCESS_TOKEN_URL.
                      replace(WxAuthUtil.APP_ID,WxMpConfig.appId).
                      replace(WxAuthUtil.SECRET, WxMpConfig.secret).
                      replace(WxAuthUtil.CODE,code);
              JSONObject jsonObject = WxAuthUtil.doGet(url);
              log.info("授权结果{}",jsonObject);
              return R.ok(jsonObject.getString("openid"));
          }
          
           @ApiOperation("拉取微信用户信息,每天获取token的次数受限,需要将accessToken缓存(目前不需要获取用户信息)")
          @GetMapping("/userInfo")
          public R getWeiChatUserInfo(@RequestParam("code")String openid,
                                                @RequestParam("accessToken")String accessToken,
                                                @RequestParam("refreshToken")String refreshToken) throws IOException {
              /**
               * 校验access_token是否失效
               */
              String checkoutUrl = WxAuthUtil.CHECK_H5_URL.
                      replace(WxAuthUtil.ACCESS_TOKEN, accessToken).
                      replace(WxAuthUtil.OPEN_ID,openid);
      
              JSONObject checkoutInfo = WxAuthUtil.doGet(checkoutUrl);
              log.info("校验信息-----{}",checkoutInfo.toString());
              if (!"0".equals(checkoutInfo.getString("errcode"))) {
                  // 刷新access_token
                  String refreshTokenUrl = WxAuthUtil.REFRESH_TOKEN_URL
                          .replace(WxAuthUtil.APP_ID,WxMpConfig.appId)
                          .replace(WxAuthUtil.REFRESH_TOKEN, refreshToken);
                  JSONObject refreshInfo = WxAuthUtil.doGet(refreshTokenUrl);
                  System.out.println(refreshInfo.toString());
                  accessToken = refreshInfo.getString("access_token");
              }
              /**
               * 使用access_token拉取用户信息
               */
              String infoUrl = WxAuthUtil.USER_INFO_URL
                      .replace(WxAuthUtil.ACCESS_TOKEN, accessToken)
                      .replace(WxAuthUtil.OPEN_ID, openid);
              JSONObject userInfo = WxAuthUtil.doGet(infoUrl);
              WxUserVO wxUserVO = JSONObject.parseObject(String.valueOf(userInfo), WxUserVO.class);
              log.info("用户数据-----{}", userInfo);
      
              return R.ok(wxUserVO);
          }
      
      • JS-SDK生成signature

      @ApiOperation("生成微信JS-SDK签名")
          @PostMapping( "/signature")
          public R> makeWxSignature(@RequestBody @Validated WxH5VO wxH5VO){
      
              //获取jsapiTicket
              String jsapiTicket = getJsapiTicket();
      
              Map ret = new HashMap<>(6);
              String nonceStr = RandomUtils.getRandomStr();
              long timestamp = System.currentTimeMillis()/1000L;
      
              //加密
              String signature = SHA1.genWithAmple(
                      "jsapi_ticket=" + jsapiTicket, "noncestr=" + nonceStr, "timestamp=" + timestamp, "url=" + wxH5VO.getUrl());
      
              ret.put("url", wxH5VO.getUrl());
              ret.put("jsapi_ticket", jsapiTicket);
              ret.put("nonceStr", nonceStr);
              ret.put("timestamp", Long.toString(timestamp));
              ret.put("signature", signature);
              ret.put("appId", WxMpConfig.appId);
      
              return R.ok(ret);
          }
      
          /**
           * 1、缓存中获取 jsApiTicket
           * 2、没有 已过期 重新获取
           * 2、获取缓存中的access_token
           * 3、没有 证明已经过期 重新获取
           * 获取jsapiTicket
           * @return jsapiTicket
           */
          private String getJsapiTicket(){
              String jsapiTicket = null;
              try{
                  //根据access_token 获取jsapiTicket
                  jsapiTicket = redisService.getCacheObject(WxAuthUtil.WX_JSAPI_TICKET);
                  if(Objects.isNull(jsapiTicket)){
                      //获取access_token
                      String accessToken = redisService.getCacheObject(WxAuthUtil.WX_ACCESS_TOKEN);
                      if(Objects.isNull(accessToken)){
                          JSONObject accessTokenMap = WxAuthUtil.getAccessToken();
                          accessToken = accessTokenMap.getString("access_token");
                          log.info("访问获取access_token数据{}", accessTokenMap);
                          redisService.setCacheObject(WxAuthUtil.WX_ACCESS_TOKEN, accessToken, accessTokenMap.getLong("expires_in"), TimeUnit.SECONDS);
                      }
                      String requestUrl = WxAuthUtil.JS_API_TICKET_URL.replace("ACCESS_TOKEN", accessToken);
                      //向微信发送get请求
                      JSONObject  callBack = WxAuthUtil.doGet(requestUrl);
                      log.info("访问获取jsApiTicket数据{}", callBack);
                      jsapiTicket = callBack.getString("ticket");
                      redisService.setCacheObject(WxAuthUtil.WX_JSAPI_TICKET, jsapiTicket,callBack.getLong("expires_in"), TimeUnit.SECONDS);
                  }
                  log.info("获取jsApiTicket:{}", jsapiTicket);
              }catch (IOException e){
                  log.info("获取签名异常:" + e.getMessage());
              }
              return jsapiTicket;
          }
      

      错误处理

      ? 官方常见错误及解决方法

      • invalid signature签名错误。建议按如下顺序检查:

        • 确认签名算法正确,可用http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign 页面工具进行校验。
        • 确认config中nonceStr(js中驼峰标准大写S), timestamp与用以签名中的对应noncestr, timestamp一致。
        • 确认url是页面完整的url(请在当前页面alert(location.href.split('#')[0])确认),包括'http(s)??/'部分,以及'?'后面的GET参数部分,但不包括'#'hash后面的部分。
        • 确认 config 中的 appid 与用来获取 jsapi_ticket 的 appid 一致。
        • 确保一定缓存access_token和jsapi_ticket
        • 确保你获取用来签名的url是动态获取的,动态页面可参见实例代码中php的实现方式。如果是html的静态页面在前端通过ajax将url传到后台签名,前端需要用js获取当前页面除去'#'hash部分的链接(可用location.href.split('#')[0]获取,而且需要encodeURIComponent),因为页面一旦分享,微信客户端会在你的链接末尾加入其它参数,如果不是动态获取当前链接,将导致分享后的页面签名失败。