微信扫二维码跳转小程序绑定手机号进行登录


一、角色及流程

1.1 角色

角色作用
web后端提供生成二维码,校验二维码登录接口
web前端调用生成二维码接口 ,轮训检查登录状态
小程序后端提供小程序登录,小程序绑定手机号接口
小程序前端调用小程序后端接口

1.2 流程

详细步骤及交互

	网页端 ->> 后端服务: 生成带scene的二维码
    后端服务 ->> 微信服务器: 调用getwxacodeunlimited接口
    微信服务器 -->> 后端服务: 返回二维码图片
    网页端	 ->> 网页端: 展示二维码
    小程序端 ->> 微信服务器: 扫码获取临时code
    微信服务器 -->> 小程序端: 返回code和encryptedData
    小程序端 ->> 后端服务: 提交code和scene
    后端服务 ->> 微信服务器: 通过code获取openid
    微信服务器 -->> 后端服务: 返回openid和session_key
    小程序端 ->> 后端服务: 提交手机号授权数据
    后端服务 ->> 数据库: 查询/创建用户记录
    数据库 -->> 后端服务: 返回用户状态
    后端服务 ->> 网页端: 轮训或WebSocket通知登录结果

1.2.1. 生成小程序二维码

1.2.2. 用户扫码进入小程序

1.2.3. 用户登录授权

1.2.4. 获取手机号授权

1.2.5. 解密并绑定手机号

1.2.6. 登录完成

二、准备工作

微信开放平台,微信小程序注册认证,获取appid和secretkey、配置微信二维码访问规则(文件校验等问题)、微信开发者工具等

二、实现代码

1.1 web后端

1.1.1 引入依赖

		<dependency>
            <groupId>com.github.binarywang</groupId>
            <artifactId>weixin-java-miniapp</artifactId>
            <version>4.6.0</version>
        </dependency>

1.1.2 后端代码


import cn.binarywang.wx.miniapp.api.WxMaQrcodeService;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.util.WxMaConfigHolder;
import com.base.api.BaseStaffService;
import com.base.api.LoginService;
import com.base.constant.BaseConstant;
import com.common.core.exception.CheckedException;
import com.domain.base.entity.BaseStaff;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 后台登录控制器
 */
@RestController
@RequestMapping("/api/login")
@RequiredArgsConstructor
@Slf4j
public class WxLoginController {

    private final WxMaService wxMaService;

    private final BaseStaffService baseStaffService;

    private final LoginService loginService;

    private final RedisTemplate<String, String> redisTemplate;

    @Value("${spring.profiles.active}")
    private String activeProfile;

    @GetMapping("/qrcode")
    @ApiOperation(value = "生成微信二维码")
    public ResponseEntity<byte[]> generateQrCode() {

        try {
            String scene = UUID.randomUUID().toString().substring(0, 8);
            System.out.println("scene=" + scene);

            // 保存token到Redis,设置5分钟过期
            redisTemplate.opsForValue().set(BaseConstant.WX_CACHE + scene, BaseConstant.UN_SCANNED, 5, TimeUnit.MINUTES);

            WxMaQrcodeService codeService = wxMaService.getQrcodeService();
            String envVersion = BaseConstant.SYS_ENV_MAPPER_WX_ENV.getOrDefault(activeProfile,
                    BaseConstant.SYS_ENV_MAPPER_WX_ENV_DEFAULT);

            // 生成小程序码
            byte[] qrCodeBytes = codeService.createWxaCodeUnlimitBytes(scene, BaseConstant.WX_SCAN_TO_PAGE, false, envVersion, 430, false, null, false);

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.IMAGE_JPEG);

            log.info("生成微信二维码,scene:{}", scene);

            return new ResponseEntity<>(qrCodeBytes, headers, HttpStatus.OK);
        } catch (WxErrorException e) {
            throw new CheckedException("生成微信二维码失败");
        } catch (Exception e) {
            throw new CheckedException(e);
        } finally {
            WxMaConfigHolder.remove();//清理ThreadLocal
        }
    }

    @GetMapping("/check")
    @ApiOperation(value = "检查登录状态:检查成功后返回token")
    public ResponseEntity<?> checkLoginStatus(@RequestParam String scene) {
        try {
            log.info("检查登录状态,scene:{}", scene);
            String status = redisTemplate.opsForValue().get(BaseConstant.WX_CACHE + scene);
            if (status == null) {
                return ResponseEntity.status(404).body("二维码已过期");
            } else if (status.equals(BaseConstant.UN_SCANNED)) {
                return ResponseEntity.ok().body(Collections.singletonMap("status", "等待扫码"));
            } else {
                // 已确认,返回用户ID
                String userId = status;
                BaseStaff baseStaff = baseStaffService.getById(Long.valueOf(userId));
                if (baseStaff == null) {
                    return ResponseEntity.status(404).body("用户不存在");
                }
                String token = loginService.createToken(baseStaff);

                redisTemplate.delete(BaseConstant.WX_CACHE + scene);
                log.info("用户小程序登录成功,token:{},用户ID:{},用户名称:{}", token, baseStaff.getId(), baseStaff.getUserName());
                return ResponseEntity.ok().body(token);
            }
        } catch (Exception e) {
            throw new CheckedException(e);
        }

    }
}

常量类

public interface BaseConstant {

    /**
     * 微信小程序登录二维码跳转页面
     */
    String WX_SCAN_TO_PAGE = "pages/index/index";

    /**
     * 系统环境对应微信环境
     * release:正式
     * trial:体验
     * develop:开发
     */
    Map<String, String> SYS_ENV_MAPPER_WX_ENV = Map.of(
            "prod", "release",//正式
            "uat", BaseConstant.SYS_ENV_MAPPER_WX_ENV_DEFAULT,//体验
            "dev", "trial"          //开发
    );

    String SYS_ENV_MAPPER_WX_ENV_DEFAULT = "trial"; //默认体验环境

    String UN_SCANNED = "UN_SCANNED";

    String WX_SESSION_KEY_CACHE_PREFIX = "session:WX_SESSION_KEY_";

    String WX_CACHE = "wx:";
}

1.1.3 其他

1.白名单配置
2.微信校验文件访问等

1.2 web前端

略:展示二维码和轮训

1.3 小程序后端


import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.binarywang.wx.miniapp.util.WxMaConfigHolder;
import com.base.api.BaseStaffService;
import com.base.constant.BaseConstant;
import com.base.controller.wx.ao.LoginBindPhoneRequestAo;
import com.base.controller.wx.ao.LoginRequestAo;
import com.common.core.exception.CheckedException;
import com.common.core.utils.ArgumentAssert;
import com.common.core.utils.StrPool;
import com.domain.base.entity.BaseStaff;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**

 * @description: 小程序登录控制器
 * @author: biejiahao

 * @create: 2025-04-07 19:25
 */
@RestController
@RequestMapping("/api/miniapp")
@RequiredArgsConstructor
@Slf4j
public class MiniAppController {

    private final WxMaService wxMaService;

    private final BaseStaffService baseStaffService;

    private final RedisTemplate<String, String> redisTemplate;

    @PostMapping("/login")
    @ApiOperation(value = "小程序登录:如果用户已经绑定了手机号和openId,则直接调用check方法返回token,否则再调用bind-phone方法")
    public ResponseEntity<?> miniAppLogin(@RequestBody @Validated LoginRequestAo loginRequest) {
        String code = loginRequest.getCode();
        String scene = loginRequest.getScene();
        log.info("小程序登录,scene:{},code:{}", scene, code);
        try {
            String sceneCache = redisTemplate.opsForValue().get(BaseConstant.WX_CACHE + scene);
            if (sceneCache == null) {
                throw new CheckedException("二维码已过期");
            }

            WxMaJscode2SessionResult session = wxMaService.getUserService().getSessionInfo(code);
            String sessionKey = session.getSessionKey();
            String openid = session.getOpenid();

            // 检查用户是否已存在openid
            BaseStaff baseStaff = baseStaffService.getByOpenId(openid).orElse(null);
            if (baseStaff != null) {
                // 直接关联token和用户,无需手机号授权
                Long baseStaffId = baseStaff.getId();
                redisTemplate.opsForValue().set(BaseConstant.WX_CACHE + scene, baseStaffId.toString(), 5, TimeUnit.MINUTES);

                log.info("小程序登录:已绑定openid{},scene:{},code:{},staffId:{}", openid, scene, code, baseStaffId);

                return ResponseEntity.ok().body(Collections.singletonMap("action", "LOGIN_SUCCESS"));
            }
            redisTemplate.delete(BaseConstant.WX_CACHE + scene);
            // 将sessionKey与token关联,存储到Redis
            redisTemplate.opsForValue().set(BaseConstant.WX_SESSION_KEY_CACHE_PREFIX + scene, sessionKey + StrPool.AMPERSAND + openid, 5, TimeUnit.MINUTES);

            return ResponseEntity.ok().build();
        } catch (WxErrorException e) {
            return ResponseEntity.status(500).body("登录失败");
        } catch (Exception e) {
            throw new CheckedException(e);
        } finally {
            WxMaConfigHolder.remove();//清理ThreadLocal
        }
    }

    @PostMapping("/bind-phone")
    @ApiOperation(value = "绑定手机号:根据手机号绑定用户openid")
    public ResponseEntity<?> bindPhone(@RequestBody @Validated LoginBindPhoneRequestAo bindPhoneRequest) {
        String scene = bindPhoneRequest.getScene();
        String encryptedData = bindPhoneRequest.getEncryptedData();
        String iv = bindPhoneRequest.getIv();
        log.info("绑定手机号入参,scene:{},encryptedData:{},iv:{}", scene, encryptedData, iv);

        // 从Redis获取sessionKey
        String sessionKeyAndOpenId = redisTemplate.opsForValue().get(BaseConstant.WX_SESSION_KEY_CACHE_PREFIX + scene);
        if (sessionKeyAndOpenId == null) {
            throw new CheckedException("Session已过期");
        }

        try {
            String[] split = sessionKeyAndOpenId.split(StrPool.AMPERSAND);
            String sessionKey = split[0];
            String openId = split[1];

            // 解密手机号
            WxMaPhoneNumberInfo phoneInfo = wxMaService.getUserService().getPhoneNoInfo(sessionKey, encryptedData, iv);
            if (phoneInfo == null || StringUtils.isBlank(phoneInfo.getPurePhoneNumber())) {
                throw new CheckedException("获取手机号失败");
            }

            BaseStaff staff = baseStaffService.getStaffByTelephone(phoneInfo.getPurePhoneNumber());
            ArgumentAssert.notNull(staff, "用户尚未录入,手机号:{}", phoneInfo.getPurePhoneNumber());
            ArgumentAssert.isTrue(StringUtils.isBlank(staff.getOpenId()), "手机号已绑定其他微信账号");

            BaseStaff baseStaff = new BaseStaff();
            baseStaff.setId(staff.getId());
            baseStaff.setOpenId(openId);
            baseStaffService.updateById(baseStaff);

            log.info("用户名已绑定openid:staffName:{},用户ID:{},openid:{}", staff.getStaffName(), staff.getId(), openId);

            redisTemplate.delete(BaseConstant.WX_SESSION_KEY_CACHE_PREFIX + scene);
            // 更新token状态为已确认,并关联用户ID
            redisTemplate.opsForValue().set(BaseConstant.WX_CACHE + scene, baseStaff.getId().toString(), 5, TimeUnit.MINUTES);

            return ResponseEntity.ok().body(Collections.singletonMap("success", true));
        } catch (Exception e) {
            throw new CheckedException(e);
        } finally {
            WxMaConfigHolder.remove();//清理ThreadLocal
        }
    }
}

1.4 小程序前端

1.4.1 页面:pages/index/index.wxml

<!--pages/index/index.wxml-->
<view wx:if="{{showAuthButton}}">
  <button 
    open-type="getPhoneNumber" 
    bindgetphonenumber="handleGetPhoneNumber"
  >授权手机号登录</button>
</view>

1.4.1 js文件

Page({
  data: { 
    showAuthButton: true, // 初始隐藏按钮
    scene: '' 
  },

  onLoad(options) {
    const scene = decodeURIComponent(options.scene);
    this.setData({ scene });
    this.loginAndSendCode(scene);
  },

  // 获取code并发送到后端
  loginAndSendCode(scene) {
    wx.login({
            success: function(res) {
              if (res.code) {
                // 发送 res.code 到后台换取 openId, sessionKey, unionId
                console.log('获取到code:', res.code);
                // 这里可以将code发送到你的服务器进行后续操作
                wx.request({
                  url: 'http://192.168.101.153:8100/base/api/miniapp/login',
                  method: 'post',
                  data: {
                    scene: scene,
                    code:res.code
                  },
                  success: function(res) {
                    console.log(res.data);
                  },
                  fail: function(res) {
                    console.log('请求失败', res);
                  }
                })
      
              } else {
                console.log('登录失败!' + res.errMsg);
              }
            },
            fail: function(err) {
              console.log('登录失败', err);
            }
          });
  },
  // 用户点击授权按钮
  handleGetPhoneNumber(e) {
    const { encryptedData, iv } = e.detail;
    console.log("this.data.scene=",this.data.scene)
    wx.request({
      url: 'http://192.168.101.153:8100/base/api/miniapp/bind-phone',
      method: 'POST',
      data: {
        "encryptedData": encryptedData,
        "iv":iv,
        scene: this.data.scene
      },
      success: (res) => {
        if (res.data.success) {
          wx.showToast({ title: '登录成功' });
        } else {
          wx.showToast({ title: '登录失败,请重试' });
        }
      }
    });
  }
});


<think>好的,用户想实现网页应用通过微信登录后,手机端自动跳转到关联的小程序。需要结合微信登录、网页与小程序的联动,以及OAuth2.0授权机制。首先,我需要理清楚整个流程。 根据提供的引用内容,微信登录网页应用通常涉及OAuth2.0授权,用户二维码后,手机端会进入授权页面,用户确认后网页获取用户信息完成登录。但用户希望手机端在登录后自动跳转小程序,这需要额外的步骤。 首先,微信的网页授权和小程序跳转机制是否允许这样的联动?引用[3]和[4]提到通过配置二维码规则,可以让描普通链接二维码跳转小程序。或许可以将这两个流程结合起来。当用户登录二维码时,网页授权完成后,在重定向的URL中触发小程序跳转。 可能的步骤是:在网页应用的登录流程中,当用户并授权后,服务端在回调处理中生成一个带有小程序路径的URL,然后通过微信的URL Scheme或小程序跳转协议,让手机端自动打开小程序。需要检查微信开放平台的配置,确保网页应用和小程序关联,并且有相同的AppID或者已经绑定在同一个开放平台账号下。 另外,引用[3]提到需要在微信小程序平台配置二维码规则,可能需要将回调URL添加到这些规则中,使得授权完成后跳转到该URL时,微信能识别并打开对应的小程序。此外,可能需要在授权后的重定向URL中使用小程序跳转的特定参数,如appId、path等。 还需要考虑用户授权后的回调处理,服务端在获取到用户信息后,生成一个重定向到小程序的URL,可能使用wx://或者weixin://协议,或者通过URL Scheme实现。但需要注意微信的安全限制,可能需要用户主动触发跳转,或者通过微信JS-SDK来实现。 可能遇到的挑战包括:如何在小程序未启动的情况下通过URL直接跳转,如何处理不同平台(iOS和Android)的兼容性问题,以及确保整个流程符合微信的安全规范,避免被拦截或拒绝。 总结来说,步骤可能包括:配置开放平台关联应用,设置二维码跳转规则,在OAuth2回调中生成小程序跳转链接,处理移动端的跳转逻辑。需要参考微信官方文档,确保每个环节都正确配置,并进行充分的测试。</think>### 分步实现方案 #### 一、前置条件 1. **注册开放平台账号** - 将网页应用与微信小程序关联至同一微信开放平台账号,确保共用同一主体资质[^2] 2. **配置应用信息** - 网页应用需通过审核并获取`AppID`和`AppSecret` - 小程序需发布正式版或体验版,并获取`小程序AppID` #### 二、技术实现流程 1. **生成登录二维码** - 使用微信网页授权接口生成带参数的二维码,格式示例: $$https://open.weixin.qq.com/connect/qrconnect?appid={APPID}&redirect_uri={ENCODED_URL}&scope=snsapi_login$$ - 通过`redirect_uri`参数指定授权后回调地址(需域名备案) 2. **监听授权回调** ```python # 示例:Django处理回调 def wechat_callback(request): code = request.GET.get('code') # 换取access_token和openid res = requests.get(f'https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=APPSECRET&code={code}&grant_type=authorization_code') user_info = res.json() # 此处触发跳转小程序逻辑 return redirect(generate_miniprogram_url(user_info['openid'])) ``` 3. **构造小程序跳转链接** - 使用**URL Scheme**或**H5页面跳转协议**: ```javascript // 方式1:直接生成URL Scheme weixin://dl/business/?appid=小程序APPID&path=页面路径 // 方式2:通过H5页面调用微信JS-SDK wx.miniProgram.navigateTo({ url: '/pages/index/index' }) ``` - 需在微信开放平台配置**业务域名**和**JS接口安全域名**[^3] 4. **配置二维码跳转规则** - 在小程序管理后台添加**普通链接二维码规则** $$规则格式:https://your-domain.com/login?params=.*$$ - 选择“**开发版**”或“**正式版**”匹配策略[^4] #### 三、关键问题解决 1. **跨应用身份关联** - 通过`unionid`实现用户身份绑定(需网页应用和小程序在同一开放平台) 2. **自动跳转限制** - iOS限制:需用户主动触发跳转(可通过中间页引导点击) - Android限制:可通过`Intent`直接唤起小程序 3. **调试注意事项** - 测试阶段使用**体验版小程序**并配置测试链接[^4] - 本地开发时使用`微信开发者工具`的远程调试功能 #### 四、完整时序图 ``` 用户描网页二维码微信服务器授权 → 返回code至服务端 → 换取用户信息 → 生成加密跳转参数 → 返回带小程序Scheme的重定向响应 → 手机浏览器触发Scheme → 微信客户端解析Scheme → 跳转指定小程序页面 ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值