评论

微信 JSSDK 签名踩坑记录

本文记录微信 JSSDK 签名的常见坑点,重点分析 iOS 签名失败的原因:iOS 只认入口 URL,非当前页面 URL。提供三种解决方案及完整代码,并补充开发者工具模拟器的特殊处理。

前言

在微信公众号/小程序 webview 中使用 H5 页面时,经常需要调用微信 JSSDK 提供的能力,如:

  • wx.chooseImage - 选择图片/拍照
  • wx.getLocation - 获取位置
  • wx.openLocation - 打开内置地图
  • wx.scanQRCode - 扫一扫

使用这些方法的前提是成功调用 wx.config 注入配置信息,而签名验证是最容易踩坑的环节。

官方文档:JS-SDK说明文档


坑点 1:测试账号提示签名非法

现象:使用微信公众平台接口测试帐号时提示 invalid signaturerequire subscribe

原因与解决

① 检查是否已关注测试账号,未关注会提示 require subscribe

② 如果项目使用 hash 模式路由,需要使用 location.href.split('#')[0] 作为签名 URL:

// hash 路由需要去掉 # 后面的部分
const url = encodeURIComponent(window.location.href.split('#')[0])

注意:URL 需要 encode,后端接口需要对应 decode


坑点 2:SPA 应用页面跳转后签名失败

现象:入口页面 JSSDK 正常,跳转到其他页面后调用失败

原因:仅在入口文件(如 App.vue)调用一次 wx.config,后续页面跳转导致 URL 变化,但签名仍是旧 URL 的签名

解决:在每个需要使用 JSSDK 的页面都调用 wx.config,确保签名 URL 与当前页面一致


坑点 3:iOS 真机签名失败(核心问题)

现象

  • Android 真机正常
  • 微信开发者工具正常
  • iOS 真机提示 invalid signature

开启 debug 模式后可以看到 realAuthUrl 字段,这是微信实际接收到的 URL。

根本原因

iOS 和 Android 对 SPA 路由的处理机制不同:

设备 路由跳转后的行为
Android router.push 会真实改变 location.href
iOS router.push 不会改变微信认可的 URL,微信只认第一次进入时的 URL

流程示例

用户扫码 → 进入页面A → router.push跳转到页面B → 在页面B调用wx.config
  • Android:签名 URL 用页面 B 的 URL(正确)
  • iOS:签名 URL 必须用页面 A 的 URL(入口 URL)(正确),用页面 B 的 URL(错误)

坑点 4:微信开发者工具模拟器的陷阱

现象:修复 iOS 真机问题后,微信开发者工具模拟器反而签名失败了

原因:微信开发者工具的 UserAgent 包含 iPhone,会被误判为 iOS 设备,但其签名机制与 Android 相同

// 微信开发者工具的 UserAgent 示例
"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) ... wechatdevtools"

签名 URL 规则汇总

环境 签名所需 URL
Android 真机 当前页面 URL
iOS 真机 入口 URL(第一次进入 webview 的 URL)
微信开发者工具 当前页面 URL

解决方案

方案一:wx.error 中刷新页面

原理:签名失败时刷新页面,使当前 URL 成为"入口 URL"

优点:实现简单,无需改动其他代码

缺点:用户体验差,会有明显的页面刷新

// 在 wx.config 调用后添加错误处理
wx.error((res) => {
  console.log('wx.config error:', res)

  // 检测是否为 iOS 设备(排除微信开发者工具)
  const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
  const isWechatDevTools = /wechatdevtools/i.test(navigator.userAgent)

  // iOS 真机签名失败时刷新页面
  if (isiOS && !isWechatDevTools) {
    // 使用 sessionStorage 标记,防止无限刷新
    const hasReloaded = sessionStorage.getItem('wx_config_reloaded')
    if (!hasReloaded) {
      sessionStorage.setItem('wx_config_reloaded', 'true')
      location.reload()
    } else {
      // 已经刷新过还是失败,清除标记并提示用户
      sessionStorage.removeItem('wx_config_reloaded')
      console.error('微信授权失败,请重新进入页面')
    }
  }
})

// 成功时清除刷新标记
wx.ready(() => {
  sessionStorage.removeItem('wx_config_reloaded')
  console.log('wx.config success')
})

方案二:保存入口 URL(推荐)

原理:在应用初始化时保存入口 URL,iOS 设备使用入口 URL 签名

优点:用户无感知,体验好

缺点:需要在应用入口处添加逻辑

步骤 1:在应用入口保存 URL

// main.js 或 App.vue 的最开始
// 保存 iOS 入口 URL(必须在任何路由跳转之前)
const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream

if (isiOS && !sessionStorage.getItem('wxIosEntryUrl')) {
  // 只在第一次进入时保存,后续页面跳转不覆盖
  sessionStorage.setItem('wxIosEntryUrl', window.location.href.split('#')[0])
}

步骤 2:封装获取签名 URL 的方法

/**
 * 获取微信 JSSDK 签名所需的 URL
 * @returns {string} 编码后的 URL
 */
export function getWxAuthUrl() {
  const isWechatDevTools = /wechatdevtools/i.test(navigator.userAgent)
  const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
  const iosEntryUrl = sessionStorage.getItem('wxIosEntryUrl')

  // iOS 真机(非开发者工具)使用入口 URL
  if (isiOS && iosEntryUrl && !isWechatDevTools) {
    return encodeURIComponent(iosEntryUrl)
  }

  // Android / 开发者工具 使用当前页面 URL
  return encodeURIComponent(window.location.href.split('#')[0])
}

步骤 3:调用签名接口时使用

// 获取签名并配置 wx.config
async function initWxConfig() {
  try {
    const url = getWxAuthUrl()

    // 调用后端接口获取签名
    const res = await fetch(`/api/wx/signature?url=${url}`)
    const { appId, timestamp, nonceStr, signature } = await res.json()

    wx.config({
      debug: false,
      appId,
      timestamp,
      nonceStr,
      signature,
      jsApiList: ['chooseImage', 'getLocation', 'openLocation']
    })

    wx.ready(() => {
      console.log('wx.config success')
    })

    wx.error((err) => {
      console.error('wx.config error:', err)
    })
  } catch (error) {
    console.error('initWxConfig error:', error)
  }
}

方案三:使用 location.href 跳转

原理:放弃前端路由,使用原生跳转,每次跳转都是"真实"的 URL 变化

优点:简单直接,不需要保存入口 URL

缺点:页面会完全刷新,丢失 SPA 的优势(状态保持、过渡动画等)

适用场景:只有个别页面需要使用 JSSDK

封装跳转方法

/**
 * 使用 location.href 进行页面跳转(脱离前端路由)
 * @param {string} path - 目标路径,如 '/pages/bindPhone'
 * @param {Object} query - 查询参数对象
 */
export function locationPush(path, query = {}) {
  const origin = window.location.origin

  // 构造查询参数字符串
  const queryString = Object.entries(query)
    .filter(([_, value]) => value !== undefined && value !== null)
    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
    .join('&')

  // 拼接完整 URL 并跳转
  const fullUrl = queryString ? `${origin}${path}?${queryString}` : `${origin}${path}`
  window.location.href = fullUrl
}

/**
 * 使用 location.replace 进行页面替换(无历史记录)
 * @param {string} path - 目标路径
 * @param {Object} query - 查询参数对象
 */
export function locationReplace(path, query = {}) {
  const origin = window.location.origin

  const queryString = Object.entries(query)
    .filter(([_, value]) => value !== undefined && value !== null)
    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
    .join('&')

  const fullUrl = queryString ? `${origin}${path}?${queryString}` : `${origin}${path}`
  window.location.replace(fullUrl)
}

使用示例

// 原本的写法(前端路由,iOS 会有签名问题)
this.$router.push({
  path: '/bindPhone',
  query: { redirect: '/bindPhone' }
})

// 改为 location 跳转(解决 iOS 签名问题)
locationPush('/bindPhone', { redirect: '/bindPhone' })

方案对比

方案 实现难度 用户体验 适用场景
方案一:刷新页面 简单 临时修复、紧急上线
方案二:保存入口URL 中等 推荐,适合大多数项目
方案三:location跳转 简单 一般 仅个别页面需要 JSSDK

完整示例(Vue 项目)

main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'

// 【关键】在应用最开始保存 iOS 入口 URL
const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
if (isiOS && !sessionStorage.getItem('wxIosEntryUrl')) {
  sessionStorage.setItem('wxIosEntryUrl', window.location.href.split('#')[0])
}

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

wxConfig.js(工具函数)

import wx from 'weixin-js-sdk'
import { getWxSignature } from '@/api/common'

/**
 * 获取微信 JSSDK 签名所需的 URL
 */
function getWxAuthUrl() {
  const isWechatDevTools = /wechatdevtools/i.test(navigator.userAgent)
  const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
  const iosEntryUrl = sessionStorage.getItem('wxIosEntryUrl')

  if (isiOS && iosEntryUrl && !isWechatDevTools) {
    return encodeURIComponent(iosEntryUrl)
  }
  return encodeURIComponent(window.location.href.split('#')[0])
}

/**
 * 初始化微信 JSSDK 配置
 * @param {string[]} jsApiList - 需要使用的 JS 接口列表
 * @returns {Promise<void>}
 */
export async function initWxConfig(jsApiList = []) {
  const url = getWxAuthUrl()

  try {
    const { appId, timestamp, nonceStr, signature } = await getWxSignature({ url })

    return new Promise((resolve, reject) => {
      wx.config({
        debug: false,
        appId,
        timestamp: Number(timestamp),
        nonceStr,
        signature,
        jsApiList
      })

      wx.ready(() => {
        console.log('wx.config success')
        resolve()
      })

      wx.error((err) => {
        console.error('wx.config error:', err)
        reject(err)
      })
    })
  } catch (error) {
    console.error('initWxConfig error:', error)
    throw error
  }
}

页面中使用

<template>
  <div>
    <button @click="handleChooseImage">选择图片</button>
  </div>
</template>

<script>
import wx from 'weixin-js-sdk'
import { initWxConfig } from '@/utils/wxConfig'

export default {
  async mounted() {
    // 初始化微信配置
    await initWxConfig(['chooseImage', 'getLocalImgData'])
  },

  methods: {
    handleChooseImage() {
      wx.chooseImage({
        count: 1,
        sizeType: ['compressed'],
        sourceType: ['album', 'camera'],
        success: (res) => {
          console.log('选择成功', res.localIds)
        },
        fail: (err) => {
          console.error('选择失败', err)
        }
      })
    }
  }
}
</script>

调试技巧

  1. 开启 debug 模式wx.config({ debug: true, ... }),会 alert 出详细错误信息

  2. 查看 realAuthUrl:debug 模式下签名失败会显示微信实际接收到的 URL

  3. 检查 URL 编码:确保前后端 URL 编码/解码一致

  4. 使用 vconsole:在移动端查看 console 日志

    import VConsole from 'vconsole'
    new VConsole()
    

总结

微信 JSSDK 签名的核心原则:传给后端生成签名的 URL 必须与微信认可的 URL 完全一致

  • Android 真机:当前页面 URL
  • iOS 真机:入口 URL(SPA 路由跳转不改变微信认可的 URL)
  • 微信开发者工具:当前页面 URL(UA 虽含 iPhone 但行为同 Android)

推荐使用方案二,在应用初始化时保存入口 URL,兼顾用户体验和代码可维护性。

最后一次编辑于  2025-12-31  
点赞 2
收藏
评论
登录 后发表内容