一、简介

微信公众号开发中,JSSDK(JavaScript SDK)是实现微信原生能力的关键。在使用微信分享、拍照、录音、获取地理位置等功能之前,必须通过JSSDK的权限验证配置(即签名算法)来确保安全。

本文以完整的PHP实现为例,详细介绍如何获取微信JSSDK所需要的jsapi_ticket、生成权限签名,以及对常见问题的处理方案。

二、JS-SDK使用权限签名算法概述

签名生成规则如下:参与签名的字段包括noncestr(随机字符串)、jsapi_ticket(有效的票据)、timestamp(时间戳)、url(当前网页的URL,不包含#及其后面部分)。

步骤:

  1. 对所有待签名参数按照字段名的ASCII码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1
  2. 所有参数名均为小写字符
  3. 对string1作sha1加密,字段名和字段值都采用原始值,不进行URL转义

三、完整PHP实现

<?php
/**
 * 微信JSSDK页面授权实现类
 * 使用方法:实例化此类,然后调用getSignPackage()方法即可
 */
class JSSDK
{
    private $appId;
    private $appSecret;

    public function __construct($appId, $appSecret)
    {
        $this->appId = $appId;
        $this->appSecret = $appSecret;
    }

    /**
     * 获取JSSDK签名包
     * 包含appId、nonceStr、timestamp、url、signature等
     */
    public function getSignPackage()
    {
        // 获取jsapi_ticket
        $jsapiTicket_data = $this->getJsApiTicket();
        // 生成随机字符串
        $nonceStr = $this->getNonceStr();
        // 当前时间戳
        $timestamp = time();
        // 当前页面URL(不含#及其后面部分)
        $url = $this->getUrl();

        if ($jsapiTicket_data['errcode'] == 0) {
            $jsapiTicket = $jsapiTicket_data['ticket'];
            // 这里参数的顺序要按照key值ASCII码升序排序
            $string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr×tamp=$timestamp&url=$url";
            $signature = sha1($string);
            return array(
                "appId" => $this->appId,
                "nonceStr" => $nonceStr,
                "timestamp" => $timestamp,
                "url" => $url,
                "signature" => $signature,
                "rawString" => $string,
                "errcode" => $jsapiTicket_data['errcode'],
                "errmsg" => $jsapiTicket_data['errmsg']
            );
        } else {
            return array(
                "appId" => $this->appId,
                "nonceStr" => $nonceStr,
                "timestamp" => $timestamp,
                "url" => $url,
                "signature" => null,
                "rawString" => null,
                "errcode" => $jsapiTicket_data['errcode'],
                "errmsg" => $jsapiTicket_data['errmsg']
            );
        }
    }

    /**
     * 获取access_token
     * 注意:access_token有效期为7200秒,需要缓存!
     */
    private function getAccessToken()
    {
        $appId = $this->appId;
        $appSecret = $this->appSecret;
        $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=' . $appId . '&secret=' . $appSecret;
        $res = $this->api_request($url);
        if (isset($res->access_token)) {
            return array(
                'errcode' => 0,
                'errmsg' => 'success',
                'access_token' => $res->access_token,
                'expires_in' => $res->expires_in
            );
        } else {
            return array(
                'errcode' => $res->errcode,
                'errmsg' => $res->errmsg,
                'access_token' => null,
                'expires_in' => null
            );
        }
    }

    /**
     * 获取jsapi_ticket
     * 有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket
     */
    private function getJsApiTicket()
    {
        $access_token_data = $this->getAccessToken();
        if ($access_token_data['errcode'] == 0) {
            $access_token = $access_token_data['access_token'];
            $url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=' . $access_token . '&type=jsapi';
            $res = $this->api_request($url);
            if ($res->errcode == 0) {
                return array(
                    'errcode' => $res->errcode,
                    'errmsg' => $res->errmsg,
                    'ticket' => $res->ticket,
                    'expires_in' => $res->expires_in
                );
            } else {
                return array(
                    'errcode' => $res->errcode,
                    'errmsg' => $res->errmsg,
                    'ticket' => null,
                    'expires_in' => null
                );
            }
        } else {
            return array(
                'errcode' => $access_token_data['errcode'],
                'errmsg' => $access_token_data['errmsg'],
                'ticket' => null,
                'expires_in' => null
            );
        }
    }

    /**
     * 获取随机字符串nonceStr
     */
    private function getNonceStr($length = 16)
    {
        $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        $nonceStr = "";
        for ($i = 0; $i < $length; $i++) {
            $nonceStr .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
        }
        return $nonceStr;
    }

    /**
     * 获取当前页面URL
     * URL不包含#及其后面部分
     */
    private function getUrl()
    {
        $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
        $url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
        return $url;
    }

    /**
     * 微信API请求方法
     */
    private function api_request($url, $data = null)
    {
        $ch = curl_init();
        $opts = array(
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_TIMEOUT => 500,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_URL => $url,
        );
        curl_setopt_array($ch, $opts);
        if (!empty($data)) {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
        }
        $output = curl_exec($ch);
        if (curl_errno($ch)) {
            var_dump(curl_error($ch));
            die;
        }
        curl_close($ch);
        $res = json_decode($output);
        return $res;
    }
}

四、使用示例

<?php
// 配置你的AppID和AppSecret
$appId = '你的AppID';
$appSecret = '你的AppSecret';

// 实例化JSSDK类并获取签名包
$jssdk = new JSSDK($appId, $appSecret);
$signPackage = $jssdk->getSignPackage();
?>

<!-- 页面引入微信JSSDK -->
[removed][removed]
[removed]
    wx.config({
        debug: false, // 调试模式
        appId: '<?php echo $signPackage["appId"]; ?>',
        timestamp: <?php echo $signPackage["timestamp"]; ?>,
        nonceStr: '<?php echo $signPackage["nonceStr"]; ?>',
        signature: '<?php echo $signPackage["signature"]; ?>',
        jsApiList: [
            'updateAppMessageShareData', // 更新分享给朋友
            'updateTimelineShareData',   // 更新分享到朋友圈
            'chooseImage',                // 拍照或从相册选图
            'getLocation',                // 获取地理位置
            'scanQRCode'                  // 扫一扫
        ]
    });

    wx.ready(function () {
        console.log('JSSDK配置成功');
        // 分享配置
        wx.updateAppMessageShareData({
            title: '分享标题',
            desc: '分享描述',
            link: [removed].href,
            imgUrl: '分享图标URL',
            success: function () {
                console.log('分享设置成功');
            }
        });
        wx.updateTimelineShareData({
            title: '朋友圈标题',
            link: [removed].href,
            imgUrl: '分享图标URL',
            success: function () {
                console.log('朋友圈设置成功');
            }
        });
    });

    wx.error(function (res) {
        console.log('JSSDK配置失败:' + JSON.stringify(res));
    });
[removed]

五、重要注意事项

5.1 缓存机制(必须实现)

access_tokenjsapi_ticket有效期均为7200秒,微信接口调用次数有限制,频繁获取会被限流!

推荐缓存方案:

<?php
/**
 * 带缓存的JSSDK实现示例
 */
class JSSDKWithCache extends JSSDK
{
    private $cacheFile = '/tmp/jssdk_cache.json';

    /**
     * 从缓存读取access_token,不存在或过期则重新获取
     */
    public function getAccessTokenWithCache()
    {
        $cache = $this->readCache();
        if (isset($cache['access_token']) && $cache['expires_at'] > time()) {
            return $cache['access_token'];
        }
        // 重新获取并缓存
        $data = parent::getAccessToken();
        if ($data['errcode'] == 0) {
            $this->writeCache(array(
                'access_token' => $data['access_token'],
                'expires_at' => time() + $data['expires_in'] - 200 // 提前200秒过期
            ));
            return $data['access_token'];
        }
        return null;
    }

    /**
     * 读取缓存文件
     */
    private function readCache()
    {
        if (file_exists($this->cacheFile)) {
            return json_decode(file_get_contents($this->cacheFile), true);
        }
        return array();
    }

    /**
     * 写入缓存文件
     */
    private function writeCache($data)
    {
        file_put_contents($this->cacheFile, json_encode($data));
    }
}

5.2 JS接口安全域名设置

在调用JSSDK之前,必须先在微信公众平台完成以下配置:

  1. 登录微信公众平台:https://mp.weixin.qq.com
  2. 进入"设置与开发" → "基本配置" → "JS接口安全域名"
  3. 设置要使用的域名(不带http://,不带路径)
  4. 点击"添加"保存

5.3 URL签名注意事项

  • 签名用的url必须是调用JS接口页面的完整URL
  • URL不包含#及其后面部分,需要在JavaScript中通过location.href.split('#')[0]获取
  • 如果是动态页面,需要用PHP动态获取当前URL
  • 页面一旦分享,微信客户端会在链接末尾加入其他参数,必须动态获取当前链接

六、常见问题及解决

6.1 错误:config:invalid signature

原因:签名生成错误

排查步骤:

  1. 确认jsapi_ticket是否有效且未过期
  2. 确认url是否包含#及后面的内容
  3. 确认nonceStr与wx.config中的nonceStr完全一致
  4. 确认生成签名的url与当前页面url一致
  5. 确认nonceStr和timestamp是大小写敏感
  6. 使用开发者工具或打日志输出签名信息,与官方签名校验工具对比

6.2 错误:config:url not in whitelist

原因:调用JSSDK的页面域名未在JS接口安全域名中配置

解决:在微信公众平台的"JS接口安全域名"中添加对应域名

6.3 无法调用微信JS接口

可能原因:

  • 引入了错误的JSSDK文件(建议使用官方CDN)
  • jsApiList中未包含要调用的接口名称
  • w.domain-checker w.domain-checker
  • 未通过wx.ready回调调用接口

6.4 分享链接签名失败

原因:用户分享后微信会在URL末尾追加参数

解决:对于Vue/SPA等单页应用,必须在每次页面切换时重新签名,或通过前端动态获取URL传到后端进行签名

// Vue项目中动态获取URL进行签名的方式
wx.ready(function () {
    // 异步加载的页面,需要重新配置
    wx.config({
        /* ... */
    });
});

// 或者在页面路由变化时重新获取签名
axios.post('/jssdk/sign', { url: location.href.split('#')[0] })
    .then(function (res) {
        wx.config({
            appId: res.data.appId,
            timestamp: res.data.timestamp,
            nonceStr: res.data.nonceStr,
            signature: res.data.signature,
            jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData']
        });
    });

七、完整流程图

JSSDK签名的完整流程如下:

  1. 前端 → 访问微信页面
  2. 后端 → 获取access_token(使用AppID和AppSecret调用微信接口)
  3. 后端 → 使用access_token获取jsapi_ticket
  4. 后端 → 使用jsapi_ticket、随机字符串、时间戳、URL生成签名
  5. 后端 → 将签名包(appId、nonceStr、timestamp、signature、url)返回前端
  6. 前端 → 使用wx.config注入配置信息
  7. 前端 → 在wx.ready中调用微信JSSDK接口

八、总结

微信JSSDK页面授权(签名算法)是微信公众号H5开发的核心环节,需要注意以下关键点:

  • access_token和jsapi_ticket必须缓存,避免频繁调用接口被封禁
  • 签名URL必须与调用页面的完整URL完全一致
  • 必须在微信公众平台设置JS接口安全域名
  • 所有签名参数和wx.config中的参数必须完全一致
  • 在wx.ready回调中调用JSSDK接口,不要在其他位置调用

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部