随着《个人信息保护法》深入落地和微信隐私保护机制的全面升级,小程序隐私合规已从"可选项"变为"必选项"。不合规的小程序将被限制调用敏感接口甚至下架。本文系统梳理2026年合规要求,提供可直接落地的代码方案。

一、微信隐私保护框架

1.1 隐私保护指引

自2023年9月起,微信要求所有小程序配置《小程序用户隐私保护指引》。开发者需要在小程序管理后台填写以下信息:

  • 小程序收集的个人信息类型
  • 每类信息的使用目的
  • 第三方信息共享情况
  • 用户权利保障机制

2026年新增要求:

  • 隐私保护指引必须与实际代码中使用的接口一一对应
  • 新增隐私接口类型时必须更新指引
  • 指引内容需覆盖数据存储位置、保留期限、删除方式

1.2 隐私协议弹窗机制

微信提供了内置的隐私保护弹窗机制,所有小程序必须接入:

javascript复制

// app.js 中检查隐私协议状态
App({
  onLaunch() {
    // 检查是否需要弹出隐私协议
    if (wx.getPrivacySetting) {
      wx.getPrivacySetting({
        success: (res) => {
          if (res.needAuthorization) {
            // 需要用户同意隐私协议
            this.globalData.privacyAuthorizationNeeded = true
          }
          console.log('隐私授权状态:', res)
          // res.needAuthorization: 是否需要授权
          // res.privacyContractName: 隐私协议名称
        }
      })
    }
  }
})

关键点:

  • wx.getPrivacySetting 判断是否需要弹出隐私协议
  • 用户未同意前,所有隐私接口将被拦截
  • 微信会自动弹出隐私协议弹窗(开发者也可自定义)

二、隐私接口清单

2.1 需要声明授权的接口

以下接口在用户未同意隐私协议前无法调用:

接口收集信息声明类型
wx.getUserProfile昵称、头像用户信息
wx.getLocation / wx.chooseLocation地理位置位置信息
wx.chooseImage / wx.chooseMedia图片/视频相册/摄像头
wx.getPhoneNumber手机号手机号码
wx.chooseAddress地址地址信息
wx.getClipboardData剪贴板剪贴板
wx.getRecorderManager录音麦克风
wx.startBluetoothDevicesDiscovery蓝牙设备蓝牙
wx.addPhoneContact通讯录通讯录
wx.getUserInfo用户信息用户信息
wx.chooseVideo视频摄像头
wx.saveImageToPhotosAlbum相册写入相册

2.2 2026年新增敏感接口类别

微信持续扩大隐私接口范围,以下操作也需在隐私保护指引中声明:

  • 广告组件:收集设备信息用于广告投放
  • 自定义组件中使用 wx.createSelectorQuery 获取节点信息
  • Webview 中加载的H5页面的数据收集行为
  • 插件引入的第三方隐私接口调用

三、2026年隐私合规核心要求

3.1 隐私保护指引完整性

javascript复制

// 检查隐私保护指引是否完整
// 在 app.js 的 onLaunch 中执行
function checkPrivacyGuide() {
  return new Promise((resolve, reject) => {
    wx.request({
      url: 'https://mp.weixin.qq.com/wxa-dev-logic/get_privacy_setting',
      method: 'POST',
      data: { appid: getApp().globalData.appId },
      success(res) {
        const { privacySetting } = res.data

        // 检查是否配置了隐私保护指引
        if (!privacySetting || !privacySetting.privacyContractName) {
          console.error('未配置隐私保护指引')
          reject(new Error('PRIVACY_GUIDE_MISSING'))
          return
        }

        // 检查指引是否通过审核
        if (privacySetting.status !== 'approved') {
          console.warn('隐私保护指引审核状态:', privacySetting.status)
        }

        resolve(privacySetting)
      },
      fail: reject
    })
  })
}

3.2 敏感接口授权后才能调用

javascript复制

// utils/privacyGuard.js

/**
 * 隐私接口调用守卫
 * 确保用户已同意隐私协议后才执行敏感操作
 */
class PrivacyGuard {
  constructor() {
    this.resolved = false
    this.pendingCallbacks = []
  }

  /**
   * 调用隐私接口前的统一检查
   * @param {Function} callback 用户同意后执行的操作
   */
  ensure(callback) {
    if (this.resolved) {
      callback()
      return
    }

    if (wx.requirePrivacyAuthorize) {
      this.pendingCallbacks.push(callback)

      wx.requirePrivacyAuthorize({
        success: () => {
          this.resolved = true
          this.pendingCallbacks.forEach(cb => cb())
          this.pendingCallbacks = []
        },
        fail: (err) => {
          console.warn('用户拒绝隐私协议:', err)
          this.pendingCallbacks = []
        }
      })
    } else {
      // 低版本基础库,直接执行
      callback()
    }
  }

  /**
   * 重置授权状态(用户主动触发)
   */
  reset() {
    this.resolved = false
  }

  /**
   * 检查是否已授权
   */
  isAuthorized() {
    return this.resolved
  }
}

const privacyGuard = new PrivacyGuard()
export default privacyGuard

3.3 不得默认勾选/强制授权

2026年合规红线:

  • 禁止将"同意隐私协议"默认勾选
  • 禁止将隐私同意与其他业务操作绑定(如"同意隐私协议才能领取优惠券"可以,但必须能跳过)
  • 禁止在用户拒绝授权后反复弹出隐私弹窗(建议同一会话最多弹2次)
  • 禁止在隐私协议中使用含糊表述(“收集必要信息”→需明确列出)

3.4 数据最小化原则

javascript复制

// 数据收集审查工具
const DATA_MINIMIZATION_RULES = {
  // 只收集业务必需的数据,不"贪多"
  '头像昵称': {
    required: false,       // 非登录必需
    alternative: '可使用默认头像和匿名昵称',
    retention: '用户主动更新前保留'
  },
  '手机号': {
    required: false,       // 可用微信登录替代
    alternative: '使用微信一键登录',
    retention: '仅用于必要业务场景'
  },
  '地理位置': {
    required: true,        // LBS类业务必需
    alternative: '可手动选择城市',
    retention: '服务完成后即删除'
  },
  '相册': {
    required: false,       // 仅上传图片时需要
    alternative: '可使用相机直接拍摄',
    retention: '仅服务期间使用'
  }
}

3.5 数据存储与传输安全

javascript复制

// utils/secureStorage.js
const crypto = require('crypto-js') // npm install crypto-js

/**
 * 安全存储工具
 * 对敏感数据进行AES加密后存储到本地
 */
class SecureStorage {
  constructor() {
    // 从服务端获取动态密钥,不要硬编码
    this.storageKey = 'secure_data_v2'
  }

  /**
   * 获取加密密钥(每个用户独立的盐值)
   */
  _getKey(salt = '') {
    const userId = wx.getStorageSync('userId') || 'default'
    return `${userId}_${salt}_${this.storageKey}`
  }

  /**
   * 加密存储
   */
  set(key, value, salt = '') {
    try {
      const jsonString = JSON.stringify(value)
      const encrypted = crypto.AES.encrypt(
        jsonString,
        this._getKey(salt)
      ).toString()

      wx.setStorageSync(key, {
        encrypted: true,
        version: 2,
        data: encrypted,
        timestamp: Date.now()
      })
      return true
    } catch (err) {
      console.error('加密存储失败:', err)
      return false
    }
  }

  /**
   * 解密读取
   */
  get(key, salt = '') {
    try {
      const stored = wx.getStorageSync(key)
      if (!stored) return null

      if (!stored.encrypted) {
        // 兼容旧版未加密数据
        console.warn('数据未加密,建议迁移')
        return stored.data || stored
      }

      const bytes = crypto.AES.decrypt(
        stored.data,
        this._getKey(salt)
      )
      return JSON.parse(bytes.toString(crypto.enc.Utf8))
    } catch (err) {
      console.error('解密失败:', err)
      return null
    }
  }

  /**
   * 安全删除
   */
  remove(key) {
    wx.removeStorageSync(key)
  }
}

const secureStorage = new SecureStorage()
export default secureStorage

四、隐私弹窗最佳实践

4.1 自定义隐私弹窗组件

javascript复制

// components/privacy-popup/privacy-popup.js
Component({
  properties: {
    // 是否显示
    show: {
      type: Boolean,
      value: false
    },
    // 隐私协议URL
    privacyUrl: {
      type: String,
      value: ''
    },
    // 用户协议URL
    userAgreementUrl: {
      type: String,
      value: ''
    }
  },

  data: {
    agreed: false
  },

  methods: {
    // 同意隐私协议
    handleAgree() {
      if (!this.data.agreed) {
        // 防止未勾选就同意
        wx.showToast({ title: '请先勾选同意', icon: 'none' })
        return
      }

      // 调用微信隐私协议确认接口
      if (wx.onNeedPrivacyAuthorization) {
        wx.onNeedPrivacyAuthorization(resolve => {
          resolve({ event: 'agree' })
        })
      }

      this.triggerEvent('agree')

      // 记录同意时间(用于审计)
      wx.setStorageSync('privacy_agreed_at', Date.now())
      wx.setStorageSync('privacy_version', '2026-06')
    },

    // 拒绝
    handleDisagree() {
      this.triggerEvent('disagree')
    },

    // 切换勾选状态
    toggleAgree() {
      this.setData({ agreed: !this.data.agreed })
    },

    // 打开隐私协议
    openPrivacy() {
      if (this.properties.privacyUrl) {
        wx.navigateTo({
          url: `/pages/webview/index?url=${encodeURIComponent(this.properties.privacyUrl)}`
        })
      } else {
        // 使用微信内置隐私协议页
        wx.openPrivacyContract()
      }
    },

    // 打开用户协议
    openUserAgreement() {
      if (this.properties.userAgreementUrl) {
        wx.navigateTo({
          url: `/pages/webview/index?url=${encodeURIComponent(this.properties.userAgreementUrl)}`
        })
      }
    }
  }
})

xml复制

<!-- components/privacy-popup/privacy-popup.wxml -->
<view class="privacy-mask" wx:if="{{show}}">
  <view class="privacy-dialog">
    <view class="dialog-title">用户隐私保护提示</view>

    <view class="dialog-body">
      <view class="dialog-desc">
        为了更好地保障您的合法权益,在使用我们的服务前,请仔细阅读
        <text class="link" bindtap="openPrivacy">《隐私保护指引》</text>
        和
        <text class="link" bindtap="openUserAgreement">《用户服务协议》</text>。
        我们将按照指引收集、使用和保护您的个人信息。
      </view>

      <view class="check-row" bindtap="toggleAgree">
        <view class="checkbox {{agreed ? 'checked' : ''}}">
          <text wx:if="{{agreed}}">✓</text>
        </view>
        <text class="check-text">我已阅读并同意</text>
      </view>
    </view>

    <view class="dialog-footer">
      <button class="btn-disagree" bindtap="handleDisagree">不同意</button>
      <button class="btn-agree" bindtap="handleAgree">同意并继续</button>
    </view>
  </view>
</view>

css复制

/* components/privacy-popup/privacy-popup.wxss */
.privacy-mask {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0, 0, 0, 0.6);
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
}

.privacy-dialog {
  width: 600rpx;
  background: #fff;
  border-radius: 24rpx;
  overflow: hidden;
}

.dialog-title {
  text-align: center;
  padding: 40rpx 32rpx 20rpx;
  font-size: 34rpx;
  font-weight: 600;
}

.dialog-body {
  padding: 20rpx 32rpx;
}

.dialog-desc {
  font-size: 26rpx;
  color: #666;
  line-height: 1.6;
  margin-bottom: 32rpx;
}

.link {
  color: #07c160;
  text-decoration: underline;
}

.check-row {
  display: flex;
  align-items: center;
}

.checkbox {
  width: 36rpx;
  height: 36rpx;
  border: 2rpx solid #ccc;
  border-radius: 8rpx;
  margin-right: 16rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24rpx;
  color: #fff;
}

.checkbox.checked {
  background: #07c160;
  border-color: #07c160;
}

.check-text {
  font-size: 26rpx;
  color: #333;
}

.dialog-footer {
  display: flex;
  border-top: 1rpx solid #eee;
}

.btn-disagree {
  flex: 1;
  background: #f7f7f7;
  font-size: 30rpx;
  border-radius: 0;
  border: none;
}

.btn-agree {
  flex: 1;
  background: #07c160;
  color: #fff;
  font-size: 30rpx;
  border-radius: 0;
  border: none;
  border-left: 1rpx solid #eee;
}

4.2 弹窗时机

时机是否推荐说明
首次打开小程序✅ 推荐标准做法,在onLaunch中检查
用户首次点击敏感功能时✅ 推荐延迟到实际需要时弹,体验更好
每次打开都弹❌ 不推荐用户已同意后无需重复弹
隐私协议更新后✅ 推荐检测版本变化,重新弹出

javascript复制

// app.js - 标准启动流程
App({
  onLaunch() {
    this.checkPrivacyAndInit()
  },

  checkPrivacyAndInit() {
    const agreedVersion = wx.getStorageSync('privacy_version')
    const currentVersion = '2026-06' // 隐私协议版本号

    if (wx.getPrivacySetting) {
      wx.getPrivacySetting({
        success: (res) => {
          // 需要授权 或 协议版本更新
          if (res.needAuthorization || agreedVersion !== currentVersion) {
            this.globalData.showPrivacyPopup = true
          } else {
            this.initApp()
          }
        },
        fail: () => {
          // 低版本基础库
          this.initApp()
        }
      })
    } else {
      this.initApp()
    }
  },

  initApp() {
    // 初始化应用逻辑
  },

  onPrivacyAgree() {
    this.globalData.showPrivacyPopup = false
    this.globalData.privacyAuthorized = true
    this.initApp()
  },

  onPrivacyDisagree() {
    // 用户拒绝,提供降级体验
    this.globalData.privacyAuthorized = false
    this.initApp()
  }
})

五、授权拒绝后的降级方案

核心原则:用户拒绝授权后,小程序的基本功能仍应可用,只是涉及敏感数据的功能不可用。

javascript复制

// utils/authFallback.js

/**
 * 授权降级方案
 * 根据用户授权状态提供对应的功能集合
 */
const FALLBACK_CONFIG = {
  // 拒绝位置授权 → 手动选择城市
  location: {
    degraded: true,
    fallback: 'manual_city_select',
    message: '开启定位可自动发现附近商家,您也可以手动选择城市'
  },

  // 拒绝手机号授权 → 微信一键登录
  phone: {
    degraded: true,
    fallback: 'wechat_auto_login',
    message: '使用微信登录即可体验全部功能'
  },

  // 拒绝相册授权 → 仅使用相机
  album: {
    degraded: true,
    fallback: 'camera_only',
    message: '您可以使用相机直接拍照上传'
  },

  // 拒绝用户信息 → 使用匿名头像
  userInfo: {
    degraded: true,
    fallback: 'anonymous_profile',
    message: '将使用默认头像和昵称'
  }
}

/**
 * 通用敏感接口调用封装
 * 自动处理拒绝后的降级
 */
function callWithFallback(apiName, apiFunc, fallbackFunc) {
  return new Promise((resolve) => {
    privacyGuard.ensure(() => {
      apiFunc()
        .then(resolve)
        .catch((err) => {
          console.warn(`${apiName} 调用失败:`, err)

          const config = FALLBACK_CONFIG[apiName]
          if (config && fallbackFunc) {
            wx.showToast({
              title: config.message,
              icon: 'none',
              duration: 3000
            })
            resolve(fallbackFunc())
          } else {
            resolve(null)
          }
        })
    })
  })
}

// 使用示例
Page({
  async onLoad() {
    // 获取位置:失败则降级为手动选择
    const location = await callWithFallback(
      'location',
      () => wx.getLocation({ type: 'gcj02' }),
      () => ({ latitude: 39.9, longitude: 116.4, manual: true })
    )
  }
})

六、个保法/个人信息保护法对小程序的影响

6.1 用户数据权利

根据《个人信息保护法》,用户享有的权利及其在小程序中的实现:

权利小程序实现方式
知情权隐私弹窗清晰告知收集内容和目的
同意权隐私弹窗需用户主动同意
撤回同意提供关闭入口,撤回后停止收集
更正权允许用户修改个人信息
删除权提供注销/删除数据入口
数据导出提供个人信息副本下载

6.2 用户数据删除权实现

javascript复制

// services/userDataService.js
class UserDataService {
  /**
   * 用户注销账号 - 删除所有个人数据
   * 注意:订单记录等法定保留数据需脱敏后保留
   */
  async deleteUserData(userId) {
    // 1. 订单数据脱敏(法律要求保留交易记录)
    await db.query(`
      UPDATE orders
      SET user_name = '已注销用户',
          phone = '***',
          address = '***'
      WHERE user_id = ?
    `, [userId])

    // 2. 删除用户画像和行为数据
    await db.query(`
      DELETE FROM user_profiles WHERE user_id = ?
    `, [userId])
    await db.query(`
      DELETE FROM user_behavior_log WHERE user_id = ?
    `, [userId])
    await db.query(`
      DELETE FROM user_preferences WHERE user_id = ?
    `, [userId])

    // 3. 删除微信openid关联
    await db.query(`
      UPDATE users
      SET openid = NULL,
          phone = NULL,
          nickname = '已注销用户',
          avatar = '',
          status = 'deleted',
          deleted_at = NOW()
      WHERE id = ?
    `, [userId])

    // 4. 清除缓存
    await redis.del(`user:${userId}:profile`)
    await redis.del(`user:${userId}:preferences`)
  }

  /**
   * 导出用户数据
   * 返回用户所有个人信息的JSON文件
   */
  async exportUserData(userId) {
    const [user] = await db.query(`
      SELECT id, nickname, phone, created_at
      FROM users WHERE id = ?
    `, [userId])

    const [preferences] = await db.query(`
      SELECT * FROM user_preferences WHERE user_id = ?
    `, [userId])

    const [orders] = await db.query(`
      SELECT order_id, amount, status, created_at
      FROM orders WHERE user_id = ?
      ORDER BY created_at DESC
      LIMIT 100
    `, [userId])

    return {
      exportTime: new Date().toISOString(),
      user,
      preferences,
      recentOrders: orders
    }
  }
}

module.exports = new UserDataService()

七、隐私合规自查清单

以下清单可用于上线前的逐项检查:

markdown复制

## 隐私合规自查清单

### 隐私保护指引
- [ ] 已在小程序管理后台配置《隐私保护指引》
- [ ] 指引中列出的收集类型与代码中实际使用的接口一致
- [ ] 指引通过审核(状态为"已通过")
- [ ] 指引中包含数据存储位置和保留期限
- [ ] 指引中包含第三方共享说明(如有)

### 隐私弹窗
- [ ] 首次打开小程序时弹出隐私协议
- [ ] 弹窗包含《隐私保护指引》可点击链接
- [ ] 用户未勾选同意时不能点"同意并继续"
- [ ] 同意状态本地持久化,不会每次都弹
- [ ] 隐私协议更新后能重新弹出
- [ ] 同一会话内最多弹2次

### 敏感接口
- [ ] 所有隐私接口调用前检查授权状态
- [ ] 使用 wx.requirePrivacyAuthorize 或 wx.getPrivacySetting 检查
- [ ] 授权拒绝后有降级方案
- [ ] 不在用户授权前静默收集数据

### 数据安全
- [ ] 敏感数据(手机号、地址等)加密存储
- [ ] 网络传输使用HTTPS
- [ ] 不在日志中打印用户敏感信息
- [ ] access_token等密钥使用环境变量管理

### 用户权利
- [ ] 提供账号注销/删除功能
- [ ] 注销时删除或脱敏用户数据
- [ ] 提供用户数据导出功能(可选但推荐)
- [ ] 提供撤回授权的入口

### 专项检查
- [ ] 不在后台静默获取地理位置
- [ ] 不使用剪贴板读取用户敏感数据
- [ ] 不向第三方共享用户原始数据(如需共享需单独告知)
- [ ] 广告组件合规配置(使用微信官方广告组件)
- [ ] WebView中加载的H5页面也有隐私合规
- [ ] 插件引入的隐私接口在指引中声明

八、隐私协议模板

以下是一个通用的隐私协议模板,适用于大多数小程序。请根据实际业务情况修改。

markdown复制

# 隐私保护指引

更新日期:2026年6月19日
生效日期:2026年6月19日

## 1. 我们如何收集和使用您的个人信息

### 1.1 小程序运行必需的信息
- **微信授权登录**:获取您的微信OpenID,用于创建和关联您的账号。
- **设备信息**:设备型号、操作系统版本,用于适配显示和排查故障。

### 1.2 为提供特定功能而收集的信息
- **地理位置**(仅在您使用LBS相关功能时):获取您的位置信息,用于推荐附近服务。
- **手机号码**(仅在您手动授权时):用于账号安全验证和重要通知。
- **相册/摄像头**(仅在您上传图片时):用于头像设置或内容发布。

### 1.3 我们不会收集的信息
- 我们不会收集您的聊天记录、通讯录等与小程序服务无关的信息。

## 2. 信息的存储
- 您的个人信息存储在中华人民共和国境内的服务器上。
- 我们将仅在实现服务目的所必需的合理期限内保留您的个人信息。

## 3. 信息共享
- 我们不会向第三方出售您的个人信息。
- 为提供服务所必需,我们可能与关联公司共享部分信息,且受同等隐私保护约束。

## 4. 您的权利
- **查询和更正**:您可以在小程序"个人中心"查看和修改您的信息。
- **删除**:您可以在"设置→账号与安全→注销账号"中删除您的个人信息。
- **撤回同意**:您可以在微信设置中关闭对应权限。
- **获取副本**:您可以联系我们获取您的个人信息副本。

## 5. 联系我们
如有隐私相关疑问,请联系:privacy@example.com

九、总结

2026年小程序隐私合规的要点:

  1. 指引先行:隐私保护指引必须完整配置,且与代码一致
  2. 弹窗必接:微信隐私协议弹窗是强制机制,不接就限制接口
  3. 最小收集:只收集业务必需的数据,非必要不收集
  4. 安全存储:敏感数据加密存储,传输全程HTTPS
  5. 用户主导:提供删除、导出、撤回授权等权利通道
  6. 持续自查:每次更新隐私接口后重新走一遍自查清单

隐私合规不是一次性工作,而是贯穿小程序整个生命周期的持续义务。建议将自查清单集成到CI/CD流程中,每次发布前自动检查。

javascript复制

// build-time 检查脚本(集成到 CI/CD)
const fs = require('fs')
const path = require('path')

function checkPrivacyApiUsage() {
  const PRIVACY_APIS = [
    'getLocation', 'chooseLocation', 'getUserInfo', 'getUserProfile',
    'chooseImage', 'chooseMedia', 'getPhoneNumber', 'chooseAddress',
    'getClipboardData', 'chooseVideo', 'saveImageToPhotosAlbum',
    'startRecord', 'startBluetoothDevicesDiscovery'
  ]

  // 扫描所有JS文件
  const jsFiles = scanFiles(path.resolve('./miniprogram'), '.js')

  const violations = []
  for (const file of jsFiles) {
    const content = fs.readFileSync(file, 'utf8')
    for (const api of PRIVACY_APIS) {
      if (content.includes(`wx.${api}`) || content.includes(`.${api}(`)) {
        // 检查是否包裹在 privacyGuard.ensure 中
        if (!content.includes('privacyGuard')) {
          violations.push(`${file}: 使用了隐私接口 wx.${api} 但未包裹隐私守卫`)
        }
      }
    }
  }

  if (violations.length > 0) {
    console.error('⚠️ 隐私合规检查未通过:')
    violations.forEach(v => console.error(`  - ${v}`))
    process.exit(1) // CI构建失败
  } else {
    console.log('✅ 隐私合规检查通过')
  }
}
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐