掘金 后端 ( ) • 2024-04-23 10:17

一、准备工作

  • 一个已备案的网站
  • 一个已认证的公众号(注意,个人权限是不可以的,需要企业权限)。

确保你有一个企业级的微信公众号,并完成企业认证。个人公众号可能无法获取全部接口权限。

  • 获取AppID和AppSecret

在微信公众平台登录后,进入“开发”部分,选择“基本配置”,在这里你可以找到你的AppID和AppSecret。

二、微信公众号后台设置

  • 设置JS接口安全域名: 进入公众号设置的“功能设置”中,填写“JS接口安全域名”。需确保你的网页服务部署在此域名下,且该域名已通过ICP备案。

  • 设置IP白名单: 在“安全中心”或“开发者中心”设置服务器IP白名单,以便微信服务器能够与之通信。

三、获取签名(含代码)

3.1 获取access_token

  • 接口说明:

使用你的AppID和AppSecret,通过GET请求到https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET来获取access_token。

参考以下文档获取access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token):

官方文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html

  • golang代码示例
/*
获取小程序全局唯一后台接口调用凭据:
https://developers.weixin.qq.com/minigame/dev/api-backend/open-api/access-token/auth.getAccessToken.html
*/
package weixinclient

import (
	"encoding/json"
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
)

type AccessTokenVo struct {
	AccessToken string `json:"access_token"`
	ExpiresIn   int64  `json:"expires_in"`
	Errcode     int64  `json:"errcode"`
	Errmsg      string `json:"errmsg"`
}

func GetAccessToken(c *gin.Context) (*AccessTokenVo, error) {
	ctx := commonx.GetTrace(c)

	ps := fmt.Sprintf("grant_type=client_credential&appid=%s&secret=%s", config.BookAppid, config.BookSecret)

	ep := getEndPoint("GetAccessToken")
	resp, err := doGet(c, ep, ps, nil)
	dlog.Infof("%v||GetSessionBycode resp=%v,err=%v", ctx, resp, err)
	if err != nil || len(resp) == 0 {
		return nil, errors.New("call API(GetAccessToken) fail")
	}

	var vo AccessTokenVo
	err = json.Unmarshal(resp, &vo)
	if err != nil || vo.Errcode > 0 {
		err = fmt.Errorf("err=%v", vo.Errmsg)
		return nil, err
	}

	return &vo, nil
}

返回示例:

{
	"access_token": "80_PR606SNAcwOIyhsRuuOVC11eHDiy1ZqKMiWn6JoxYZH3ANGt13s5DWgiWtIbk0JAxn5LBKyZBMK5-cP5q_NBvTVdFtIf9utExtktae_7c1t4Wm9aBkEd5fuYpw4MMQfAHARRV",
	"expires_in": 7200
}

3.2 获取jsapi_ticket

  • 接口说明:

使用上一步获取的access_token,通过GET请求到https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=ACCESS_TOKEN来获取jsapi_ticket。

参考以下文档获取access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token)

官方文档:https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html

  • 请求方式:
请求方式:GET
请求参数:上一步获取到的 access_token
请求地址:https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
注意:有效期7200秒, 这里建议将 access_token 和 jsapi_ticket 都在服务器端进行获取并缓存,前端通过接口调取结果
  • jsapi_ticket 说明:

jsapi_ticket是公众号用于调用微信 JS 接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取。由于获取jsapi_ticket的 api 调用次数非常有限,频繁刷新jsapi_ticket会导致 api 调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket (这里建议将 access_token 和 jsapi_ticket 都在服务器端进行获取并缓存,前端通过接口调取结果)。

  • golang代码示例:
package weixinclient

import (
	"encoding/json"
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
)

type GetticketVo struct {
	Ticket    string `json:"ticket"`
	ExpiresIn int64  `json:"expires_in"`
	Errcode   int64  `json:"errcode"`
	Errmsg    string `json:"errmsg"`
}

func Getticket(c *gin.Context, accessToken string) (*GetticketVo, error) {
	ctx := commonx.GetTrace(c)

	ps := fmt.Sprintf("access_token=%s&type=jsapi", accessToken)

	ep := getEndPoint("Getticket")

	resp, err := doGet(c, ep, ps, nil)
	dlog.Infof("%v||Getticket resp=%v,err=%v", ctx, resp, err)
	if err != nil || len(resp) == 0 {
		return nil, errors.New("call API(Getticket) fail")
	}

	var vo GetticketVo
	err = json.Unmarshal(resp, &vo)
	if err != nil || vo.Errcode > 0 || vo.Errmsg != "ok" {
		err = fmt.Errorf("err=%v", vo.Errmsg)
		return nil, err
	}

	return &vo, nil
}

成功返回如下JSON:

{
  "errcode":0,
  "errmsg":"ok",
  "ticket":"caLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
  "expires_in":7200
}

3.3 根据获取到的 ticket 来生成签名

  • 签名说明

微信开发中,ticket 通常用于获取 JS-SDK 的配置参数,包括签名(signature)。这些参数使得你能够在网页上使用微信的 JS-SDK。以下是一个基本的步骤说明如何使用获取到的 ticket 来生成签名:

准备用于签名的参数:nonceStr(随机字符串), timestamp(时间戳), url(当前网页的 URL,不包含 # 及其后面部分)和 jsapi_ticket

将这些参数按照字段名的ASCII 码从小到大排序(字典序),并且使用 URL 键值对的格式(即 key1=value1&key2=value2…)拼接成字符串。 对拼接后的字符串进行 SHA1 加密,得到的结果即是签名(signature)。

  • golang代码示例
package main  
  
import (  
	"crypto/sha1"  
	"encoding/hex"  
	"fmt"  
	"sort"  
	"strings"  
	"time"  
)  
  
// GenerateSignature 生成微信JS-SDK的签名  
func GenerateSignature(nonceStr, url, jsapiTicket string) string {  
	// 实时获取当前时间戳  
	timestamp := fmt.Sprintf("%d", time.Now().Unix())  
  
	// 准备用于签名的原始数据  
	params := map[string]string{  
		"jsapi_ticket": jsapiTicket,  
		"noncestr":     nonceStr,  
		"timestamp":    timestamp,  
		"url":          url,  
	}  
  
	// 对参数名进行排序并拼接  
	var keys []string  
	for k := range params {  
		keys = append(keys, k)  
	}  
	sort.Strings(keys)  
  
	var signStrings []string  
	for _, key := range keys {  
		signStrings = append(signStrings, fmt.Sprintf("%s=%s", key, params[key]))  
	}  
	signString := strings.Join(signStrings, "&")  
  
	// 使用SHA1进行签名  
	h := sha1.New()  
	h.Write([]byte(signString))  
	signature := hex.EncodeToString(h.Sum(nil))  
  
	return signature  
}  
  
func main() {  
	nonceStr := "Wm3WZYTPz0wzccnW" // 这个应该是随机生成的字符串  
	url := "http://mp.weixin.qq.com?params=value" // 你的网页URL  
	jsapiTicket := "bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA" // 从微信服务器获取的jsapi_ticket  
  
	// 生成签名  
	signature := GenerateSignature(nonceStr, url, jsapiTicket)  
	fmt.Println("生成的签名是:", signature)  
}

在这个示例中,GenerateSignature 函数接受四个参数:nonceStr(随机字符串),timestamp(时间戳),url(当前网页的URL),和jsapiTicket(从微信服务器获取的票据)。函数内部会对这些参数进行字典排序,拼接成一个待签名的字符串,然后使用SHA1算法生成签名。

请注意,为了简化示例,这里直接提供了nonceStr、timestamp、url和jsapiTicket的示例值。在实际应用中,我们需要根据具体情况动态获取这些值。特别是jsapiTicket,你需要先从微信服务器获取。

最后,main 函数中调用了 GenerateSignature 并打印出了生成的签名。我们可以将这段代码集成到我们的Go应用中,并根据需要进行适当的修改和调整。

  • noncestr 的含义是什么?应该如何动态获取?

noncestr是一个随机字符串,通常用于确保请求的唯一性和安全性。在微信JS-SDK、微信支付等接口中,noncestr 作为一个重要的参数,用于防止重放攻击和确保请求的时效性。

在微信开发中,noncestr 通常需要你自己生成,并确保其唯一性和随机性。以下是几种生成 noncestr 的方法:

  1. 使用随机数或UUID: 你可以使用编程语言中的随机数生成函数或者UUID生成库来创建一个唯一的字符串。例如,在Go语言中,你可以使用crypto/rand库生成一个随机数,并将其转换为字符串,或者使用第三方库如github.com/google/uuid来生成一个UUID。

  2. 时间戳结合随机数: 为了增加noncestr的复杂性,你也可以将当前的时间戳与随机数结合起来使用。这样既可以保证每次请求的noncestr都是唯一的,也能通过时间戳增加一定的时效性验证。

  3. 使用安全的随机数生成器: 在安全性要求较高的场景下,应使用安全的随机数生成器来产生noncestr,以确保其不可预测性。

golang动态获取 noncestr

package main  
  
import (  
	"crypto/rand"  
	"encoding/hex"  
	"fmt"  
	"log"  
)  
  
// GenerateNonceStr 生成一个随机的 noncestr  
func GenerateNonceStr(length int) (string, error) {  
	bytes := make([]byte, length/2) // 因为一个字节可以表示为两个16进制数字  
	if _, err := rand.Read(bytes); err != nil {  
		return "", err  
	}  
	return hex.EncodeToString(bytes), nil  
}  
  
func main() {  
	nonceStr, err := GenerateNonceStr(32) // 生成一个32个字符长的noncestr  
	if err != nil {  
		log.Fatalf("Failed to generate nonceStr: %v", err)  
	}  
	fmt.Println("Generated nonceStr:", nonceStr)  
}

得到的签名:

e851776f519a6c8d716bc61a5dec87b042d14c3e

四、H5页面配置与分享设置

注意:从这一步开始,后面的部分属于前端工作

4.1 引入微信JS-SDK

在需要调用 JS 接口的页面引入如下 JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.6.0.js

如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:http://res2.wx.qq.com/open/js/jweixin-1.6.0.js(支持https)。

备注:支持使用 AMD/CMD 标准模块加载方法加载

4.2 通过 config 接口注入权限验证配置

所有需要使用 JS-SDK 的页面必须先注入配置信息,否则将无法调用(同一个 url 仅需调用一次,对于变化 url 的SPA的web app可在每次 url 变化时进行调用,目前 Android 微信客户端不支持 pushState 的H5新特性,所以使用 pushState 来实现web app的页面会导致签名失败,此问题会在Android6.2中修复)。

注意:

  • 引入JS文件后,直接执行下列代码
  • debug: true 用来调试的,如果不想alert弹出,改成false即可
  • alert 弹出框中 errMsg 不一定就是错误,知识提示信息,例如 updateAppMessageShareData:ok 代表的是updateAppMessageShareData接口是没有问题的。
  • 签名用的 noncestr 和 timestamp 必须与 wx.config 中的 nonceStr 和 timestamp 相同。
  • jsApiList 接口列表:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#63
  • jsApiList 接口列表 例如 wx.updateAppMessageShareData({ 配置 }) jsApiList填写 ['updateAppMessageShareData'] 即可
wx.config({
  debug: true, // 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。
  appId: '', // 必填,公众号的唯一标识
  timestamp: , // 必填,生成签名的时间戳
  nonceStr: '', // 必填,生成签名的随机串
  signature: '',// 必填,签名(这里用上面第三步得到的签名)
  jsApiList: [] // 必填,需要使用的 JS 接口列表
});

4.3 通过 ready 接口处理成功验证

在wx.ready函数中配置onMenuShareTimeline、onMenuShareAppMessage等接口实现微信分享功能,并设置自定义的分享标题、描述、缩略图及链接。

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

4.4 通过 error 接口处理失败验证

使用wx.error函数处理验证失败的情况。

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

4.5 js代码示例

// 模拟从后端获取签名等配置信息的函数  
function getWechatConfig(url) {  
  // 通常情况下,以下代码应由后端服务生成并返回给前端  
  // 这里仅为示例,实际项目中,signature, nonceStr, timestamp 应该在服务器端生成  
  const appId = 'YOUR_APP_ID'; // 替换为你的AppID  
  const jsApiList = ['onMenuShareTimeline', 'onMenuShareAppMessage'];  
  
  // 以下为模拟数据,实际开发中应由服务器端提供  
  const nonceStr = 'Wm3WZYTPz0wzccnW'; // 随机字符串  
  const timestamp = Math.floor(Date.now() / 1000).toString(); // 当前时间戳  
  const signature = 'SOME_SIGNATURE_STRING'; // 签名,应由后端根据算法生成  
  
  return {  
    appId,  
    nonceStr,  
    timestamp,  
    signature,  
    jsApiList  
  };  
}  
  
// 调用微信JS-SDK配置  
function initWechatSDK() {  
  const currentUrl = encodeURIComponent(location.href.split('#')[0]);  
  const config = getWechatConfig(currentUrl);  
  
  wx.config({  
    debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。  
    appId: config.appId, // 必填,公众号的唯一标识  
    timestamp: config.timestamp, // 必填,生成签名的时间戳  
    nonceStr: config.nonceStr, // 必填,生成签名的随机串  
    signature: config.signature, // 必填,签名  
    jsApiList: config.jsApiList // 必填,需要使用的JS接口列表  
  });  
  
  wx.ready(function () {  
    // 在这里调用 API  
    configShare();  
  });  
  
  wx.error(function (res) {  
    // config信息验证失败会执行error函数,如签名过期(7200s)等原因触发error函数  
    console.error('微信JS-SDK配置失败', res);  
  });  
}  
  
// 配置微信分享  
function configShare() {  
  const shareData = {  
    title: '自定义分享标题', // 分享标题  
    desc: '自定义分享描述', // 分享描述  
    link: 'https://example.com', // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致  
    imgUrl: 'https://example.com/path/to/image.jpg', // 分享图标  
    type: '', // 分享类型,music、video或link,不填默认为link  
    dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空  
    success: function () {  
      // 用户点击了分享后执行的回调函数  
      console.log('分享成功');  
    }  
  };  
  
  wx.onMenuShareTimeline(shareData); // 分享到朋友圈  
  wx.onMenuShareAppMessage(shareData); // 分享给朋友  
  wx.onMenuShareQQ(shareData); // 分享到QQ  
  wx.onMenuShareWeibo(shareData); // 分享到微博  
}  
  
// 初始化微信JS-SDK  
initWechatSDK();