微信小程序登录与手机号获取:前端到后端全链路实战
1. 先搞明白微信小程序的信息获取链路到底长什么样做小程序开发最绕不开的一件事就是怎么拿到微信用户的身份信息和手机号。不管你是要做会员登录、电商下单、还是做用户画像第一步都是先解决这个用户是谁的问题。很多新手一上来就去看wx.getUserInfo、wx.getPhoneNumber这些 API结果发现要么拿不到数据要么拿到的是一堆nickName: 微信用户的默认值直接卡在第一步。其实微信的授权体系这些年改得挺狠的。早期版本里用户一进来弹个授权框你就能拿到完整的昵称头像甚至能通过encryptedData解密出手机号。但从 2021 年起微信把这条路基本堵死了——昵称头像改成了头像昵称填写能力手机号改成了手机号快速验证组件底层逻辑全部切换成 code 换数据的模式。也就是说小程序端不再直接给你敏感信息所有数据都要通过 code 传到你的后端由后端调用微信服务端接口换取。这篇文章就用一个前后端分离的完整项目来演示整套流程前端用原生微信小程序后端用 Spring Boot把登录、头像昵称获取、手机号获取三条链路全部打通。核心代码可以直接抄重点是让你明白每一步为什么这么做踩到坑了怎么排查。适合正准备开发小程序、或者想把旧授权逻辑升级到新版的朋友参考。2. 登录态与数据获取方案选型解析2.1 登录态openid、session_key、token 三者的关系先说登录。微信小程序里的登录机制和传统 Web 登录不太一样它没有密码也没有短信验证码靠的是微信自己的一套静默授权流程。整个链路里的核心角色有三个code临时凭证由小程序端调用wx.login()获取有效期大概 5 分钟只能用一次。它的作用就是递给后端让后端去微信那边换更重要的数据。openid用户的唯一标识同一个微信用户在同一小程序下的 openid 是固定的。它是你在自己用户表里建用户、绑定业务数据的主键。session_key会话密钥用来解密敏感数据比如老版手机号解密、用户手机号解密。注意它不能下发到小程序前端只能存在后端否则会有安全风险。至于登录成功之后给前端返回什么一般是你自己签发的 token比如 JWT。因为前端后续的每次请求后端都需要知道这个请求是哪个用户在发总不能每次都让小程序调一次wx.login。所以标准流程是小程序wx.login()拿 code → 传给后端 → 后端用 code 调微信接口换openidsession_key→ 后端查库/建用户 → 签发 token → 返回给前端 → 前端存起来每次请求带上。2.2 为什么 getUserInfo 被废弃了现在用什么2021 年之前用户进入小程序弹出授权你就能拿到昵称头像甚至能通过wx.getUserInfoencryptedData解密出手机号。这套方案对开发者很爽但对用户不太友好——很多人看到授权框就反感而且很多小程序拿了手机号之后乱打电话发短信搞得微信不得不严管。现在的方案是头像昵称用户主动触发。使用button open-typechooseAvatar让用户选择微信头像使用input typenickname让用户填写/选择昵称。微信只负责提供入口用户有完全的选择权和控制权。手机号使用button open-typegetPhoneNumber用户点击后弹窗确认确认后前端拿到一个 code传给后端后端调用微信接口换取手机号。这套方案的好处是数据更合规坏处是开发者不能静默拿到手机号了必须用户主动点一次按钮。但没办法微信就是要逼你把收集用户数据这件事做得明明白白。2.3 前后端分离为什么是小程序开发的默认姿势市面上小程序项目基本都是前后端分离的。前端是微信小程序原生代码或者 Taro / uni-app 这类跨端框架跑在微信客户端里后端是独立的 API 服务可以放服务器上Spring Boot、Node.js、Go 都行。两者通过 HTTPS 接口通信。为什么必须分因为微信的敏感数据交换必须由后端完成。code 换 openid 需要用到appSecret这东西如果放在小程序前端代码里就等于把钥匙交出去了——任何人都能通过反编译工具看你的源码把你的 appSecret 扒出来然后伪装成你的小程序调微信接口。所以前端永远只做一件事把 code 传给后端剩下的活交给后端干。这种架构天然要求前后端分离。还有一点小程序的请求域名必须在微信公众平台配置白名单且必须是 HTTPS。这也是为什么本地开发时经常遇到request 域名不在合法域名列表的报错——解决办法要么配域名要么在开发者工具里关掉域名校验但真机预览必须配好域名。3. 基础准备与开发环境搭建3.1 注册小程序账号拿到 AppID 和 AppSecret做微信小程序开发第一步是去微信公众平台注册小程序账号。注册完成后在开发管理 - 开发设置里能看到两个关键字段AppID小程序的唯一标识前端代码里配置公开没关系。AppSecret调微信服务端接口的密钥必须严格保密只放在后端。需要注意手机号快速验证组件有一个硬性条件小程序主体必须是企业、个体工商户或政府等认证主体。个人主体的小程序无法申请该能力调用接口时微信会直接报无权限。如果你的项目还在规划阶段这一点务必提前确认不然辛苦开发完发现手机号功能根本没法用返工成本很高。3.2 配置服务器域名小程序前端调后端的接口域名必须在开发管理 - 开发设置 - 服务器域名里面配置好。具体配置项配置项作用要求request 合法域名前端 wx.request 的请求地址必须 HTTPS不能带路径支持端口downloadFile 合法域名前端 wx.downloadFile 下载文件地址必须 HTTPSuploadFile 合法域名前端 wx.uploadFile 上传文件地址必须 HTTPS开发阶段可以在微信开发者工具里勾选不校验合法域名方便本地调试。但真机预览和上线发布时域名必须真实配置且备案否则直接请求失败。3.3 后端工程基础骨架后端我这边用 Spring Boot主要是生态成熟、社区资料多你搜任何问题基本都能找到答案。项目结构大概长这样src/main/java/com/example/demo ├── controller │ └── AuthController.java // 登录、手机号获取接口 ├── service │ ├── WxApiClient.java // 封装微信服务端接口调用 │ └── UserService.java // 用户注册/查询逻辑 ├── entity │ └── User.java // 用户实体 ├── mapper │ └── UserMapper.java // 数据库操作或用MyBatis-Plus └── config └── WebMvcConfig.java // 拦截器、跨域配置数据库表设计也不复杂最小的用户表就这几个字段id BIGINT 主键 openid VARCHAR(64) 微信用户唯一标识唯一索引 nickname VARCHAR(64) 用户昵称 avatar_url VARCHAR(512) 用户头像地址 phone VARCHAR(20) 手机号可为空 create_time DATETIME 注册时间 update_time DATETIME 更新时间这里有个经验手机号字段不要一味追求一开始就补齐很多用户点进来就是想浏览一下并不想马上把手机号给你。所以我的做法是先登录、后补手机号——用户进入小程序先静默登录拿 openid 建立用户身份等他要下单、要领取权益的时候再弹手机号授权。这样既合规又不会把用户挡在第一道门外。4. 前端实现登录、头像昵称、手机号三件套4.1 静默登录wx.login 出 codewx.login是小程序里最基础的登录接口它不需要用户授权静默就能执行。代码如下// utils/auth.js function wxLogin() { return new Promise((resolve, reject) { wx.login({ success: (res) { if (res.code) { // 拿到临时凭证code resolve(res.code); } else { reject(new Error(wx.login 失败 res.errMsg)); } }, fail: (err) reject(err) }); }); } function silentLogin() { return wxLogin().then((code) { // 调后端登录接口换token return new Promise((resolve, reject) { wx.request({ url: https://your-api.com/api/auth/login, method: POST, data: { code }, success: (res) { if (res.statusCode 200 res.data.success) { const { token, userInfo } res.data.data; wx.setStorageSync(token, token); wx.setStorageSync(userInfo, userInfo); resolve(userInfo); } else { reject(new Error(res.data.message || 登录失败)); } }, fail: (err) reject(err) }); }); }); }页面onLoad时直接调silentLogin()用户无感完成身份建立这就是整套系统的地基。需要提醒一个新手常见坑wx.login获取的 code 是一次性的如果后端处理失败前端不能拿同一个 code 重试必须重新调一次wx.login拿新 code。4.2 头像昵称填写能力实战WXML 部分view classprofile-card button classavatar-wrapper open-typechooseAvatar bind:chooseavataronChooseAvatar image src{{avatarUrl || /assets/default-avatar.png}} modeaspectFill / /button input typenickname classnickname-input value{{nickName}} bind:inputonNicknameInput placeholder请输入昵称 / /viewJS 部分Page({ data: { avatarUrl: , nickName: }, onChooseAvatar(e) { // 用户授权微信头像后返回临时路径 const tempPath e.detail.avatarUrl; this.setData({ avatarUrl: tempPath }); // 注意这个tempPath是本地临时文件需要上传到自己的服务器 this.uploadAvatar(tempPath); }, onNicknameInput(e) { this.setData({ nickName: e.detail.value }); }, uploadAvatar(tempPath) { wx.uploadFile({ url: https://your-api.com/api/upload, filePath: tempPath, name: file, success: (res) { const result JSON.parse(res.data); if (result.success) { this.setData({ avatarUrl: result.data.url }); wx.setStorageSync(avatarUrl, result.data.url); } } }); } });这里有个细节e.detail.avatarUrl返回的是本地临时文件路径比如wxfile://tmp_xxx这个路径只在当前会话有效不能直接当成线上头像使用。你需要把图片上传到自己的服务器或云存储再拿固定 URL 存数据库。上传后记得处理一下防盗链微信对图片外链的校验还是挺严的。昵称输入框用typenickname微信会在键盘上方给出昵称快捷填入选项用户点一下就能自动填入微信昵称体验很顺。4.3 手机号快速验证组件落地手机号获取是整套流程里最核心、也最容易踩坑的部分。WXML 如下button classphone-btn open-typegetPhoneNumber bind:getphonenumberonGetPhoneNumber loading{{phoneLoading}} 获取手机号 /buttonJS 部分Page({ data: { phoneLoading: false }, async onGetPhoneNumber(e) { if (!e.detail.code) { // 用户点了拒绝或者授权失败 wx.showToast({ title: 需要授权才能获取手机号, icon: none }); return; } this.setData({ phoneLoading: true }); try { const res await new Promise((resolve, reject) { wx.request({ url: https://your-api.com/api/auth/phone, method: POST, header: { Authorization: Bearer wx.getStorageSync(token) }, data: { code: e.detail.code }, success: (res) { if (res.data.success) resolve(res.data.data); else reject(new Error(res.data.message)); }, fail: reject }); }); wx.showToast({ title: 手机号获取成功, icon: success }); this.setData({ phone: res.phoneNumber }); wx.setStorageSync(phone, res.phoneNumber); } catch (err) { wx.showToast({ title: err.message || 获取失败, icon: none }); } finally { this.setData({ phoneLoading: false }); } } });很多人会问为什么前端拿不到手机号只拿到一个 code这是微信故意设计的。手机号属于敏感个人信息如果直接把手机号返回给前端前端代码被反编译后就能批量偷取用户数据。所以微信只给一个一次性 code后端拿到 code 之后自己调微信服务端接口取手机号这个 code 有效期也很短并且只能换一次。4.4 三块串成完整登录流程一个典型的用户首次进入小程序流程应该是这样onLoad触发静默登录拿 openid 建立用户身份存 token。用户点击编辑资料弹出头像昵称编辑面板用户主动选择微信头像、填写昵称前端上传头像图片、提交昵称到后端。用户点击绑定手机号按钮弹出微信手机号授权弹窗确认后前端把 code 发给后端后端换到手机号返回并提示绑定成功。后续用户再次进入token 有效期内直接拉起用户信息不再重复登录。前端这里有个建议不要在一进页面就把头像昵称和手机号三个授权一起弹出来那样很容易引起用户反感。最优做法是区分必要授权和非必要授权头像昵称可以在用户准备下单、准备发布内容时再要求填写手机号则在支付前、领取优惠券前这种强场景再要。这个顺序直接影响用户的转化率别嫌啰嗦真的重要。5. 后端实现code2session、token 签发与手机号换取5.1 登录接口用 code 换 openid 和 session_key后端先封装一个微信 API 客户端专门处理与微信服务端的通信// WxApiClient.java Service public class WxApiClient { Value(${wx.appid}) private String appid; Value(${wx.secret}) private String secret; private final RestTemplate restTemplate new RestTemplate(); /** * 用 login code 换 openid 和 session_key */ public WxSession code2Session(String code) { String url https://api.weixin.qq.com/sns/jscode2session ?appid appid secret secret js_code code grant_typeauthorization_code; String response restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(response); // 微信返回 errcode 表示失败 if (json.containsKey(errcode) json.getIntValue(errcode) ! 0) { throw new BizException(微信登录失败 json.getString(errmsg)); } WxSession session new WxSession(); session.setOpenid(json.getString(openid)); session.setSessionKey(json.getString(session_key)); session.setUnionid(json.getString(unionid)); return session; } }然后在 AuthController 里写登录接口RestController RequestMapping(/api/auth) public class AuthController { Autowired private WxApiClient wxApiClient; Autowired private UserService userService; Autowired private JwtUtil jwtUtil; PostMapping(/login) public Result login(RequestBody LoginDTO dto) { // 1. 用 code 换微信会话 WxSession wxSession wxApiClient.code2Session(dto.getCode()); // 2. 查用户不存在则注册 User user userService.findOrCreate(wxSession.getOpenid()); // 3. 签发 JWT token String token jwtUtil.generateToken(user.getId(), user.getOpenid()); // 4. 返回给前端 LoginVO vo new LoginVO(); vo.setToken(token); vo.setUserInfo(user); return Result.success(vo); } }这里的重点是后端用自己的 appSecret 去调微信接口appSecret 全程不出服务器这是安全的底线。登录接口返回的 token我这边用 JWT原因很简单无状态、不需要存 Redis、跨服务认证方便。你如果系统里有 Spring Security直接用它的 JWT 支持也行。5.2 手机号获取接口用 code 换手机号登录之后用户点击手机号授权前端把e.detail.code传过来。后端收到后需要先拿到access_token再调用微信的手机号获取接口。先看 access_token 的获取和缓存// 在 WxApiClient 中新增 private String accessTokenCache; private long accessTokenExpireTime 0; public synchronized String getAccessToken() { long now System.currentTimeMillis(); // 提前5分钟过期防止临界区请求失败 if (accessTokenExpireTime - now 5 * 60 * 1000) { return accessTokenCache; } String url https://api.weixin.qq.com/cgi-bin/token ?grant_typeclient_credential appid appid secret secret; String response restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(response); if (json.containsKey(errcode) json.getIntValue(errcode) ! 0) { throw new BizException(获取access_token失败 json.getString(errmsg)); } accessTokenCache json.getString(access_token); accessTokenExpireTime now json.getLongValue(expires_in) * 1000; return accessTokenCache; }注意 access_token 的官方有效期是 7200 秒2 小时。如果每次请求都重新拿一是微信接口有频率限制二是性能也差。所以必须做缓存最好用 Redis 存多实例部署时也不会互相踢掉 token。我这里的实现是单机缓存生产环境建议换成 Redis。然后写获取手机号的接口/** * 手机号快速验证组件用 code 换手机号 */ public PhoneInfo getPhoneInfo(String phoneCode) { String accessToken getAccessToken(); String url https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token accessToken; HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); JSONObject requestBody new JSONObject(); requestBody.put(code, phoneCode); HttpEntityString entity new HttpEntity(requestBody.toJSONString(), headers); String response restTemplate.postForObject(url, entity, String.class); JSONObject json JSON.parseObject(response); if (json.getIntValue(errcode) ! 0) { throw new BizException(获取手机号失败 json.getString(errmsg)); } JSONObject phoneInfoJson json.getJSONObject(phone_info); PhoneInfo phoneInfo new PhoneInfo(); phoneInfo.setPhoneNumber(phoneInfoJson.getString(phoneNumber)); phoneInfo.setPurePhoneNumber(phoneInfoJson.getString(purePhoneNumber)); phoneInfo.setCountryCode(phoneInfoJson.getString(countryCode)); return phoneInfo; }Controller 层PostMapping(/phone) public Result bindPhone(RequestBody PhoneDTO dto, RequestHeader(Authorization) String token) { // 1. 解析token拿用户id Long userId jwtUtil.parseToken(token.replace(Bearer , )); // 2. 用 code 换手机号 PhoneInfo phoneInfo wxApiClient.getPhoneInfo(dto.getCode()); // 3. 绑定到用户 userService.bindPhone(userId, phoneInfo.getPurePhoneNumber()); return Result.success(phoneInfo.getPurePhoneNumber()); }这个接口返回的数据里phoneNumber带 86 前缀purePhoneNumber是纯手机号。实际业务里建议存purePhoneNumber方便后续发短信、做手机号匹配但展示给用户时可以用phoneNumber或自己格式化。5.3 老版解密方案简述兼容兜底目前新版open-typegetPhoneNumber返回的是 code 换取方案但如果你接的是老系统或者遇到某些兼容场景可能还会碰到encryptedDataiv的老方案。处理逻辑是后端先用session_key做 AES-128-CBC 解密代码如下public String decryptData(String sessionKey, String encryptedData, String iv) { try { byte[] keyBytes Base64.decodeBase64(sessionKey); byte[] ivBytes Base64.decodeBase64(iv); byte[] encryptedBytes Base64.decodeBase64(encryptedData); Cipher cipher Cipher.getInstance(AES/CBC/PKCS7Padding); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] original cipher.doFinal(encryptedBytes); return new String(original, StandardCharsets.UTF_8); } catch (Exception e) { throw new BizException(解密失败 e.getMessage()); } }解密出来的 JSON 里有purePhoneNumber、countryCode这些字段。需要注意的是session_key不会长期有效微信在用户调用某些接口时可能会滚动更新它。老方案如果出现session_key失效解密就失败。所以我现在做项目基本都是直接把老方案丢弃统一用新方案逻辑简单也不容易出岔子。6. 前后端完整联调与常见问题排查实录6.1 联调走通从小程序到服务器的全链路把前端和后端都部署好之后全链路怎么自测我一般按这个顺序微信开发者工具里打开项目确保 AppID 已配置。关掉不校验合法域名选项用真实 HTTPS 域名调接口确认 request 合法域名已配置好。打开调试模式进入页面触发wx.loginNetwork 面板里确认登录接口返回了 token。点击选择头像按钮确认上传接口正常返回的 URL 能直接访问。点击手机号快捷登录确认弹窗正常弹出授权后手机号接口返回成功。数据库里确认用户表的 openid、nickname、avatar_url、phone 字段都已正确落库。联调过程中我习惯把所有微信接口的请求和响应都打日志特别是 code、errcode、errmsg 这三个字段。微信的错误码信息本身已经写得足够清晰遇到问题看日志基本就能定位了。6.2 常见问题速查表问题现象可能原因解决方案登录接口报 errcode 40029code 无效或已过期重复使用了同一个 code重新调 wx.login 拿新 code一次代码只换一次数据errcode 40001access_token 无效或过期重新获取 access_token并做缓存errcode 40013AppID 不合法核对后端的 appid 和 secret 是否写反errcode 40125AppSecret 填写错误去公众平台重新查看 secret注意别复制到空格errcode 63002手机号 code 无效/会话失效重新触发 getPhoneNumber 授权再拿新 code前端拿不到 e.detail.code用户取消授权或基础库版本过低引导用户重新授权升级基础库最低要求 2.21.2个人主体小程序调手机号接口报无权限手机号快速验证组件仅对认证后非个人主体开放升级主体或改用其他合规方案开发者工具能跑真机请求失败服务器域名白名单未配置/未备案在公众平台配置 request 合法域名必须 HTTPS 且已备案头像上传后返回的临时路径失效wxfile:// 临时路径只在本会话有效必须把图片上传到自己的服务器或云存储登录后 openid 在库里总是生成新用户前端登录接口没复用每次都重新调 login登录成功后的 token 要缓存后续请求带上 token不要重复调 login6.3 避坑经验与实操心得这套流程我自己做了至少五六个项目有些坑是真的踩过之后才长记性的。第一个坑手机号授权按钮的样式问题。open-typegetPhoneNumber必须用在button组件上不能用在view或text上。而且这个按钮的授权行为只在用户真实点击时触发任何通过 JS 模拟的点击都不会弹授权窗也没办法在onShow里自动调。如果你希望通过点击整块区域弹出授权直接把这个按钮的样式撑满容器就行但不要试图绕过用户点击。第二个坑code 只能换一次。这是微信的硬性约束。我遇到过生产环境的问题前端把手机号 code 传给后端后端起先没做异常处理网络超时导致前端重试结果同一个 code 被用了两次第二次就报 40029。解决办法是前端对这类请求做 loading 状态锁定防止重复提交后端也要对 code 做幂等处理至少做到换过了就不再换。第三个坑access_token 多实例缓存。如果你的后端部署了多台机器每台机器各本地缓存一份 access_token 没问题但获取的时候要注意并发——两个实例同时刷新 token前一个 token 会被后一个挤下线。表情包虽然是苏州码子但这种问题在生产是真的遇到过的。最简单的方案用 Redis 做分布式锁 缓存或者干脆用一个专门的 token 管理服务。第四个坑隐私协议声明。新版微信要求小程序在「小程序管理后台 - 设置 - 服务内容声明 - 用户隐私保护指引」中声明会收集用户的昵称、头像、手机号等信息否则调用相关接口时会报隐私协议未声明或者弹窗提示。具体操作是在后台添加对应的收集项选「用户信息 - 昵称、头像」以及「手机号」。如果这块不配置真机预览时会发现授权弹窗不出来开发者工具里却能正常跑很容易让人以为代码有问题。第五个坑头像上传的临时路径。chooseAvatar获得的临时文件路径在不同平台的表现不太一样。iOS 有时会返回wxfile://tmp_xxxAndroid 有时返回http://tmp/xxx这些都要通过wx.uploadFile上传后才能得到稳定 URL。另外上传之后如果返回的 URL 是 http 协议在 Android 真机上可能会被拦所以生产环境必须是 HTTPS。6.4 上线前必做的检查清单写到这里我把每次上线前必过的检查项列一下小程序后台已配置服务器域名白名单且全部为 HTTPS。后端环境变量里 appid、secret 已正确配置secret 不进代码仓库。用户隐私保护指引已声明收集昵称、头像、手机号。手机号接口已做频率限制防止恶意刷接口。用户表 openid 已建唯一索引避免并发注册产生脏数据。敏感数据token、session_key、手机号在日志中脱敏不打印明文。已在小程序后台配置「客服」等必要的服务能力如果用到。这套流程做完前后端就完全打通了。最后再分享一个我实际体验中的小技巧做手机号授权时按钮文案不要写获取手机号写一键登录或者微信手机号快捷登录用户点击意愿会高很多。别小看这点文案的差异我实测过转化率能差 10% 以上。另外授权成功后把手机号脱敏展示给用户看让用户确认这个号是不是自己的能避免后续一堆客诉。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →