文章目录
一、角色及流程
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: '登录失败,请重试' });
}
}
});
}
});
4556

被折叠的 条评论
为什么被折叠?



