微信小程序获取微信运动步数:授权解密全流程实战

发布时间:2026/10/17 15:49:05
微信小程序获取微信运动步数:授权解密全流程实战
简介这是一份可直接运行的小程序源码包面向需要在小程序中接入微信运动步数功能的开发者覆盖从用户登录态获取到运动数据解密展示的完整链路适合前端入门者学习接口串联也适合有基础者快速移植复用。资源共三个文件含页面文件、环境配置与Git忽略规则压缩包仅9KB导入开发工具后即可查看运行效果已有一百五十八人学习下载。源码清晰演示了wx.login获取openid与session_key、wx.getWeRunData读取加密运动数据、WXBizDataCrypt解密得出步数的具体流程同时给出wx.getSetting、wx.openSetting、wx.authorize等权限引导用法便于在用户开启微信运动的前提下优化授权体验整个流程对用户运动开关状态、接口返回异常与解密失败场景均有可参考的应对思路。读者可对照源码拆解接口调用顺序、参数含义与异常处理细节直接二次开发或嵌入自身小程序项目有效节省查阅文档与踩坑时间。1. 小程序获取微信运动步数这个需求为什么卡住了不少人做微信小程序的人应该都遇到过这类需求做个运动打卡、步数排行榜、健康周报第一步就是拿用户的微信运动步数。听起来不就是调用一个 API 吗真做起来会发现坑比想象的多——授权弹窗要写对、步数只能在特定时间段读、解密验签要在后端做、模拟器上永远拿不到真实数据。这一套走通新手容易在某个环节卡上一两天。这篇东西要解决的就是「从零到跑通」这件事。我会把获取微信运动步数的完整链路拆开前端怎么发起授权、后端怎么验签解密、数据要落在什么结构里以及每一步最容易踩的坑。最后会给出一套前后端可运行的思路和关键代码拿到手改改 appid 和密钥就能跑。适合刚接触微信小程序、或者被运动步数卡过的同学照着做。我们直接进正题。2. 微信运动步数获取原理先搞懂这个能力的前提与边界2.1 这背后的接口与授权逻辑微信小程序获取运动步数靠的是wx.getWechatMovementStep这个接口。它不是普通 API而是与微信运动深度联动的能力。用户必须已经在微信运动里授权过数据小程序端还要再次拉起授权弹窗两层授权都过了才有机会拿到步数。接口的行为逻辑可以这样理解每次拉起这个接口微信会返回一段加密数据encryptedData和对应的初始向量iv外加一个校验用的signature。步数数据并不直接出现在接口返回值里而是包在加密字符串里。也就是说你拿到那串密文才是刚刚开始。这是我建议你先在开发者工具里跑通的探测代码wx.getWechatMovementStep({ timeRange: { startTime: Date.now() - 86400000, // 昨天 endTime: Date.now() }, success(res) { console.log(encryptedData:, res.encryptedData) console.log(iv:, res.iv) console.log(signature:, res.signature) }, fail(err) { console.error(获取失败, err) } })这里的timeRange是必传参数起始时间用毫秒时间戳。上面这段代码取的是「昨天」到「现在」的范围实际开发里通常也是这个区间——微信运动步数接口对跨天数据的支持并不友好后面避坑章节我会专门说。你先跑这段能在 console 里打出三样东西说明授权链路没问题接下来才谈得上解密。2.2 为什么必须后端解密而不是前端直接解这里有一个很常见的误区。很多新手看到encryptedData会想前端拿到之后直接用小程序里存的session_key解密不就行了代码写起来好像也不难几分钟就能跑通。但真要在项目里这么干你就是把用户的步数数据暴露在了一个完全不受控的环境里。session_key是通过wx.login获取的会话密钥它在官方设计里就只能存在服务器端。一旦把这个值下发到小程序前端任何人都可以通过抓包或者调试工具把它捞出来。步数属于用户隐私数据后续微信审核时也会重点关注如果发现小程序在本地拿着session_key做用户数据解密审核被打回是大概率事件。所以整个正确的链路是小程序端 wx.login 获取 code 小程序端将 code 发送到自有后端 后端用 code 换 session_key (需要 appid secret) 后端用 session_key 解密 encryptedData 后端把解密后的步数存入自己的数据库或缓存 小程序端只拿到解密结果关键就在于session_key不落地前端后端解密完成后直接落库连返回给前端都可以只给「步数值」而不再回传密文。这既是安全要求也是审核要求的常规姿势。常见做法是后端加一个缓存层把session_key按用户维度存起来避免每次拿步数都重新登录一次减少接口往返。3. 可运行源码的落地前端调用与授权握手完整实现3.1 授权链路的最小可运行代码小程序端先要引导用户完成微信运动的授权。注意这里的授权分两段第一段是微信运动在微信 App 层面的授权第二段是小程序内部的授权弹窗。第一段不是你能主动拉起的用户没开小程序端只能提示去开。我一般会把这两段状态合并成一个前置检查函数放在首页的onLoad或进入步数页面前调用// pages/steps/index.js const authService require(../../services/authService) Page({ data: { hasAuthorized: false, stepList: [], todaySteps: 0 }, async onLoad() { const authResult await authService.checkMovementAuth() if (!authResult.authorized) { // 还没授权用 wx.getSetting 尝试拉起授权弹窗 await this.requestMovementAuth() } else { this.setData({ hasAuthorized: true }) await this.fetchStepData() } }, async requestMovementAuth() { wx.getSetting({ success: async (res) { if (res.authSetting[scope.werun]) { // 之前拒绝过需要引导去设置页打开 wx.showModal({ title: 提示, content: 需要微信运动授权才能读取步数是否前往设置开启, success: (modalRes) { if (modalRes.confirm) { wx.openSetting() } } }) } else { // 首次申请权限 wx.authorize({ scope: scope.werun, success: () { this.setData({ hasAuthorized: true }) this.fetchStepData() }, fail: () { wx.showToast({ title: 授权失败, icon: none }) } }) } } }) }, async fetchStepData() { wx.login({ success: async (loginRes) { const code loginRes.code const stepsRes await this.getEncryptedStepData() console.log(步数查询原始数据:, stepsRes) // code 和加密数据一起发给后端解 wx.request({ url: https://your-domain.com/api/steps/decrypt, method: POST, data: { code, encryptedData: stepsRes.encryptedData, iv: stepsRes.iv, signature: stepsRes.signature }, success: (res) { const steps res.data.steps this.setData({ todaySteps: steps[steps.length - 1]?.stepCount || 0 }) } }) } }) } })这段代码的核心逻辑有几个地方值得说明。首先scope.werun是小程序里微信运动授权的固定 scope 名不能写错。其次wx.authorize只适合首次弹窗如果用户之前已经点过拒绝再调它不会弹窗而是直接走 fail所以要用wx.openSetting引导用户去设置页手动打开。最后wx.login要放在每次获取步数之前因为后端需要新的code去换session_key。3.2 后端接密与解密的完整代码参考后端选型上用 Node.js 的开发者比较多因为小程序生态里 Node 的库最齐。这里给一份基于 Node.js Koa2 的解密接口实现。session_key的获取依赖小程序官方的code2Session接口你需要把 appid 和 secret 配置在环境变量里不要写死在代码里。// routes/steps.js const Router require(koa-router) const crypto require(crypto) const axios require(axios) const router new Router() const APPID process.env.WX_APPID const SECRET process.env.WX_SECRET async function code2Session(code) { const url https://api.weixin.qq.com/sns/jscode2session const { data } await axios.get(url, { params: { appid: APPID, secret: SECRET, js_code: code, grant_type: authorization_code } }) return data } function decryptWechatData(encryptedData, sessionKey, iv) { // 微信官方推荐方式是 AES-128-CBC 解密iv 是十六位 const decodedKey Buffer.from(sessionKey, base64) const decodedIV Buffer.from(iv, base64) const decipher crypto.createDecipheriv(aes-128-cbc, decodedKey, decodedIV) decipher.setAutoPadding(true) let decoded decipher.update(encryptedData, base64, utf8) decoded decipher.final(utf8) const data JSON.parse(decoded) if (data.watermark.appid ! APPID) { throw new Error(水印校验失败appid 不匹配) } return data } router.post(/decrypt, async (ctx) { const { code, encryptedData, iv, signature } ctx.request.body // 1. code 换 session_key const sessionRes await code2Session(code) if (!sessionRes.session_key) { ctx.status 400 ctx.body { message: code 失效或非法错误码: sessionRes.errcode } return } // 2. 签名校验可选但建议做多一层防御 const signText ${encryptedData}${sessionRes.session_key} const localSign crypto.createHash(sha1).update(signText).digest(hex) if (localSign ! signature) { ctx.status 400 ctx.body { message: 签名校验失败 } return } // 3. 解密 try { const stepData decryptWechatData(encryptedData, sessionRes.session_key, iv) // stepData.stepInfoList 就是步数数组 ctx.body { steps: stepData.stepInfoList } } catch (err) { ctx.status 500 ctx.body { message: 解密失败: err.message } } }) module.exports router解密之后的stepInfoList长这样数组里每个元素包含timestamp和stepCount时间戳是秒级。一天之内的数据是整点切分的比如分成 24 个段。你前端拿到的步数可以取数组最后一个元素作为“到目前为止”的步数。这里我特别加了签名校验很多人会忽略这一步。虽然它不是解密流程的必需步骤但它能帮你提前发现请求被篡改或者 session_key 不匹配的问题。如果signature校验失败几乎可以肯定前端传的encryptedData和你后端手里换到的session_key对应不上。3.3 数据字段与存储建议解密出来的 JSON 结构其实是固定的你只需要关注一个顶层字段和两个子字段字段类型说明stepInfoListArray步数列表按时间戳升序排列stepInfoList[].timestampNumber步数对应的时间戳单位是秒stepInfoList[].stepCountNumber该时间点的累计步数注意timestamp是秒不是毫秒。前端Date.now()出来是毫秒后端解析时千万换算一下否则你把数据直接扔给前端渲染会出现所有步数都指向 1970 年附近的情况这也是个容易闹笑话的翻车点。存储上我建议直接按天存储不要按小时存储。因为每天的stepInfoList会在你下一次调用时被微信重新生成历史数据不保留。如果你要做一个 7 天步数趋势图那就要每天定时拉一次并存起来。一个简单的表结构大概是uid, stat_date, step_value, source, update_time每天一行即可。常见做法是用户在当天第一次打开小程序时拉取并 Upsert同一天重复拉取就覆盖这样既省存储也避免重复数据干扰统计。4. 微信运动步数的完整演示流从登录到拉取一次搞定4.1 登录态与步数拉取的先后关系很多第一次做这个功能的开发者会把顺序搞反。他们会先调wx.getWechatMovementStep拿到加密数据再调wx.login拿code然后把两个数据装一个包里发给后端。这个顺序不是不行但如果你把两步反过来先wx.login再拉步数整体流程会更顺因为后端必须先有session_key才能解密而code优先到达后端可以减少一次等待。更值得注意的一点是wx.login生成的code有效期只有五分钟用一次就作废。如果你的后端解密失败前端重新发起wx.login生成新code再重新拉encryptedData而不是拿旧的encryptedData去复用否则新旧session_key不匹配解密必然失败。这里给一个典型的前后端交互时序图——不是流程图是文字版小程序端 - wx.login 获取 code 小程序端 - wx.getWechatMovementStep 获取 encryptedData / iv 小程序端 - 发送 {code, encryptedData, iv} 到自有后端 后端 - code 换 session_key 后端 - 校验 signature 与 watermark.appid 后端 - AES-128-CBC 解密 后端 - 返回 stepInfoList 小程序端 - 渲染步数图表这一步如果失败建议先用后端code2Session的返回结果做排查点。只要session_key能正常换到剩下的解密就是纯算法问题问题定位范围会小很多。4.2 微信运动步数的完整前端处理示例把上面的代码整理成一个可以直接跑通的页面是很多同学想要的东西。我把它整合成一个完整的Page逻辑页面里只做三件事检查授权、拉取步数、展示步数列表。const app getApp() const DOMAIN https://api.your-domain.com Page({ data: { steps: [], total: 0, loading: true, authDenied: false }, onShow() { this.init() }, async init() { const res await this.checkAuth() if (!res) return await this.loadSteps() }, checkAuth() { return new Promise((resolve) { wx.getSetting({ success: (res) { const auth res.authSetting[scope.werun] if (auth undefined) { wx.authorize({ scope: scope.werun, success: () resolve(true), fail: () { this.setData({ authDenied: true }) resolve(false) } }) } else if (auth true) { resolve(true) } else { this.setData({ authDenied: true }) resolve(false) } } }) }) }, loadSteps() { wx.login({ success: (loginRes) { wx.getWechatMovementStep({ timeRange: this.getTimeRange(), success: (stepRes) { wx.request({ url: ${DOMAIN}/api/steps/decrypt, method: POST, data: { code: loginRes.code, encryptedData: stepRes.encryptedData, iv: stepRes.iv }, success: (resp) { const list resp.data.steps || [] const total list.length ? list[list.length - 1].stepCount : 0 this.setData({ steps: list, total, loading: false }) }, fail: () { this.setData({ loading: false }) wx.showToast({ title: 步数解析失败, icon: none }) } }) }, fail: (err) { console.error(getWechatMovementStep fail, err) this.setData({ loading: false }) } }) } }) }, getTimeRange() { const start new Date() start.setHours(0, 0, 0, 0) return { startTime: start.getTime(), endTime: Date.now() } } })这段代码在原有基础上做了两个调整。第一个调整是把timeRange的起点改为「今天零点」而不是昨天相同时间——因为用户打开页面时最关注的是今天走到多少步而且微信运动接口对跨天数据的返回粒度不稳定容易出现昨天残留数据干扰今天的统计。第二个调整是把onLoad改成了onShow因为用户从设置页开完授权返回小程序时onShow会重新触发体验上更顺滑。4.3 开发者工具里拿到的测试数据特征在小程序开发者工具里运行上面的代码你会发现在模拟器环境里wx.getWechatMovementStep经常返回固定的模拟数据或者直接报错。这是正常现象不代表代码有问题。真实设备上微信运动会返回过去一段时间的真实步数而且接口在开发者工具里的表现和真机上差异很大。模拟器常见现象是encryptedData往往很短或者stepCount一直为 0。这背后是因为开发者工具没有真实的运动传感器数据源只能模拟一个默认值。真正的验证只能靠真机预览在开发者工具右上角点「预览」扫码拿到真机上报的数据后才知道代码靠不靠谱。这也是为什么很多同学会在这一步反复怀疑自己代码写错了——别慌换个方式验证就好。# 小程序开发者工具上传代码后, 用预览码在真机上调试 # 真机调试模式下 vConsole 可以直接看到 wx.request 返回结果真机调试时timeRange.endTime不要传未来时间否则接口会报参数错误。另外startTime和endTime的最大跨度不要超过 30 天这是接口的隐藏限制。5. 获取微信运动步数的 5 个典型坑与排查手段5.1 开发者工具里永远报错 no permission现象在开发者工具里调用wx.getWechatMovementStep返回fail: no permission或者错误码 63001。原因开发者工具默认没有开启微信运动权限需要手动在工具的「详情 - 本地设置」里勾选「模拟微信运动」或者你的 appid 没有在主账号下完成微信运动功能的开通。解决先在「微信公众平台 - 设置 - 接口设置」里确认已开通「微信运动数据」权限。代码层面在开发者工具右上角「详情」里找到「本地设置」勾选「模拟微信运动」然后把appid换成自己的测试号重新编译。如果用的还是测试号注意测试号并不支持全部接口能力。5.2 真机上步数一直为 0现象真机调试时stepCount全部是 0但接口本身返回成功没有报错。原因微信运动步数接口有一个特性——用户在微信运动里没有打开「数据同步」或者手机系统没有将运动健康数据同步给微信 App。这会导致微信侧拿到的原始步数就是 0代码层面再正确也没用。解决让用户先在微信 App 里搜索「微信运动」并打开公众号进入后开启「记录运动数据」。iOS 用户还要检查「健康 App」里是否授权给了微信。这一步属于用户侧配置小程序里发现步数始终为 0要引导用户检查微信运动公众号设置而不是反复调接口。我一般在stepCount全为 0 时加一个引导弹窗让用户知道不是程序坏了而是系统数据源没开。5.3 signature 校验永远失败现象后端在本地用sha1(encryptedData session_key)计算出的签名和前端传过来的signature不一致。原因通常是session_key不是当前encryptedData对应的密钥。常见的前端错误是用户在小程序里登录过一次后来重新调了wx.login获取了新code但encryptedData还是旧登录态下拿到的新旧不匹配签名必然对不上。解决每次调用wx.getWechatMovementStep并拿到encryptedData之后立刻执行wx.login获取新code然后把两个数据放在同一个请求里发给后端。不要缓存encryptedData也不要复用之前的code。当你怀疑session_key过期时强制重新执行一次wx.login即可。5.4 跨天时间戳导致图表数据乱跳现象前端拿到stepInfoList后直接渲染发现步数曲线在凌晨 0 点时突然跳回很低的数值或者曲线出现重复段。原因stepInfoList返回的是整个时间范围内的所有分段点。如果你传的是「昨天 今天」的跨度列表里就会包含两天的数据且第二天的数据会从 0 重新开始累计直接渲染就在图表上出现断崖式回落。解决前端拿到列表后先做一次过滤只保留timestamp属于「今天」范围的分段数据。比较稳的做法是后端在返回前也做一次过滤避免前端逻辑被绕开。我一般会让前端先拿到完整数据用于调试但渲染层面过滤一次这样定位问题更可控。对stepInfoList的解析推荐把timestamp统一转成HH:mm格式再渲染免得不同端显示偏差搞混排查思路。5.5 审核被拒小程序涉及收集用户健康数据现象提审时被驳回理由通常是「小程序存在收集用户微信运动数据的行为但未在隐私保护指引中明确说明用途」。原因微信运动步数属于用户敏感信息平台对于这类数据的收集是有硬性审查机制的。你的小程序如果要在「用户隐私保护指引」里声明收集「运动数据」这一项并且说明用途但很多开发者最初做这个功能时没有及时去后台更新隐私配置。解决登录公众平台在「设置 - 服务内容声明 - 用户隐私保护指引」中添加收集的信息类型为「运动数据」用途写清楚「用于展示用户每日运动步数和运动趋势」。同时在代码里做弹窗提示告知用户步数会被用于哪些功能让用户知情。这两步都做到位审核正常就能通过。6. 验证这套方案是否真的跑通三个关键检查点整条链路跑通之后怎么确定每一步没有踩坑我建议按顺序检查三个点。第一个检查点是signature是否校验通过。你在后端把sha1(encryptedData session_key)的结果拿出来对比一下前端传的signature一致说明code与encryptedData用的是同一个会话链路的前半段没问题。如果这一步失败不要去查后面的解密逻辑先把登录和会话关系理清。第二个检查点是解密后的watermark.appid是否正确。解密成功不代表安全你要在代码里明确校验data.watermark.appid 你的 appid。这个字段是官方用来防止数据被放到其他环境解密的保护机制。有些开发者忽略这一步遇到数据异常时排查起来会非常痛苦因为错误往往不在算法层面而是密文本身就来自另一个小程序环境。检查点放这里相当于给解密过程上了一道保险。第三个检查点是stepInfoList的完整度。拿到的列表长度对应你传入时间跨度的小时数加一。比如你传startTime是今天 0 点endTime是下午 4 点正常情况下应该拿到 17 个数据点也就是 0 点到 16 点每小时一个点。如果点数严重低于预期可能是手机系统对微信运动数据同步的频率不够或者你的startTime和endTime跨度太大被接口截断。我会在页面上埋一个日志把「实际拿到的分段数」和「预期分段数」都记录下来对比后快速定位。最后提一个我自己的使用习惯生产环境里不要在小程序前端打印encryptedData和iv的完整内容只打印步骤数结果。之前 Debug 时把完整密文打到了 vConsole 里后来发现这些日志会直接暴露在用户调试面板中等于是把用户数据半公开了。从那以后我要求所有加密数据相关日志在后端打前端只打最终渲染值。这个习惯坚持下来省了很多不必要的隐私争议。如果读完这篇你还是觉得步数数据链路复杂那也不用灰心——它属于「全网都能搜到概念、但正确跑通的人并不多」的能力类型。收藏这份流程找个小项目先落地一次把授权、换密钥、解密、渲染走一圈你会发现第二次做运动健康类功能时整条链路会快很多。希望这篇笔记帮到你上手微信运动步数功能。本文还有配套的精品资源点击获取