ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序手机号一键登录:原生前端+Java后端完整实现

微信小程序手机号一键登录:原生前端+Java后端完整实现 简介面向微信小程序开发者的手机号一键登录原生代码资源完整覆盖了从wx.login获取临时凭证、用户授权、调用wx.getPhoneNumber获取手机号码到后台验签并下发登录态的全流程适合需要快速集成一键登录功能、或想系统学习小程序登录授权机制的前端工程师。资源包共12个文件其中5个json承担全局配置与页面配置4个js分别承担入口逻辑、页面事件处理和工具方法封装2个wxss与1个wxml实现页面样式与结构整体包体仅5KB代码精简、目录清晰便于直接对照项目理解。已有334人学习下载。示例代码在util.js中封装了可复用的登录方法app.js中统一维护全局登录态同时index页面展示了按钮触发到返回结果的完整交互project.config.json等配置项也对获取手机号权限相关设置给出了参考能帮助开发者规避授权配置遗漏导致的接口调用失败问题也能快速移植到正式项目中以缩短开发周期。 做了这么多年小程序开发手机号一键登录算是我被问得最多的需求之一。不管是电商、工具类还是社区类小程序几乎都绕不开这个入口。别看市面上全是各种封装好的SDK和云开发方案真要你手写一套原生代码对接微信接口时不少人都卡在code换手机号那一步更别说遇到access_token过期、code重复消费这种隐蔽问题。这篇就用原生小程序代码加Java后端把手机号一键登录从流程到落地完整拆一遍适合刚接触小程序登录体系的新手也给做后端对接的兄弟一个可以直接抄的参考。1. 登录方案选型为什么用getPhoneNumber而不是其他方式1.1 手机号登录在小程序里的特殊地位微信小程序和普通H5有个本质区别它没有浏览器那种天然的用户体系但又比纯网页多了一套微信账号体系。早几年大家习惯用wx.getUserInfo拿昵称头像当登录凭证但从基础库2.21.2之后这个方法返回的已经是匿名数据了真实昵称和头像必须靠open-typechooseAvatar这种新组件去引导用户填写。也就是说微信在刻意收紧静默获取用户信息的入口。手机号恰好是目前用户身份验证里最“硬”的凭证。一个通过实名认证的手机号基本等同于一个真实用户的联系方式对平台做风控、做营销触达都很有价值。所以微信提供了一套专门的能力用户点击授权按钮微信服务端直接完成手机号校验并把解密后的完整手机号返回给开发者。这就是button的open-typegetPhoneNumber组件配合后端的getuserphonenumber接口使用也是现在小程序手机号登录的主流方案。1.2 原生代码与uniapp、云开发的取舍我在这篇里写的是原生代码不是uniapp或者Taro也不是微信云开发的cloud.getOpenData。原因很简单很多中小团队的小程序就是从原生起步的或者因为历史包袱已经跑在原生体系上这时候再引入一个跨端框架反而增加重构成本。再一个原生方式能让你把整个授权流程、token管理、异常处理都掌握在自己手里不会因为云环境的限制而被动。如果你用的是uniapp按钮可以直接写open-typegetPhoneNumber事件回调里拿到的e.detail.code和原生是一致的后端接口部分完全通用。这个方案本质上就是“前端拿code后端换手机号”和具体的前端框架无关所以这篇的前端代码虽然以原生为例但思路发散到任何框架都能用。2. 权限配置与前置条件少一个都跑不起来2.1 小程序后台要做的三件事手机号一键登录不是简单写几行代码就能跑的它有一堆前置条件漏掉任何一个都会让你在调试时百思不得其解。第一小程序必须是已认证的非个人主体。个人主体的小程序没法用这个组件这也是很多人审核通过后发现按钮点了没反应的原因。第二需要在「小程序后台 - 开发管理 - 接口设置」中确认获取手机号接口已经开通新注册的小程序一般默认可用但老项目可能要手动申请。第三如果你调用了wx.login配合获取openid要确保AppID和密钥都是当前小程序正式环境的千万不要拿测试号的AppID去真机调试那个环境下授权弹窗都拉不起来。2.2 服务器域名和隐私协议配置配置完接口权限还要检查两个容易忽略的地方。一是request合法域名。你在前端用wx.request请求自己的后端接口这个域名必须是HTTPS而且要在「小程序后台 - 开发管理 - 开发设置 - 服务器域名」里配置好。开发调试时可以在开发者工具里勾选“不校验合法域名”但真机预览和上线前必须配好。二是用户隐私保护指引。这条是2023年之后新增的硬性要求微信在小程序管理后台专门增加了「用户隐私保护指引」的配置项你需要声明收集了“手机号”这一信息。如果你没配置getPhoneNumber回调会直接报错提示隐私协议未声明很多新项目在测试阶段就会卡在这里。这个过程不算复杂但确实是个新坑。3. 前端原生实现从button组件到登录态存储3.1 WXML里的授权按钮原生小程序的登录页一般长这样view classlogin-container button classphone-login-btn open-typegetPhoneNumber bindgetphonenumberhandlePhoneLogin loading{{loading}} disabled{{loading}} 微信手机号一键登录 /button view classagreement-tip登录即代表同意《用户协议》和《隐私政策》/view /view这里有几个关键点。open-typegetPhoneNumber是按钮能拉起授权弹窗的触发条件它必须是button组件你用view模拟一个按钮是不能触发这个能力的。bindgetphonenumber是授权回调用户点击弹窗里的“允许”或“拒绝”后都会触发。loading和disabled两个属性是防止重复点击的关键这个我在后面会专门展开说。3.2 JS逻辑拿到code只是第一步按钮回调里拿到的并非手机号本身而是一个code。这是微信从基础库2.21.2开始的新逻辑以前是通过getPhoneNumber回调里的encryptedData和iv去解密现在统一改成code换手机号逻辑更简单也更安全。// pages/login/login.js Page({ data: { loading: false }, // 用户点击授权按钮后的回调 async handlePhoneLogin(e) { // e.detail.errMsg 用来判断用户是否点了允许 if (e.detail.errMsg ! getPhoneNumber:ok) { wx.showToast({ title: 需要授权手机号才能登录, icon: none }); return; } // 防止重复点击code被重复消费 if (this.data.loading) return; this.setData({ loading: true }); try { const phoneCode e.detail.code; // 同时调用wx.login获取登录code用来绑定openid const loginCode await this.getWxLoginCode(); // 把两个code都传给后端 const res await this.requestLogin(phoneCode, loginCode); // 成功后存储登录态 wx.setStorageSync(token, res.token); if (res.userInfo) { wx.setStorageSync(userInfo, res.userInfo); } wx.showToast({ title: 登录成功, icon: success }); // 跳转到首页或者回跳页面 setTimeout(() { wx.switchTab({ url: /pages/index/index }); }, 600); } catch (err) { console.error(登录失败, err); wx.showToast({ title: err.message || 登录失败请重试, icon: none }); } finally { this.setData({ loading: false }); } }, // wx.login 包装成Promise getWxLoginCode() { return new Promise((resolve, reject) { wx.login({ success: (res) { if (res.code) { resolve(res.code); } else { reject(new Error(wx.login 获取code失败)); } }, fail: (err) reject(err) }); }); }, // 调用后端登录接口 requestLogin(phoneCode, loginCode) { return new Promise((resolve, reject) { wx.request({ url: https://your-domain.com/api/wx/login, method: POST, data: { phoneCode: phoneCode, loginCode: loginCode }, success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else { reject(new Error((res.data res.data.msg) || 登录接口异常)); } }, fail: (err) reject(err) }); }); } });很多新手会漏掉wx.login这一步。手机号code只能换到手机号本身完全没有用户身份信息。同一部手机换个人用绑定的还是这个号所以必须额外通过wx.login拿一个code发给后端后端拿这个code去换openid再把openid和手机号绑在你的用户表里。这两者是配合使用的缺一不可。4. 后端Java实现code换手机号与登录态下发4.1 接口调用顺序和完整流程整个后端要处理的事情我按顺序列一下用前端传来的loginCode调用微信的code2Session接口换出openid和session_key。用前端传来的phoneCode调用getuserphonenumber接口换出手机号。根据openid查用户表如果不存在就创建新用户并绑定手机号。生成自定义登录态我这里用JWT返回给前端。这个顺序其实可以并行因为两个换取的接口互不依赖。但最终落到数据库时要对同一个openid做幂等处理避免并发请求下创建出两条用户记录。4.2 code换手机号的完整代码我用的是Spring Boot框架HTTP客户端用现成的RestTemplateJSON处理用Fastjson。下面是关键代码。Service public class WxAuthService { Value(${wx.appid}) private String appid; Value(${wx.secret}) private String secret; private final RestTemplate restTemplate new RestTemplate(); /** * 手机号code换手机号 */ public String getPhoneByCode(String phoneCode) { // access_token要缓存这里省略见4.3 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); MapString, String body new HashMap(); body.put(code, phoneCode); HttpEntityMapString, String request new HttpEntity(body, headers); String response restTemplate.postForObject(url, request, String.class); JSONObject json JSON.parseObject(response); if (json.getIntValue(errcode) ! 0) { throw new BizException(手机号获取失败 json.getString(errmsg)); } JSONObject phoneInfo json.getJSONObject(phone_info); return phoneInfo.getString(phoneNumber); } /** * wx.login 的code换openid */ public WxSession code2Session(String loginCode) { String url https://api.weixin.qq.com/sns/jscode2session ?appid appid secret secret js_code loginCode grant_typeauthorization_code; String response restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(response); if (json.getIntValue(errcode) ! 0) { throw new BizException(登录code校验失败 json.getString(errmsg)); } WxSession session new WxSession(); session.setOpenid(json.getString(openid)); session.setSessionKey(json.getString(session_key)); return session; } }4.3 access_token的缓存问题getuserphonenumber接口要求带上access_token这个token是通过appid和secret调用微信的cgi-bin/token接口获取的。关键是这个token的有效期是7200秒而且微信对获取接口有频率限制如果你每个手机号登录请求都去现取一个token很快就会被限流。正确做法是缓存起来。我用Redis存wx_access_token过期时间设置为7000秒留200秒的余量避免边缘时间请求时token刚好失效public String getAccessToken() { // 先从缓存取 String token redisTemplate.opsForValue().get(wx_access_token); if (StringUtils.hasText(token)) { return token; } // 缓存没有去微信取 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); token json.getString(access_token); if (StringUtils.hasText(token)) { redisTemplate.opsForValue().set(wx_access_token, token, 7000, TimeUnit.SECONDS); } else { throw new BizException(获取access_token失败 json.getString(errmsg)); } return token; }4.4 登录接口与token下发用户从数据库查到或者创建之后我用JWT生成一个自定义登录态。不建议把微信的session_key直接当登录态因为客户端没有你服务端的密钥并且session_key换了session就会变不利于做长连接和状态管理。RestController RequestMapping(/api/wx) public class WxLoginController { Resource private WxAuthService wxAuthService; Resource private UserService userService; Resource private JwtUtil jwtUtil; PostMapping(/login) public ResultLoginResponse login(RequestBody LoginRequest request) { // 1. 手机号code换手机号 String phone wxAuthService.getPhoneByCode(request.getPhoneCode()); // 2. wx.login的code换openid WxSession session wxAuthService.code2Session(request.getLoginCode()); // 3. 根据openid查找或创建用户 User user userService.findOrCreate(session.getOpenid(), phone); // 4. 生成JWT token String token jwtUtil.createToken(user.getId(), user.getOpenid()); LoginResponse response new LoginResponse(); response.setToken(token); response.setUserInfo(buildUserInfo(user)); return Result.ok(response); } private UserInfoVo buildUserInfo(User user) { UserInfoVo vo new UserInfoVo(); vo.setNickname(user.getNickname()); // 手机号做脱敏 vo.setPhone(maskPhone(user.getPhone())); vo.setAvatar(user.getAvatar()); return vo; } private String maskPhone(String phone) { if (phone null || phone.length() 7) { return phone; } return phone.substring(0, 3) **** phone.substring(7); } }脱敏返回手机号这个细节很多人会忽略。前端拿到完整手机号之后除了一些特殊业务场景之外展示层最好都用脱敏形式一方面降低敏感数据泄露风险另一方面也符合现在各平台对个人隐私保护的要求。5. 常见问题与排查技巧实录5.1 授权弹窗点了没反应这是最常被问的现象。点按钮之后什么弹窗都没出现或者出现后立刻消失。排查顺序一般是先看小程序是不是个人主体个人主体没有这个能力再看后台接口设置里有没有开通获取手机号接口再看基础库版本是不是低于2.21.2。还有一个小概率原因是开发者工具缓存异常把工具整个重启一下或者清除全部缓存再看。注意用开发者工具调试时如果当前用的是测试号getPhoneNumber会出现“功能未授权”的报错。测试号不支持手机号快速验证组件必须在真实AppID环境下测试这一点非常容易踩坑。5.2 code报40029错误errcode: 40029对应的错误信息是invalid code意思是这个code无效或已被消费。这里要注意前端传给后端的phoneCode是一次性的用一次之后立即失效。如果你在接口里不小心打了日志把同一个请求重复提交了或者前端因为重复点击把同一个code提交了两次第二次就会拿到这个错误。解决方案其实在前面已经埋了伏笔前端在收到回调后立刻设置loading并disabled按钮防止用户手快连点。后端同样要做好防重比如用Redis把用过的code存一下发现重复就直接拒绝。另外phoneCode的有效期也很短一般就几分钟如果用户授权后隔很久才提交也会过期。5.3 手机号拿到了但没绑定微信openid很多人第一次做会只调getuserphonenumber拿到手机号然后直接拿手机号当业务主键这样用起来问题很多。同一个手机号可能在不同微信号上登录如果不做openid和手机号的双重绑定用户换设备后就会出现登录态错乱。正确做法是openid是用户在微信生态里的唯一标识手机号是业务联系凭证用户表里两个字段都要存且以openid作为登录的主要依据。5.4 隐私协议报错的处理getPhoneNumber:fail 隐私协议未声明或者privacy permission is not configured这类报错基本都是因为后台没配置隐私保护指引。进入「小程序后台 - 设置 - 服务内容声明 - 用户隐私保护指引」勾选“手机号”字段然后重新发布体验版。注意配置完之后可能要等几分钟才能生效不需要修改代码。5.5 真机可以但开发者工具不行这个现象多半出现在开发者工具版本比较老或者工具里的小程序基础库设置偏低。建议在开发者工具右上角「详情 - 本地设置」里把调试基础库调到最新版本再点“真机调试”试试。如果真机正常而工具里一直报错不影响实际用户但为了开发效率还是建议把工具升级到最新。6. 个人实操中的几点体会这套方案我自己在多个项目里跑过从最早的encryptedData解密时代一路跟到现在的code换取模式整体感觉是流程越来越简化但对开发者的细节要求反而变高了。尤其是code的一次性特性和access_token的缓存这两个点是线上最容易出问题的环节但很多人不到上线那一刻根本发现不了。最后再分享一个小细节如果你在页面上同时有“微信一键登录”和“手机验证码登录”两种方式建议把按钮状态做成统一的loading并且在整个登录请求期间禁止页面返回。因为用户如果中途退出页面请求还在进行中回来之后token写入Storage就会出现竞态。我在一个电商项目里就遇到过用户快速退出重进导致登录态未写入却跳转了首页的bug后来加了全局的请求锁和页面栈判断才彻底解决。这种边界问题不遇到一次光看文档是真的想不到的。本文还有配套的精品资源点击获取
返回列表