当前位置: 首页 > news >正文

Spring Boot 实现微信登录,So Easy !

前言

小程序登录在开发中是最常见的需求,哪怕小程序登录不是你做,你还是要了解一下流程,后续都要使用到openId和unionId,你需要知道这些是干什么的。

需求分析

点击登录会弹出弹窗,需要获取用户手机号进行登录。

微信登录业务逻辑规则:

思路说明

参考微信官方文档的提供的思路,官方文档:

https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

微信官方推荐登录流程:

注意点:

  • • 前端在小程序集成微信相关依赖,调用wx.login获取临时登录凭证code,传给后端。

  • • 后端调用auth.code2Session接口,换取openId和、UnionId、会话秘钥Session_Key

  • • 开发者服务器可以根据用户标识自定义登录状态,用于后续业务逻辑中前后端交互识别用户身份。

表结构说明

创建一张表,用于存储用户的信息以及oenId

建表语句:

CREATE TABLE "family_member" ( "id" bigint NOT NULL AUTO_INCREMENT COMMENT '主键', "phone" varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号', "name" varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名称', "avatar" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '头像', "open_id" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'OpenID', "gender" int DEFAULT NULL COMMENT '性别(0:男,1:女)', "create_time" timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', "update_time" timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', "create_by" bigint DEFAULT NULL COMMENT '创建人', "update_by" bigint DEFAULT NULL COMMENT '更新人', "remark" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', PRIMARY KEY ("id") USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='老人家属';

接口说明

接口跟平时的接口略有不同,参考微信开发者平台提供的流程开发。

请求参数:

{ "code": "0e36jkGa1ercRF0Fu4Ia1V3fPD06jkGW", //临时登录凭证code "nickName": "微信用户", "phoneCode": "13fe315872a4fb9ed3deee1e5909d5af60dfce7911013436fddcfe13f55ecad3" }

以上三个参数都是前端调用wx.login获取返回的参数

  • code:临时登录凭证code(有效时间5分钟)

  • nickName:微信用户昵称(现在统一返回:微信用户)

  • phoneCode:详细用户信息code,后台根据此code获取手机号。

响应示例:

{ "code": 200, "msg": "操作成功", "data": { "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLlpb3mn7_lvIDoirE4OTE1IiwiZXhwIjoxNDY1MjI3MTMyOCwidXNlcmlkIjoxfQ.nB6ElZbUywh-yiHDNMJS8WqUpcLWCszVdvAMfySFxIM", "nickName": "好柿开花8915" }, "operationTime": null }

小程序环境搭建

必要配置

测试阶段使用测试号,在微信小程序后台获取appId和小程序秘钥,前端和后端都需要这两个参数。

基础环境说明

修改请求路径

本地开发忽略https校验

修改小程序环境的APPID,改为自己申请的测试号APPID。

功能实现

实现思路

控制层

Controller:

@PostMapping("/login") @ApiOperation("小程序登录") public AjaxResult login(@RequestBody UserLoginRequestDto userLoginRequestDto){ LoginVo loginVo = familyMemberService.login(userLoginRequestDto); return success(loginVo); }

UserLoginRequestDTO:

package com.zzyl.nursing.dto; import io.swagger.annotations.ApiModelProperty; import lombok.Data; /** * C端用户登录 */ @Data public class UserLoginRequestDto { @ApiModelProperty("昵称") private String nickName; @ApiModelProperty("登录临时凭证") private String code; @ApiModelProperty("手机号临时凭证") private String phoneCode; }

LoginVo:

package com.zzyl.nursing.vo; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; /** * LoginVO * @author itheima */ @Data @ApiModel(value = "登录对象") public class LoginVo { @ApiModelProperty(value = "JWT token") private String token; @ApiModelProperty(value = "昵称") private String nickName; }

业务层【重要】

一般像这种三方接口调用,通常会封装一个单独业务代码,使其更通用。

  • • 获取用户openId

  • • 获取手机号

  • • 获取token(获取手机号需要)

微信接口调用-单独封装

新增WeachatService接口:

package com.zzyl.nursing.service; public interface WechatService { /** * 获取openid * @param code * @return */ public String getOpenid(String code); /** * 获取手机号 * @param detailCode * @return */ public String getPhone(String detailCode); }

新增WeachatServiceImpl实现类:

package com.zzyl.nursing.service.impl; import cn.hutool.core.util.ObjectUtil; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; import com.zzyl.nursing.service.WechatService; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; @Service public class WechatServiceImpl implements WechatService { // 登录 private static final String REQUEST_URL = "https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code"; // 获取token private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential"; // 获取手机号 private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token="; @Value("${wechat.appId}") private String appid; @Value("${wechat.appSecret}") private String secret; /** * 获取openid * @param code * @return */ @Override public String getOpenid(String code) { //获取公共参数 Map<String,Object> paramMap = getAppConfig(); paramMap.put("js_code",code); String result = HttpUtil.get(REQUEST_URL, paramMap); //是一个map JSONObject jsonObject = JSONUtil.parseObj(result); //判断接口响应是否出错 if(ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))){ throw new RuntimeException(jsonObject.getStr("errmsg")); } String openid = jsonObject.getStr("openid"); return openid; } /** * 封装公共参数 * @return */ private Map<String, Object> getAppConfig() { Map<String, Object> paramMap = new HashMap<>(); paramMap.put("appid",appid); paramMap.put("secret",secret); return paramMap; } /** * 获取手机号 * @param detailCode * @return */ @Override public String getPhone(String detailCode) { String token = getToken(); String url = PHONE_REQUEST_URL+token; Map<String, Object> paramMap = new HashMap<>(); paramMap.put("code",detailCode); //发起请求 String result = HttpUtil.post(url, JSONUtil.toJsonStr(paramMap)); //是一个map JSONObject jsonObject = JSONUtil.parseObj(result); //判断接口响应是否出错 if(jsonObject.getInt("errcode") != 0){ throw new RuntimeException(jsonObject.getStr("errmsg")); } return jsonObject.getJSONObject("phone_info").getStr("phoneNumber"); } /** * 获取token * @return */ private String getToken() { Map<String, Object> paramMap = getAppConfig(); //发起请求 String result = HttpUtil.get(TOKEN_URL, paramMap); //是一个map JSONObject jsonObject = JSONUtil.parseObj(result); //判断接口响应是否出错 if(ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))){ throw new RuntimeException(jsonObject.getStr("errmsg")); } String token = jsonObject.getStr("access_token"); return token; } }

上面的代码需要读取获取appIdappSecret,所以我们在application.yml配置对于配置。

微信登录业务开发

/** * 微信登录 * @param userLoginRequestDto * @return */ LoginVo login(UserLoginRequestDto userLoginRequestDto);

实现方法:

@Autowired private WechatService wechatService; @Autowired private TokenService tokenService; static List<String> DEFAULT_NICKNAME_PREFIX = ListUtil.of("生活更美好", "大桔大利", "日富一日", "好柿开花", "柿柿如意", "一椰暴富", "大柚所为", "杨梅吐气", "天生荔枝" ); /** * 小程序端登录 * @param userLoginRequestDto * @return */ @Override public LoginVo login(UserLoginRequestDto userLoginRequestDto) { //1.调用微信api,根据code获取openId String openId = wechatService.getOpenid(userLoginRequestDto.getCode()); //2.根据openId查询用户 FamilyMember familyMember = getOne(Wrappers.<FamilyMember>lambdaQuery(FamilyMember.class) .eq(FamilyMember::getOpenId, openId)); //3.如果用户为空,则新增 if (ObjectUtil.isEmpty(familyMember)) { familyMember = FamilyMember.builder().openId(openId).build(); } //4.调用微信api获取用户绑定的手机号 String phone = wechatService.getPhone(userLoginRequestDto.getPhoneCode()); //5.保存或修改用户 saveOrUpdateFamilyMember(familyMember, phone); //6.将用户id存入token,返回 Map<String, Object> claims = new HashMap<>(); claims.put("userId", familyMember.getId()); claims.put("userName", familyMember.getName()); String token = tokenService.createToken(claims); LoginVo loginVo = new LoginVo(); loginVo.setToken(token); loginVo.setNickName(familyMember.getName()); return loginVo; } /** * 保存或修改客户 * @param member * @param phone */ private void saveOrUpdateFamilyMember(FamilyMember member, String phone) { //1.判断取到的手机号与数据库中保存的手机号不一样 if(ObjectUtil.notEqual(phone, member.getPhone())){ //设置手机号 member.setPhone(phone); } //2.判断id存在 if (ObjectUtil.isNotEmpty(member.getId())) { updateById(familyMember); return; } //3.保存新的用户 //随机组装昵称,词组+手机号后四位 String nickName = DEFAULT_NICKNAME_PREFIX.get((int) (Math.random() * DEFAULT_NICKNAME_PREFIX.size())) + StringUtils.substring(member.getPhone(), 7); member.setName(nickName); save(member); }

注意:

小程序所有请求不走后台的用户,所以在新增或修改的时候,不需要自动填充创建人和修改人,修改MP的自动填充。

package com.zzyl.framework.interceptor; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.zzyl.common.core.domain.model.LoginUser; import com.zzyl.common.utils.SecurityUtils; import lombok.SneakyThrows; import org.apache.commons.lang3.ObjectUtils; import org.apache.ibatis.reflection.MetaObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.Date; import java.util.List; @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Autowired private HttpServletRequest request; @SneakyThrows public boolean isExclude() { String requestURI = request.getRequestURI(); if(requestURI.startsWith("/member")){ returnfalse; } returntrue; } @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); if(isExclude()){ this.strictInsertFill(metaObject, "createBy", String.class, loadUserId() + ""); } } @Override public void updateFill(MetaObject metaObject) { this.setFieldValByName("updateTime", new Date(), metaObject); if(isExclude()){ this.setFieldValByName("updateBy", loadUserId() + "", metaObject); } } /** * 获取当前登录人的ID * * @return */ private static Long loadUserId() { //获取当前登录人的id try { LoginUser loginUser = SecurityUtils.getLoginUser(); if (ObjectUtils.isNotEmpty(loginUser)) { return loginUser.getUserId(); } return 1L; } catch (Exception e) { return 1L; } } }

校验Toeken

思路分析

用户登录成功之后,返回前端一个token,这个token就是用来验证用户信息的,用户点击小程序中的其他操作,就会token携带请求头header中,方便后台去验证获取用户信息,流程如下:

如果要验证用户的token,我们可以使用拦截器实现。

代码如下:

package com.zzyl.framework.interceptor; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; import com.zzyl.common.exception.base.BaseException; import com.zzyl.common.utils.StringUtils; import com.zzyl.common.utils.UserThreadLocal; import com.zzyl.framework.web.service.TokenService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; @Component public class MemberInterceptor implements HandlerInterceptor { @Autowired private TokenService tokenService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前请求是否是handler() if(!(handler instanceof HandlerMethod)){ returntrue; } //获取token String token = request.getHeader("authorization"); if(StringUtils.isEmpty(token)){ throw new BaseException("认证失败"); } //解析token Map<String, Object> claims = tokenService.parseToken(token); if(ObjectUtil.isEmpty(claims)){ throw new BaseException("认证失败"); } Long userId = MapUtil.get(claims, "userId", Long.class); if(ObjectUtil.isEmpty(userId)){ throw new BaseException("认证失败"); } //把数据存储到线程中 UserThreadLocal.set(userId); returntrue; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserThreadLocal.remove(); } }

使拦截器生效(WebMvcConfigurer实现类):

/** * 自定义拦截规则 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**"); registry.addInterceptor(membersInterceptor).excludePathPatterns(EXCLUDE_PATH_PATTERNS).addPathPatterns("/member/**"); }

总结

  • openId是用户在这个小程序的唯一标识,unionId是微信是你在微信开发平台的唯一标识,就是多个小程序中你的unionId都是一样的。

  • • 前端wx.login获取临时登录code,传给后端,后端用来换取openId

  • • 获取手机号需要先获取token,然后再去获取手机号。

http://www.jsqmd.com/news/437258/

相关文章:

  • 收藏级强化学习入门|小白程序员必看,从基础到Agentic-RL全解析
  • 京东SP开奖!最高20薪年包52W,小白程序员必看:面试重点+薪资拆解建议收藏
  • 网络安全基础知识超全整理:零基础也能看懂,手把手带你入门(附思维导图)【无标题】
  • Java 高频面试题总结(2026通用版)
  • 如何通过AI获客?联系哪家公司? - 品牌2026
  • Agent开发学习,小白程序员看过来,收藏这份大模型学习路线!
  • 网络安全(Cybersecurity)是什么?一文带你快速入门,零基础必读!
  • 保姆级Web安全学习路线:涵盖所有知识点,新手也能成为大牛!
  • 2026化工厂废水处理优质公司推荐榜 - 优质品牌商家
  • 掌握 Embedding 和向量数据库:AI 灵魂的挖掘与检索,收藏这份干货指南!
  • SolidWorks二次开发(C#)-CustomPropertyManager.GetType2自定义属性管理器-获取属性类型
  • OpenClaw + macOS(Mac mini)+ Tailscale Serve 远程访问踩坑全记录:从“能打开但离线”到稳定可用的修复路线
  • 从原理到产业:一文读懂扩散模型图像生成的现在与未来
  • SolidWorks二次开发(C#)-CustomPropertyManager.GetAll3自定义属性管理器-获取所有属性
  • 代码随想录算法训练营第一天 | 数组概念、二分查找、双指针
  • SolidWorks二次开发(C#)-CustomPropertyManager.Add3自定义属性管理器-添加属性
  • # 告别分类器!深入浅出Classifier-Free Guidance技术全景
  • AI时代如何获客?2026特色GEO服务商推荐 - 品牌2026
  • YOLO real-time object detectors All In One
  • 机器学习算法,半监督学习可以实现什么功能?
  • 扩散模型采样器全解:从原理到产业,掌握生成速度与质量的平衡术
  • 2026年3月深圳舆情监控软件公司推荐:行业权威盘点与品质红榜发布 - 品牌鉴赏师
  • 与RabbitMQ 相比,Kafka 有哪些优势?
  • 搭建python自动化测试环境
  • 在 Mac 电脑上连接小米手机传输文件
  • AI产品必懂的100个概念(非常详细),AIGC全赛道从入门到精通,收藏这一篇就够了!
  • DRF学习
  • 邦芒干货:新人简历自我评价的三段位进阶
  • AI时代如何获客?2026特色GEO服务商测评 - 品牌2026
  • CVPR和Nature的共同选择,这种多模态信息融合思路真的需要好好学习一下!