资讯详情

微信小程序登录全解析:从wx.login到code2session完整指南

📅 2026/10/9 11:22:04 | 华诺云谱 👁 阅读
微信小程序登录全解析:从wx.login到code2session完整指南
这个需求几乎每个小程序开发者都躲不掉用户打开小程序需要识别出他是谁。我刚接到这个任务时心里想的是“不就是调一下 wx.login 嘛”但真正做完才发现微信登录这条链路里藏着 wx.login、code2session、openid、session_key、自定义登录态这么多环节任何一个点理解不到位排查问题时都能把你卡到怀疑人生。这篇笔记就是把整个学习过程重新捋一遍从原理到代码从踩坑到验收适合刚接触小程序登录、或者做过一两次但总觉得“哪里没想透”的同学也适合后端同学快速理解前端在折腾什么。1. 先弄明白微信登录到底在解决什么问题1.1 一个用户两个身份微信身份和你的业务身份小程序跑在微信生态里但微信不会随随便便把用户信息交给开发者。它给你一套“授权 凭证”的机制让你既能识别用户又碰不到敏感数据。这里先要分清楚两个身份微信身份用户在这个小程序里的 openid一串不透明字符串唯一且不会变。业务身份用户在你自己的系统里对应的那条记录通常是一个自增 id 或者 UUID后面跟着昵称、头像、积分、订单。微信登录要解决的核心问题就是把这两个身份安全地绑定起来。用户第一次打开小程序你不知道他是谁wx.login 之后你通过微信接口拿到他的 openid再拿这个 openid 去你的用户表里查有记录就是老用户没记录就建一个新账号。实际项目里还有一种常见情况用户在你的 App 或 H5 里已经注册过现在从小程序进来你希望他直接用微信登录就能看到之前的订单。这就涉及 openid 和业务账号的合并技术上要在 user 表里加 openid 字段并设计好账号绑定关系。1.2 为什么不能把 openid 直接给前端很多初学者问既然 wx.login 能拿到 code后端能用 code 换 openid那能不能让前端直接拿到 openid然后带着 openid 请求后端接口答案是绝对不能。原因很简单小程序前端跑在用户设备上它不是可信环境。如果前端能拿到 openid任何懂点技术的人都可以通过修改请求参数把自己伪装成任意用户。你把接口改成?openidxxx就能以别人身份下单这个系统就废了。正确的做法是让 openid 在“后端到微信服务器”这条链路上流转前端拿到的只是一个临时凭证 code。整个流程就像你去银行取钱你在前台出示身份证wx.login 生成 code银行柜员拿着身份证去后台系统核验后端拿 code 调 code2session核验通过后柜员给你一张银行卡后端下发的自定义登录态 token以后你每次来都刷卡柜员不再看身份证请求接口时带 token。所以说前端永远只需要打交道两样东西code和token。openid、session_key 这些敏感信息一律留在后端。2. 登录链路拆解wx.login、code 与 openid 的三角关系2.1 wx.login 到底做了什么先看前端这行最核心的调用wx.login({ success: (res) { // res.code 就是一个临时登录凭证 console.log(res.code); } });这个 code 有几个很关键的特性有效期约 5 分钟过期作废一次性使用过后立即失效由微信客户端生成关联当前用户和当前小程序静默完成不弹任何授权框用户无感知。这几点决定了它的用途它只适合作为“换取 openid 的入场券”不适合保存、复用、跨端传递。有时候会遇到wx.login返回的 code 和预期不一致先检查是不是有的项目用了 uni-app 这类跨端框架封装成了uni.login。用法上等价底层还是 wx.login只是回调风格略有差异。2.2 code2session后端接力的关键一跳前端拿到 code 后要把它通过自己的后端接口传过去用 HTTPS POST别放在 URL 查询参数里避免日志泄漏。后端拿到 code 之后去请求微信提供的接口GET https://api.weixin.qq.com/sns/jscode2session?appidAPPIDsecretSECRETjs_codeCODEgrant_typeauthorization_code成功时返回类似这样的结果{ openid: oXkRz5xxxxxxx, session_key: tGzh3xxxxxxx, unionid: o6_bmjrPTlm6_2sgVt7hMZOPfL2M }注意unionid 不是每次都会返回。只有当前小程序绑定了微信开放平台账号且用户曾经在该开放平台账号下的其他应用里授权过才会带上 unionid。没绑开放平台的话就只有 openid 和 session_key。后端拿到 openid 后要立刻把这个用户写入或更新到自己的用户表并生成自定义登录态返回给前端。这里有个小细节很多后端同学图省事直接把 session_key 或 openid 一起返回给前端后续接口靠它来识别用户。我强烈不建议这么干原因后面展开。2.3 openid、unionid 与 session_key 的角色差异这三个东西非常容易混我一开始也搞不清楚做了个表对比字段作用域特点典型用途openid单个小程序同一个用户在不同小程序里 openid 不同仅在当前小程序内唯一稳定用户表的唯一标识、登录态底层依据unionid开放平台账号下所有应用同一用户在同一开放平台主体下所有小程序/公众号/App 中一致多端账号打通、跨应用识别用户session_key单个小程序会话会话期间有效微信不明确告知过期时间用于解密敏感数据或校验签名解密手机号、用户信息等加密数据用一句话记忆openid 是“你在这个小程序里的工号”unionid 是“你在整个集团里的身份证号”session_key 是“这次会话的密钥”。需要特别注意的是 session_key 的有效期。微信官方文档没有给出确切的过期时间实践中通常认为与用户进入小程序的会话生命周期相关可能几小时也可能更长。这意味着后端不能长期缓存 session_key需要解密用户数据时遇到失败应当引导前端重新执行 wx.login 获取新会话。3. 从零实现前端登录态与后端 code2session 的联调过程3.1 前端代码wx.login 加一个请求就够了吗如果你的需求只是“让用户登录”那么前端的核心逻辑确实很简单wx.login 拿到 code传给后端后端返回 token存起来。但工程上还要考虑几个问题已有登录态时不要重复弹登录请求要统一封装自动带 token登录失败的兜底提示。下面是一段比较完整的原生小程序登录页逻辑// pages/login/login.js const app getApp(); Page({ data: { loading: false }, onLoad() { // 如果本地已有 token直接进首页 const token wx.getStorageSync(token); if (token) { wx.reLaunch({ url: /pages/index/index }); } }, handleWechatLogin() { if (this.data.loading) return; this.setData({ loading: true }); wx.login({ success: async (res) { if (!res.code) { wx.showToast({ title: 获取登录凭证失败, icon: none }); this.setData({ loading: false }); return; } try { const loginRes await new Promise((resolve, reject) { wx.request({ url: https://api.example.com/auth/wechat, method: POST, data: { code: res.code }, success: resolve, fail: reject }); }); const { code, data, message } loginRes.data; if (code 0) { wx.setStorageSync(token, data.token); wx.setStorageSync(userInfo, data.userInfo || {}); wx.reLaunch({ url: /pages/index/index }); } else { wx.showToast({ title: message || 登录失败, icon: none }); } } catch (err) { wx.showToast({ title: 网络异常请重试, icon: none }); } finally { this.setData({ loading: false }); } }, fail: () { wx.showToast({ title: wx.login 调用失败, icon: none }); this.setData({ loading: false }); } }); } });这段代码看起来平淡但有几个点是我实际开发中吃过亏后加上的用loading状态防止用户连续点击避免同一个页面同时发出多个 wx.login 请求从而产生多个 code登录成功后用wx.reLaunch而不是wx.navigateBack是因为登录页通常不该留在页面栈里token 放 Storage同时要在请求拦截器里带上。3.2 后端拉取微信接口并下发自定义登录态后端逻辑也不复杂核心三步拿 code 换微信身份、查/建用户、签发 token。我用 Node.js 写个示例思路可以平移到 Java、Go、Python// auth.js const crypto require(crypto); const WX_APPID process.env.WX_APPID; const WX_SECRET process.env.WX_SECRET; // 1. code2session拿 code 换 openid session_key async function code2session(code) { const url https://api.weixin.qq.com/sns/jscode2session?appid${WX_APPID}secret${WX_SECRET}js_code${code}grant_typeauthorization_code; const res await fetch(url); const data await res.json(); if (data.errcode) { const err new Error(code2session failed: ${data.errmsg}); err.errcode data.errcode; throw err; } return data; // { openid, session_key, unionid } } // 2. 查用户不存在则创建 async function findOrCreateUser(openid, unionid) { let user await db.findOne({ where: { openid } }); if (!user) { user await db.create({ openid, unionid: unionid || null, nickname: 微信用户, lastLoginAt: new Date() }); } else { user.lastLoginAt new Date(); await user.save(); } return user; } // 3. 签发自定义登录态 token function signToken(userId) { const payload { uid: userId, exp: Math.floor(Date.now() / 1000) 7 * 24 * 3600 // 7 天过期 }; // 实际项目可换成 jose / jsonwebtoken 等标准库 const header Buffer.from(JSON.stringify({ alg: HS256, typ: JWT })).toString(base64url); const body Buffer.from(JSON.stringify(payload)).toString(base64url); const signature crypto.createHmac(sha256, process.env.TOKEN_SECRET) .update(${header}.${body}) .digest(base64url); return ${header}.${body}.${signature}; } // 4. 登录接口 async function wechatLoginHandler(req, res) { const { code } req.body; if (!code) { return res.status(400).json({ code: -1, message: 缺少 code }); } try { const wxSession await code2session(code); const user await findOrCreateUser(wxSession.openid, wxSession.unionid); const token signToken(user.id); res.json({ code: 0, data: { token, userInfo: { nickname: user.nickname, avatar: user.avatar } } }); } catch (err) { res.status(500).json({ code: -1, message: err.message }); } }几个容易被忽略的点secret 不要硬编码在代码里更不能出现在前端代码中。它一旦泄露别人就能用你的 appid 换取任意 openid。code2session 是 GET 请求但你的登录接口建议用 POST避免 code 出现在访问日志中。微信接口返回的errcode要显式处理尤其是40029和40163前端收到后通常会提示用户重新登录。3.3 数据库表设计与用户归属用户表设计看着简单但字段规划不好后面加功能会很难受。我最常用的最小设计是这样的CREATE TABLE user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL, unionid VARCHAR(64) DEFAULT NULL, nickname VARCHAR(64) DEFAULT 微信用户, avatar VARCHAR(512) DEFAULT , phone VARCHAR(20) DEFAULT , created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, last_login_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid), KEY idx_unionid (unionid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;说明一下设计意图openid 加唯一索引防止并发下同一个微信用户被创建多条记录unionid 建普通索引因为多端绑定时要通过它查用户手机号单独存业务字段和微信登录解耦用户可以不填手机号但一旦填了就能做营销触达不要用 openid 直接当主键业务系统里所有外键都引用自增 id性能更好。如果同一用户在小程序 A 和 B 都登录了而这两个小程序绑定了同一个开放平台账号那么两条 user 记录会有相同的 unionid。此时可以设计一张user_bind表或者直接在 user 表上用 unionid 做关联在查询时做合并展示。4. 授权、手机号与多端登录容易被忽略的用户体验细节4.1 静默登录与授权登录的正确顺序微信登录有一个特别容易被产品经理和开发一起搞混的点wx.login 是无感知的不需要用户点任何按钮而获取头像昵称是需要用户主动触发的授权行为。正确的姿势通常是这样小程序启动后悄悄调 wx.login 完成静默登录拿到 openid 并建立会话从业务接口拉取用户资料如果业务要求必须有头像昵称再引导用户去“完善资料”而不是一进来就弹授权框。我以前见过一个反面案例用户刚打开小程序第一个页面就弹“是否授权获取你的昵称头像”很多人直接拒绝然后后续所有功能都用不了。合理的设计是先用默认头像和“微信用户”跑通全部功能用户真想换昵称头像时再让他自己改。这里还要提一个重要变化从 2021 年 4 月起wx.getUserProfile接口虽然还能弹窗但已经不再返回真实的头像昵称而是返回默认的灰色头像和“微信用户”。2022 年 10 月之后官方推荐用新的“头像昵称填写能力”也就是button classavatar-wrapper open-typechooseAvatar bind:chooseavataronChooseAvatar image src{{avatarUrl}} / /button input typenickname bindbluronNicknameBlur placeholder请输入昵称 /用open-typechooseAvatar让用户选头像用typenickname的输入框让用户填昵称。这是当前获取头像昵称的主流方式老接口只作为兼容保留。4.2 手机号快速验证组件的选择很多小程序商城的登录流程里“微信登录”之后还有一个动作绑定手机号。这里的坑比想象中多。先说结论现在推荐用官方button open-typegetPhoneNumber方案。前端拿到的不是明文手机号而是一个 codebutton open-typegetPhoneNumber bindgetphonenumbergetPhoneNumber绑定手机号/buttongetPhoneNumber(e) { if (e.detail.code) { // 把 code 传给后端由后端调用微信接口换取手机号 wx.request({ url: https://api.example.com/auth/bind-phone, method: POST, data: { code: e.detail.code } }); } else { // 用户拒绝授权 wx.showToast({ title: 未授权手机号, icon: none }); } }后端拿到这个 code 后用小程序全局的access_token调用微信接口换取手机号返回结果里包含phone_info.phone。使用这个组件有几个硬性条件小程序必须是非个人主体企业、个体户等才行需要在小程序后台申请开通“手机号快速验证组件”能力该能力目前按调用次数计费免费额度用完后需要付费所以不是所有业务场景都适合无脑用。如果只是做个个人练手项目或者对成本敏感可以考虑让用户手动输入手机号 短信验证码体验上略差但不受资质和费用限制。4.3 多端登录与 UnionID 打通同一个微信用户进入你的小程序 A 会得到一个 openid A进入小程序 B 会得到 openid B两者完全不同。如果不做开放平台绑定你在 A 和 B 里看到的可能是两个“新用户”。解决方案是注册微信开放平台账号把同主体下的小程序、公众号、App 都绑上去。绑定之后code2session 返回的 unionid 才会出现后续就可以用 unionid 识别同一用户。在实际项目中打通多端经常涉及到老账号合并这是最麻烦的部分。我处理过一个场景用户在 H5 里用手机号注册过后来在小程序里用微信登录系统通过手机号匹配到老账号再提示用户确认合并。技术上就是做个UPDATE user SET openid ? WHERE phone ?但要处理重复记录、用户确认、合并后订单归属等问题千万别在线上直接跑一条 SQL 了事。5. 学习路上踩过的坑会话过期、code 复用与接口变更5.1 最典型的坑同一个 code 用两次我第一次联调时前端手滑把登录请求发了两次后端直接报错errcode: 40029, errmsg: invalid code原因就是 code 一次性。第一次请求已经用掉并换取了 openid第二次再用同一个 code 必然失效。这个坑的变体很多用户快速点击登录按钮产生多个请求前端请求超时后自动重试重试时带同一个 code后端并发环境下同一个 code 被两个服务实例同时消费。解决方案也很清晰前端加 loading 锁后端做好接口幂等性同一个 code 如果已经处理过就直接返回旧结果如果条件允许用 Redis 之类的存储记录 code 的消费状态。5.2 session_key 过期与解密失败如果需要解密微信返回的加密用户数据旧版手机号方案、运动数据等会用到 session_key。这个 key 有有效期且微信不告诉你确切过期时间解密时经常遇到-41003之类的错误码。我踩过的最深的一次坑是后端把 session_key 存到了 Redis结果用户第二天再来解密手机号时微信端会话已经刷新session_key 变了解密一直失败。后来才意识到session_key 更新时机用户不可控只要用户重新 wx.login旧的 session_key 就可能失效。应对策略每次 wx.login 成功后后端都无条件更新 session_key解密失败时主动向前端返回“需要重新登录”让前端再走一遍 wx.login 流程不要在业务里依赖 session_key 的持久性它只是会话期的临时密钥。现在新的手机号方案已经不需要 session_key 解密了后端用 access_token 换手机号这条路会干净很多。但老项目如果还跑在旧方案上上面的经验依旧适用。5.3 接口变更的“隐形坑”getUserProfile 与 phoneNumber 调整微信小程序的登录相关接口这两年调整得非常频繁如果不看公告很容易被“昨天还能用、今天突然不行了”搞崩溃。时间线大致是这样的时间变化影响2021-04-28wx.getUserInfo不再弹出授权无法再通过旧接口获取真实头像昵称2021 年后推荐wx.getUserProfile但后来返回的也是默认头像昵称2022-10-25头像昵称填写能力正式接入chooseAvatarnickname输入框成为主流2023-08-26手机号验证能力开始收费旧手机号加密方案逐步下线新 code 换手机号方案成为标准这些变更带来的教训是做微信小程序必须养成阅读微信官方公告的习惯。社区里的老代码、老教程很容易过时尤其是涉及用户隐私的接口微信的态度一直是在收紧。遇到登录相关能力异常时第一步不是改代码而是去官方文档和公告里看接口是不是又被调整了。5.4 开发环境与外网请求的域名问题本地开发时后端接口跑在http://localhost:8080小程序默认请求不了非 HTTPS、非备案域名。微信开发者工具有一个“不校验合法域名”的开关开发期勾上就能联调但真机预览时这个开关不生效必须走 HTTPS 域名。比较稳妥的开发姿势开发期用 ngrok 之类工具把本地服务映射成 HTTPS 临时域名或者直接把接口部署到测试环境用https://test-api.example.com联调上线前把正式域名配到小程序后台的 request 合法域名里注意域名需要 ICP 备案。我第一次真机预览时就被这个坑卡了半小时开发者工具里一切正常手机上一直报“request:fail”。后来才意识到是域名没配好小程序后台的合法域名列表和代码里写的地址必须完全一致包括协议和端口。6. 调试验证与上线前检查清单6.1 用开发者工具把登录流程走一遍微信登录这种链路型功能最怕“局部看着对整体走不通”。我推荐按下面的顺序在开发者工具里过一遍打开开发者工具Network 面板清空杀掉小程序重新编译启动观察 Network找到请求序列第一波应该是 wx.login 对应产生的后端登录接口请求再往后是业务接口请求点开登录接口的响应确认返回了 token 和 userInfo切到 Storage 面板确认 token 已写入本地缓存手动清除 Storage再编译确认回到登录态退出小程序重新进入确认 token 存在时不重复调登录。这一步能发现大多数问题接口报错、token 没存上、页面跳转时机不对等等。不用一开始就上真机工具里跑顺了再测真机效率更高。6.2 一套可复现的验证脚本除了手动点还可以准备一组固定的验收用例。我自己常用的验收清单长这样首次登录新 openid 入库返回 token前端进入首页二次登录同一 openid 不再新建用户last_login_at 更新token 过期手动改本地 token 为乱串请求业务接口应返回 401前端跳登录页code 重复提交把同一个 code 手动提交两次第二次应被后端拦截且不能产生脏数据用户拒绝授权点击授权按钮后选择拒绝应用不应崩溃也不应卡在授权页手机号绑定新手机号能绑定成功重复绑定同一手机号到不同微信账号时要提示冲突。这些用例不用写成自动化测试但每次改动登录相关代码后手动过一遍能省掉很多线上事故。6.3 常见错误码速查表登录联调中后端日志里最常出现的几个微信错误码我整理了一份错误码含义处理方式-1系统繁忙稍后重试或检查微信接口是否临时故障40029code 无效重新调 wx.login 获取新 code40163code 已被使用同 40029重新登录45011API 调用频率限制检查是否高频调用做服务端限流40226高风险等级用户小程序登录拦截按官方要求处理一般需人工客服遇到错误码先别慌去微信官方文档搜“错误码 errcode”通常能定位到原因。最怕的是把错误码吞掉只在前端弹一个“失败”那样排查起来才是真的灾难。结尾回头再看这次学习过程微信登录本身并不复杂复杂度来自它背后一整套“身份与授权”的规则以及微信时不时调整接口的历史包袱。我个人最大的体会是遇到登录相关需求先把 wx.login、code2session、openid、session_key 这四个概念彻底吃透再动手写代码能少走很多弯路。开发中养成看官方公告的习惯尤其是涉及用户隐私的接口变化。最后提醒一句secret 和 session_key 这类凭证一定要藏好不然后端辛辛苦苦搭起来的信任体系可能就毁在一个随手提交到代码仓库的配置文件上。后面我准备再写一篇小程序登录态与后端会话管理的联动笔记把 token 刷新、续期和踢人下线的细节也补上。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑