微信小程序手机号解密:Java后端AES-CBC完整实现
2026/9/13 12:47:50 网站建设 项目流程

简介:本资源是一套完整的微信小程序用户信息获取与后端解密实战方案,面向Java后端开发者及小程序全栈工程师,解决小程序中安全获取用户手机号、openid、session_key及基础信息等核心鉴权问题。压缩包共30个文件,包含5个JS前端逻辑文件、6个JSON配置文件、3个Java源码文件、3个Class编译文件、3个JAR依赖库(含commons-codec-1.8、bcprov-jdk15-136等关键加解密组件),以及WXML/WXSS页面结构与样式文件,整体1.79MB,结构清晰,前后端分离明确,便于快速集成到现有项目。已有8080人学习下载,资源源自真实商用项目剥离,经实测可用,附带完整Java解密工具类与小程序端调用示例,涵盖从code换取session_key、加密数据解密、敏感信息安全传输等全流程代码与配置要点,特别适合需要落地微信授权体系的中高级开发者参考与复用。

1. 微信小程序获取手机号不是“点一下就拿到”,而是「前端授权 + 后端解密 + 联动 session_key」的闭环流程

很多开发者第一次在微信小程序里调用getPhoneNumber时,以为只要用户点授权按钮,后端就能直接拿到明文手机号——结果发现返回的是加密的encryptedDataiv,而解密失败、报错errcode: -41003invalid signature的情况比比皆是。根本原因在于:微信不直接下发手机号,而是要求服务端用session_key对密文做 AES-128-CBC 解密,且该session_key本身需通过code换取,有效期仅 2 小时,不可复用。这个流程天然耦合了openid(用户唯一标识)、session_key(会话密钥)和encryptedData(加密数据)三要素,缺一不可。本文面向已接入微信登录但卡在「手机号解密」环节的 Java 后端开发者,聚焦真实生产环境中的可落地方案:从wx.login()换取code开始,到 Java 侧完整解析出phoneNumber,覆盖密钥时效性管理、PKCS#7 填充处理、签名验签逻辑、以及常见40001/40003错误的定位路径。不讲概念复述,只写你部署时真正要写的代码、要配的参数、要查的日志位置。

2. 用 Java 实现微信小程序手机号解密:从 code 换取 session_key 到 AES-CBC 解密全流程

微信小程序获取手机号的完整链路分为两个强依赖阶段:第一阶段是通过wx.login()获取临时登录凭证code,并用该code向微信接口https://api.weixin.qq.com/sns/jscode2session换取openidsession_key;第二阶段是将用户点击button open-type="getPhoneNumber"后触发的encryptedDataiv,用上一步获得的session_key进行 AES-128-CBC 解密。这两个阶段必须串行执行,且session_key不能跨用户、跨请求复用。Java 侧实现时,核心难点不在加解密算法本身,而在于微信返回的session_key是 base64 编码字符串,需转为 byte[] 作为密钥;encryptedData同样是 base64 编码,需先解码再参与解密;解密后数据需按 PKCS#7 规则去除填充字节,并校验watermark.timestamp防重放。下面分步展开。

2.1 用 code 换取 openid 和 session_key:HTTP 请求构造与响应解析

微信官方接口sns/jscode2session要求以 GET 方式传入appidsecretjs_code(即前端wx.login()返回的code)和grant_type=authorization_code。注意:secret是小程序后台配置的 AppSecret,绝不可硬编码在前端或日志中,应存于配置中心或环境变量。Java 中推荐使用OkHttpClientRestTemplate发起请求,避免手动拼接 URL 导致编码错误。

// 使用 OkHttp 构造请求(推荐,线程安全且支持连接池) OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + URLEncoder.encode(appId, "UTF-8") + "&secret=" + URLEncoder.encode(appSecret, "UTF-8") + "&js_code=" + URLEncoder.encode(jsCode, "UTF-8") + "&grant_type=authorization_code"; Request request = new Request.Builder().url(url).get().build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new RuntimeException("微信 session 接口返回非 200:" + response.code()); } String responseBody = response.body().string(); JSONObject json = JSON.parseObject(responseBody); // 关键字段提取:openid 和 session_key 必须同时存在 String openid = json.getString("openid"); String sessionKey = json.getString("session_key"); // errcode 为 0 表示成功,非 0 需按微信文档查错(如 40001=secret 错误,40029=code 过期) int errcode = json.getIntValue("errcode"); if (errcode != 0) { throw new RuntimeException("微信 session 接口错误码:" + errcode + ",msg:" + json.getString("errmsg")); } // 注意:session_key 是 base64 字符串,后续解密需转为 byte[] byte[] sessionKeyBytes = Base64.getDecoder().decode(sessionKey); return new SessionResult(openid, sessionKeyBytes, json.getLongValue("expires_in")); }

提示:expires_in字段表示session_key有效期(单位秒),微信返回值固定为 7200(2 小时),但实际应以该值为准做本地缓存过期控制,而非硬写 2 小时。openid是用户在此小程序下的唯一标识,可用于关联数据库用户表。

2.2 解密 encryptedData:AES-128-CBC + PKCS#7 填充的 Java 实现细节

微信对encryptedData的加密方式为 AES-128-CBC,密钥为session_key(16 字节),初始向量iv由前端提供(也是 base64 编码)。解密前必须确保:①session_key已转为 16 字节byte[];②encryptedDataiv均已完成 base64 解码;③ 使用PKCS5Padding(Java 中PKCS5Padding实际等价于PKCS7Padding,因 AES 块长为 128 位);④ 解密后需手动移除 PKCS#7 填充字节。以下为完整解密方法:

public static String decryptPhoneNumber(String encryptedData, String iv, byte[] sessionKey) throws Exception { // 1. base64 解码 encryptedData 和 iv byte[] dataBytes = Base64.getDecoder().decode(encryptedData); byte[] ivBytes = Base64.getDecoder().decode(iv); // 2. 构建 SecretKeySpec(AES-128 要求密钥长度为 16 字节) SecretKeySpec keySpec = new SecretKeySpec(sessionKey, "AES"); // 3. 初始化 Cipher,指定 AES/CBC/PKCS5Padding Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); IvParameterSpec ivSpec = new IvParameterSpec(ivBytes); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); // 4. 执行解密 byte[] result = cipher.doFinal(dataBytes); // 5. 移除 PKCS#7 填充(填充字节数 = 最后一个字节的值) int pad = result[result.length - 1] & 0xFF; if (pad < 1 || pad > 16) { throw new RuntimeException("PKCS#7 填充格式错误,pad=" + pad); } byte[] cleaned = new byte[result.length - pad]; System.arraycopy(result, 0, cleaned, 0, cleaned.length); // 6. 解析 JSON 并提取 phoneNumber String jsonStr = new String(cleaned, StandardCharsets.UTF_8); JSONObject decryptedJson = JSON.parseObject(jsonStr); // 校验 watermark.appid 是否匹配(防 token 被盗用) JSONObject watermark = decryptedJson.getJSONObject("watermark"); if (watermark == null || !appId.equals(watermark.getString("appid"))) { throw new RuntimeException("watermark appid 不匹配"); } return decryptedJson.getString("phoneNumber"); }

注意:Cipher.getInstance("AES/CBC/PKCS5Padding")是标准写法,不要写成PKCS7Padding(Java 不识别该字符串);pad的计算必须用& 0xFF防止负数;watermark.timestamp可选校验(如要求 5 分钟内有效),但appid校验是强制项,否则攻击者可复用其他小程序的密文。

2.3 完整调用链封装:从 controller 入参到手机号返回

将上述两步封装为一个原子服务,接收前端传来的codeencryptedDataiv,返回解密后的手机号。关键点在于:codesession_key和解密必须在同一请求内完成,禁止将session_key存入 Redis 后异步解密,因为code一次性且session_key有时效性

@PostMapping("/api/wx/phone") public Result<String> getPhoneNumber(@RequestBody PhoneRequest request) { try { // Step 1: 用 code 换取 session_key 和 openid SessionResult session = wechatService.getSessionKey(request.getCode()); // Step 2: 用 session_key 解密 String phoneNumber = AesUtil.decryptPhoneNumber( request.getEncryptedData(), request.getIv(), session.getSessionKeyBytes() ); // Step 3: (可选)根据 openid 查询或创建用户,并绑定手机号 userService.bindPhone(session.getOpenid(), phoneNumber); return Result.success(phoneNumber); } catch (Exception e) { log.error("解密手机号失败", e); return Result.fail("获取手机号失败:" + e.getMessage()); } } // 请求体定义 @Data public static class PhoneRequest { private String code; // wx.login() 返回 private String encryptedData; // button getPhoneNumber 返回 private String iv; // button getPhoneNumber 返回 }

提示:SessionResult类应包含openidsessionKeyBytesexpiresIn,便于后续扩展缓存逻辑;userService.bindPhone()是业务层操作,与解密无关,此处仅为示意。

3. 微信小程序获取 openid 和 session_key 的 3 个必调参数与 2 类典型错误排查

sns/jscode2session接口虽简单,但参数错误会导致errcode固定返回40001invalid credential)或40029invalid code),这类错误不报具体原因,需结合参数含义逐项核对。以下列出三个必须严格校验的参数及其常见陷阱,并给出对应错误码的快速定位表。

3.1 appid 和 secret:配置一致性与权限范围检查

appid必须与小程序后台「开发管理」页显示的 AppID 完全一致(含大小写),secret必须是同一小程序的 AppSecret,且未被重置。常见错误包括:① 复制secret时末尾多了一个空格;② 在测试环境用了生产环境的secret;③ 小程序已迁移主体,但secret未同步更新。验证方法:登录微信公众平台 → 「开发管理」→ 「开发设置」→ 查看「AppID」和「AppSecret」,确认无隐藏字符。

参数正确示例错误示例错误表现
appidwx1234567890abcdefWX1234567890ABCDEF(大写)errcode: 40001
secreta1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6\n(含换行)errcode: 40001

3.2 js_code:时效性与单次性约束

js_codewx.login()成功回调返回的字符串,有效期为5 分钟,且只能使用一次。若前端未及时将code传给后端,或后端重复使用同一code请求,均会返回errcode: 40029。排查时需确认:① 前端wx.login()调用后是否立即this.setData({ code: res.code })并触发后续请求;② 后端日志中js_code是否与前端传入完全一致(无截断、无 URL 编码残留);③ 是否存在并发请求导致code被多次提交。

注意:js_code不是openid,也不是unionid,它只是一个临时票据,无业务含义,仅用于换取session_key

3.3 grant_type:固定值 authorization_code,不可省略或拼错

grant_type参数必须显式传入authorization_code,且值为字符串字面量,不能是codeauth_code或空。微信接口对此参数校验严格,缺失或错误会导致40002invalid grant_type)错误。常见疏漏是开发者误以为grant_type是可选参数,或在构建 URL 时漏掉该参数。

3.3.1 错误码速查表:从响应体快速定位问题根源
errcode含义常见原因日志检查点
0成功openidsession_key字段存在
40001invalid credentialappidsecret错误检查配置文件中appid/secret是否与后台一致,有无空格
40029invalid codejs_code过期、已使用、或为空查看前端wx.login()调用时间与后端收到code时间差,确认是否超 5 分钟
40002invalid grant_typegrant_type缺失或值错误检查 URL 中是否包含&grant_type=authorization_code
40003invalid openidcode对应用户不存在(极罕见)确认小程序已发布,且用户确实在当前版本中登录过

4. 微信小程序手机号解密的进阶技巧:session_key 缓存策略与 watermark 时间戳校验

生产环境中,高频调用sns/jscode2session接口不仅增加网络延迟,还可能触发微信的频率限制(目前未公开阈值,但实测单 IP 每分钟超 100 次易被限流)。更优方案是将session_keyopenid组合作为缓存 key,设置合理过期时间。同时,watermark.timestamp字段提供了防重放能力,应在解密后校验其与当前时间的偏差,避免密文被截获后重复使用。

4.1 基于 Redis 的 session_key 缓存设计:key 结构与过期策略

session_key的生命周期由expires_in决定(微信固定返回 7200 秒),但实际缓存时间应略短于该值(如设为 7000 秒),避免因网络延迟导致缓存未过期但session_key已失效。缓存 key 设计为wx:session:${appid}:${openid},value 存储session_key的 base64 字符串(便于调试)及生成时间戳。

// 缓存写入(在 getSessionKey 方法中) String cacheKey = "wx:session:" + appId + ":" + openid; String cacheValue = sessionKeyBase64 + "|" + System.currentTimeMillis(); redisTemplate.opsForValue().set(cacheKey, cacheValue, 7000, TimeUnit.SECONDS); // 缓存读取(优先查缓存,命中则跳过 HTTP 请求) String cacheKey = "wx:session:" + appId + ":" + openid; String cacheValue = redisTemplate.opsForValue().get(cacheKey); if (cacheValue != null && !cacheValue.isEmpty()) { String[] parts = cacheValue.split("\\|"); if (parts.length == 2) { long genTime = Long.parseLong(parts[1]); if (System.currentTimeMillis() - genTime < 7000 * 1000L) { byte[] sessionKeyBytes = Base64.getDecoder().decode(parts[0]); return new SessionResult(openid, sessionKeyBytes, 7200L); } } }

提示:session_keyopenid强绑定,不同用户不可共享;缓存 value 中拼接时间戳,是为了在过期判断时更精确(Redis TTL 有精度误差)。

4.2 watermark.timestamp 校验:5 分钟窗口防重放的具体实现

encryptedData解密后的 JSON 中,watermark对象包含appidtimestamp(单位毫秒)。timestamp表示微信服务器生成密文的时间,校验逻辑为:Math.abs(System.currentTimeMillis() - watermarkTimestamp) <= 300000(5 分钟)。该检查应在decryptPhoneNumber方法内部完成,且必须在appid校验之后、phoneNumber提取之前执行。

// 在 decryptPhoneNumber 方法中,解析完 decryptedJson 后插入: JSONObject watermark = decryptedJson.getJSONObject("watermark"); if (watermark == null) { throw new RuntimeException("watermark 不存在"); } long watermarkTimestamp = watermark.getLongValue("timestamp"); long now = System.currentTimeMillis(); if (Math.abs(now - watermarkTimestamp) > 300000L) { // 5 分钟 throw new RuntimeException("watermark timestamp 超时,当前时间:" + now + ",密文时间:" + watermarkTimestamp); }

注意:timestamp是微信服务器时间,与你的服务器时间可能存在几秒偏差,因此校验窗口应设为 5 分钟而非 1 分钟;若业务对安全性要求极高,可同步 NTP 时间或使用微信服务器时间戳(需额外调用https://api.weixin.qq.com/cgi-bin/getcallbackip获取可信时间源)。

4.3 生产环境日志埋点建议:关键节点打点提升排错效率

getSessionKeydecryptPhoneNumber方法入口及异常处添加结构化日志,字段至少包含traceIdcode(脱敏前 4 位)、openid(脱敏中间 8 位)、errcode(如有)。例如:

[WX-LOGIN] traceId=abc123 code=wx_4567 openid=wx_...efgh90 errcode=0 [WX-PHONE-DECRYPT] traceId=abc123 openid=wx_...efgh90 ivLen=24 dataLen=324 status=success [WX-ERROR] traceId=def456 code=wx_1234 errcode=40029 errmsg="invalid code"

此类日志可直接对接 ELK 或 Grafana,当出现批量失败时,通过errcode聚合即可快速定位是code问题还是secret问题,无需翻查原始请求体。

5. 微信小程序获取手机号的边界场景处理:多端登录、UnionID 关联与敏感信息脱敏

实际项目中,用户可能在多个平台(公众号、小程序、APP)使用同一微信账号,此时openid各端不同,但unionid唯一。若业务需打通多端用户体系,必须在获取session_key后,通过https://api.weixin.qq.com/cgi-bin/user/info接口(需公众号 access_token)或https://api.weixin.qq.com/sns/userinfo(需网页授权)获取unionid。此外,手机号属于敏感个人信息,Java 侧存储前必须脱敏(如保留前 3 后 4 位),且日志中严禁打印明文。

5.1 UnionID 获取路径:小程序侧无法直接获取,需后端联动公众号

小程序自身无法直接获取unionid,因为unionid生成条件是用户在同一微信开放平台账号下关注过公众号或使用过 APP。若你的小程序已绑定到微信开放平台,且用户已关注同主体公众号,则调用sns/jscode2session返回的 JSON 中会包含unionid字段(微信文档明确说明:“如果用户已关注公众号,且公众号与小程序同主体,则返回 unionid”)。但该行为不稳定,更可靠的方式是:在公众号后台开通“用户信息授权”,引导用户在公众号内完成一次授权,后端用该用户的openid换取access_token,再调用cgi-bin/user/info接口获取unionid

// 伪代码:通过公众号 access_token 获取用户信息(含 unionid) String url = "https://api.weixin.qq.com/cgi-bin/user/info" + "?access_token=" + accessToken + "&openid=" + officialAccountOpenid + "&lang=zh_CN"; // 返回 JSON 中的 unionid 字段即为跨平台唯一 ID

提示:unionid是打通多端的核心,但获取成本高,建议在用户首次注册时完成绑定,而非每次登录都拉取。

5.2 手机号存储与日志脱敏:符合《个人信息保护法》的 Java 实现

根据中国《个人信息保护法》,手机号属于敏感个人信息,存储和传输必须加密,日志中不得明文记录。Java 中推荐使用BCrypt单向哈希存储(不可逆),或AES-GCM加密存储(可逆,需密钥管理)。日志脱敏则用正则替换:

// 日志脱敏工具类 public static String maskPhoneNumber(String phone) { if (phone == null || phone.length() < 11) return phone; return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"); } // 使用示例 log.info("绑定手机号:{}", maskPhoneNumber(phoneNumber));

5.3 前端 button 的正确写法与用户体验优化

小程序端button必须设置open-type="getPhoneNumber"且绑定bindgetphonenumber事件,encryptedDataiv由微信 SDK 自动注入,无需手动拼接。为避免用户误点后无反馈,建议在bindgetphonenumber回调中立即展示 loading,并在后端返回成功后再跳转或刷新页面。

<!-- wxml --> <button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber"> 获取手机号 </button>
// js onGetPhoneNumber(e) { if (e.detail.errMsg === 'getPhoneNumber:ok') { wx.showLoading({ title: '正在验证...' }); wx.request({ url: '/api/wx/phone', method: 'POST', data: { code: this.data.code, encryptedData: e.detail.encryptedData, iv: e.detail.iv }, success: res => { wx.hideLoading(); if (res.data.code === 0) { wx.showToast({ title: '绑定成功' }); } } }); } }

注意:bindgetphonenumber事件仅在真机调试或体验版/正式版生效,开发者工具中点击无效;e.detail在用户拒绝授权时为undefined,需判空处理。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询