微信认证授权详细代码示例

背景

公司有个H5页面分享到微信之后不显示图片,且样式跟其他应用的分享卡片存在差异.后来发现是因为没有调用微信统一的分享接口.下面就是我踩坑踩出来的心得.

开发前必须知道的东西

  • 要有一个公众号,有了公众号就会有appId和appsecret.
  • 要有一个外网可以访问的域名,测试的话,可以用内网穿透工具,比如natapp.
  • 微信公众号接口必须以http://或https://开头,分别支持80端口和443端口
  • 可以阅读微信的JS-SDK的说明文档

操作步骤

1 HttpUtil工具类的编写

这个工具类的作用就是获取签名,可分为四步:

  1. 根据appId和appsecret获取access_token;
  2. 使用access_token获取jsapi_ticket;
  3. 使用时间戳,随机数,jsapi_ticket和要访问的url按照签名算法拼接字符串;
  4. 对第三步的字符串进行SHA1加密,得到签名;

注意: check_signature方法中的clearLove是我的token,记得换成你们自己的

public class HttpUtil {

    /**
     * 获得ACCESS_TOKEN
     * @param appId
     * @param appSecret
     * @return
     */
    public static String getAccess_token(String appId, String appSecret){

        String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret;
        String accessToken = null;
        try
        {
            URL urlGet = new URL(url);
            HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
            http.setRequestMethod("GET"); // 必须是get方式请求
            http.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            http.setDoOutput(true);
            http.setDoInput(true);
            System.setProperty("sun.net.client.defaultConnectTimeout", "30000");// 连接超时30秒
            System.setProperty("sun.net.client.defaultReadTimeout", "30000"); // 读取超时30秒
            http.connect();
            InputStream is = http.getInputStream();
            int size = is.available();
            byte[] jsonBytes = new byte[size];
            is.read(jsonBytes);
            String message = new String(jsonBytes, "UTF-8");
            JSONObject jsonObj = JSONObject.parseObject(message);
            accessToken = jsonObj.getString("access_token");
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        return accessToken;
    }

    /**
     * 获得ACCESS_TICKET
     *
     * @Title: ACCESS_TICKET
     * @Description: 获得ACCESS_TICKET
     * @param @return 设定文件
     * @return String 返回类型
     * @throws
     */
    public static String getAccess_ticket(String access_token) {
        String ticket = null;
        String url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token="+ access_token +"&type=jsapi";//这个url链接和参数不能变
        try {
            URL urlGet = new URL(url);
            HttpURLConnection http = (HttpURLConnection) urlGet.openConnection();
            http.setRequestMethod("GET"); // 必须是get方式请求
            http.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
            http.setDoOutput(true);
            http.setDoInput(true);
            System.setProperty("sun.net.client.defaultConnectTimeout", "30000");// 连接超时30秒
            System.setProperty("sun.net.client.defaultReadTimeout", "30000"); // 读取超时30秒
            http.connect();
            InputStream is = http.getInputStream();
            int size = is.available();
            byte[] jsonBytes = new byte[size];
            is.read(jsonBytes);
            String message = new String(jsonBytes, "UTF-8");
            JSONObject demoJson = JSONObject.parseObject(message);
            System.out.println("JSON字符串:"+demoJson);
            ticket = demoJson.getString("ticket");
            is.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ticket;
    }

    /**
     * 获取SHA1加密后的字符串
     * @param decript
     * @return
     */
    public static String SHA1(String decript) {
        try {
            MessageDigest digest = java.security.MessageDigest.getInstance("SHA-1");
            digest.update(decript.getBytes());
            byte messageDigest[] = digest.digest();
            // Create Hex String
            StringBuffer hexString = new StringBuffer();
            // 字节数组转换为 十六进制 数
            for (int i = 0; i < messageDigest.length; i++) {
                String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
                if (shaHex.length() < 2) {
                    hexString.append(0);
                }
                hexString.append(shaHex);
            }
            return hexString.toString();

        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }
    
    /**
     * 验证签名
     * @param signature
     * @param timestamp
     * @param nonce
     * @return
     */
    public static boolean check_signature(String signature,String timestamp,String nonce) {
        String[] arr = new String[]{timestamp, nonce, "clearLove"};
        Arrays.sort(arr);
        String s = arr[0] + arr[1] + arr[2];
        //String md = MessageDigest.getInstance("SHA-1");
        //byte[] digest = md.digest(s.getBytes("utf-8"));
        // return signature == bytes2HexString(digest);
        String md = SHA1(s);
        return signature.equals(md);
    }
}

注意:
***access_token***有每天2000次的获取上限,并且有效时间长达两小时,所以建议缓存
access_token***和***access_ticket

2 申请测试公众号

  1. 登录微信公众平台申请测试号
  2. 下面这个是JS接口安全域名,把自己准备好的外网可访问的域名写上就可以了,没有任何验证.正式注册的公众号绑定的域名有修改次数限制,一个月最多修改三次.而测试公众号的没有这个问题.

注意!!!: 我这里配的不对,域名就是域名,不要带http或者https!!!
4. 上面这个是接口配置信息,不是填上URL和Token就完事了,微信会往你填写的这个接口发送请求,验证签名,验证通过了才会配置成功.

/**
     * 微信认证
     * @param request
     * @param response
     * @throws IOException
     */
 @GetMapping(value = "/wx")
    public void weChart(HttpServletRequest request, HttpServletResponse response) throws IOException {
         Map<String,String> map = new HashMap<>();
        String qs = request.getQueryString();
        // signature=79a282667d19b5578a9857c4dc87e1982843c1da&echostr=7129991673103630289×tamp=1591066064&nonce=1339036005
        String [] strs = qs.split("&");
        for(String s:strs){
            String [] str = s.split("=");
            map.put(str[0],str[1]);
        }
        String signature = map.get("signature");
        String echostr = map.get("echostr");
        String timestamp = map.get("timestamp");
        String nonce = map.get("nonce");
        if(HttpUtil.check_signature(signature,timestamp,nonce)){
             response.getWriter().println(echostr);
        }
    }

注意:

  1. 我一开始尝试的下图的方法,不行之后才换的上述方法,你们也可以试试这个:
  2. 如果方法写好了,并且确定可以正确的返回echostr.那就关闭断点再提交,否则会因为响应超时配置失败.

3 页面相关代码

  1. 在需要调用js接口的页面接入JS文件,这个文件就是这个样子的,你们复制一下就好了
  2. 通过config接口注入权限并验证配置,这一步也需要用到我们前面写好的HttpUtil.
wx.config({
    debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
    appId: '', // 必填,公众号的唯一标识
    timestamp: , // 必填,生成签名的时间戳
    nonceStr: '', // 必填,生成签名的随机串
    signature: '',// 必填,签名
    jsApiList: [] // 必填,需要使用的JS接口列表
});

下面是比较完整的前端代码,你们可以拿去改一下:

$.ajax({
            url: 'XXXXXXXXX',
            type: 'POST',
            dataType: 'json',
            //url需要编码传入而且要是完整的url除#之后的
            data: {"url":encodeURIComponent(window.location.href.split("#")[0])}
        })
        .done(function(res) {

            wx.config({
                debug: ture, //调试阶段建议开启
                appId: res.appId,//APPID
                timestamp: res.timestamp,//上面main方法中拿到的时间戳timestamp
                nonceStr: res.nonceStr,//上面main方法中拿到的随机数nonceStr
                signature: res.signature,//上面main方法中拿到的签名signature
                //需要调用的方法接口
                jsApiList: [ 'onMenuShareTimeline','onMenuShareAppMessage','onMenuShareWeibo','onMenuShareQQ','onMenuShareQZone']
            });
   }
wx.ready(function(){
                // alert("我已经进来了");
                wx.onMenuShareTimeline({
                    title: title, // 分享标题
                    link: window.location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
                    imgUrl: "//upload-images.jianshu.io/upload_images/3429385-09282d70c0390d94.png?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240", // 分享图标
                    success: function () {
                        // alert("成功")
                        // 用户点击了分享后执行的回调函数
                    }
                });
                wx.onMenuShareAppMessage({
                    title: title, // 分享标题
                    desc: descContent, // 分享描述
                    link: window.location.href, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
                    imgUrl: "//upload-images.jianshu.io/upload_images/3429385-09282d70c0390d94.png?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240", // 分享图标
                    type: '', // 分享类型,music、video或link,不填默认为link
                    dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空
                    success: function () {
                        // alert("成功")
                        // 用户点击了分享后执行的回调函数
                    }
                });
                wx.onMenuShareQQ({
                    title: title, // 分享标题
                    desc: descContent, // 分享描述
                    link: window.location.href, // 分享链接
                    imgUrl: "//upload-images.jianshu.io/upload_images/3429385-09282d70c0390d94.png?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240", // 分享图标
                    success: function () {
                        // alert("成功")
                        // 用户确认分享后执行的回调函数
                    },
                    cancel: function () {
                        // alert("失败")
                        // 用户取消分享后执行的回调函数
                        // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
                    }
                });
            });

注意:
在vue项目中引入jssdk,微信为了方便用户使用,将官方的jssdk发布到了npm上,有一个叫weixin-js-sdk的,但我们需要使用的不是这个,网上很多在vue中引用的是这个,但是这个是为commonjs发布的版本,只能通过require引入,很多人发现在vue中引入import wx from ‘weixin-js-sdk’,console.log(wx)会出现undefined,实际为了方便es6使用,官方发布了一个weixin-jsapi,这个才是我们能在vue中引用的jssdk。

下面是后台controller中的获取配置信息的方法:

@RequestMapping("/getConfig")
    protected void doPost(HttpServletRequest request, HttpServletResponse response){
        //appId和appSecret
        String appId = "wx001ad717b5e73574";
        String appSecret = "5eb0766e0fe465d1e95a7b460abc045c";
        //外部传入url获取url url需要是微信中打开的url否则会报错。
        String URL = request.getParameter("url");
        //转换url
        String url="";
        //需要转换解码url
        try {
            url = java.net.URLDecoder.decode(URL,"UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        //获取access_token
        String aeecss_token = HttpUtil.getAccess_token(appId, appSecret);
        //获取access_ticket
        String aeecss_ticket = HttpUtil.getAccess_ticket(aeecss_token);
        //3、时间戳和随机字符串
        String nonceStr = UUID.randomUUID().toString().replace("-", "").substring(0, 16);//随机字符串
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);//时间戳

        System.out.println("accessToken:"+aeecss_token+"\njsapi_ticket:"+aeecss_ticket+"\n时间戳:"+timestamp+"\n随机字符串:"+nonceStr);

        //4、获取url
        //5、将参数排序并拼接字符串
        String str = "jsapi_ticket="+aeecss_ticket+"&noncestr="+nonceStr+"×tamp="+timestamp+"&url="+url;
        //6、将字符串进行sha1加密
        String signature =HttpUtil.SHA1(str);
        System.out.println("参数:"+str+"\n签名:"+signature);
        
        Map<String,String> map=new HashMap();
        map.put("appId",appId);
        map.put("timestamp",timestamp);
        map.put("accessToken",aeecss_token);
        map.put("ticket",aeecss_ticket);
        map.put("nonceStr",nonceStr);
        map.put("signature",signature);
        //JSONObject jsonObject = JSONObject.fromObject(map);
        String json = JSON.toJSONString(map);//map转String
        JSONObject jsonObject = JSON.parseObject(json);//String转json

        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");

        PrintWriter pw = null;
        try {
            pw = response.getWriter();
        } catch (IOException e1) {
            //logger.error("**********reponse getWriter exception**********");
            e1.printStackTrace();
        }
        pw.write(jsonObject.toString());
        pw.close();
    }

注意:

  • 如果==getConfig()=接收到url,可以把请求方式改成get试试.
  1. 通过ready接口处理成功验证
wx.ready(function(){

    // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
});
  1. 通过error接口处理失败验证
wx.error(function(res){

    // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。

});

小结

搞之前觉得这个东西应该挺简单的,但是中间还是踩了很多坑,所以写了这文章总结一下,写完这篇文章的发现确实挺简单的,简单到代码放上来我都不知道该说啥了.