微信小程序+Spring Boot:三步教你搞定微信小程序登录+Token加密+全局拦截器

微信小程序登录功能开发全攻略:从零实现用户秒登录

随着移动互联网的普及,微信作为国民级应用已成为用户登录各类小程序的核心入口。如何在小程序中高效集成微信登录,实现用户身份的快速验证与信息同步,成为开发者必须掌握的关键技能。本文将从零开始,手把手带你完成基于Spring Boot框架的小程序微信登录功能开发,涵盖环境搭建、接口对接、业务逻辑实现及安全校验的全流程。通过本文,你将掌握以下核心能力:

✅ 微信开放平台接口调用与参数解析

✅ 小程序临时登录凭证(Code)的获取与使用

✅ 基于UnionID的用户唯一标识绑定策略

✅ JWT Token动态生成功能与安全拦截器设计

让我们从零起步,一步步构建安全可靠的小程序登录体系!

本篇博客配合黑马程序员项目中州养老讲解~

微信登录

1. 需求分析

打开原型文档: 定位在家属端原型图->登录模块

在任意需要登录才能显示信息的页面点击时,都会判断当前用户是否登录,如未登录,则进入登录页面

点击微信账号登录,弹出弹窗,需要获取用户手机号进行登录

2. 准备小程序环境

提前准备

  1. 申请微信小程序测试号 参考 mfg6vszyp7.feishu.cn/wiki/ZXlpw9...
  2. 安装微信开发者工具:直接到官网下载

2.1 使用微信开发者工具打开项目

然后使用微信开发者工具打开小程序项目:

  1. 打开微信小程序工具,使用自己的微信扫码登录
  1. 登录成功后,点击右上角的 "导入" ,找到刚才解压的小程序代码的目录,然后打开

    如果打开项目后有弹窗是否信任的话,点击"信任并运行"

  2. 打开后的界面

2.2 微信开发者工具的必要设置

2.2.1 修改后端地址
2.2.2 忽略HTTPS请求
2.2.3 取消预览模式

2.3 前后端测试

2.3.1 准备后端代码
放行小程序的所有请求

由于小程序端单独有一套认证校验的逻辑,不需要使用spring security框架来做认证和授权,需要修改SecurityConfig类,对小程序中的所有请求放行。

准备Controller类

将资料中的MemberRoomTypeController类拷贝到zzyl-nursing-platform模块的com.zzyl.serve.controller.member包中

这个Controller类是用于 为小程序查询所有房型的

2.3.2 查看小程序显示效果

打开微信开发者工具,刷新之后 查看一下是否显示出了热门房型

3. 微信登录的思路

我们可以根据微信小程序开放平台给提供的实现思路来梳理微信登录的流程:developers.weixin.qq.com/miniprogram...

注意点:

  • 前端在小程序中集成微信相关依赖,当用户请求登录的同时,调用wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
  • 后端向微信平台发请求,调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID (若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key
  • 开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

4. 表结构与API接口

4.1 表结构

从小程序端登录的用户主要有两类,第一类是参观预约的用户,第二类是老人的家人(子女),方便查看老人信息、给老人下单、查看账单、查看合同等服务。

如果是老人的家人,需要跟入住的老人进行绑定,方便后期享受更多服务,所以,我们需要将在小程序端登录的用户保存下来,保存到一张表中 family_member(家属表)

sql 复制代码
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='老人家属';

4.2 API接口

这个接口跟我们平时开发的接口略有不同,这个需要参考微信开发者平台提供的流程来开发。

目前,小程序的代码已经开发完了,我们需要在后台提供接口就可以了

  • 接口路径(小程序代码中已固定):/member/user/login

  • 请求方式(小程序代码中已固定):POST

  • 请求参数:(小程序代码中已固定)

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

    以上三个参数,都是小程序开发人员调用wx.login()方法得到的数据,再调用我们的这个API接口传递给后端

    • code:临时登录凭证code
    • nickName:微信用户昵称(现在微信统一返回为:微信用户)
    • phoneCode:详细用户信息code,后台根据此code可以获取用户手机号
  • 响应示例

    css 复制代码
    {
      "code": 200,
      "msg": "操作成功",
      "data": {
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLlpb3mn7_lvIDoirE4OTE1IiwiZXhwIjoxNDY1MjI3MTMyOCwidXNlcmlkIjoxfQ.nB6ElZbUywh-yiHDNMJS8WqUpcLWCszVdvAMfySFxIM",
        "nickName": "好柿开花8915"
      }
    }

5. 功能实现

5.1 实现思路

基于微信官方提供的流程图,结合我们的业务,最终的实现思路,如下图

5.2 准备基础代码

按照之前的套路,我们使用代码生成的功能,来生成代码,

  • 包名:com.zzyl.serve
  • 模块名:nursing
  • 除了主键之外的Long类型,改为Integer

⚡️注意:只拷贝后端代码到idea内的zzyl-nursing-platform模块中

5.3 封装微信工具

我们可以先分析微信开发者平台的接口,接口地址:

5.3.1 准备工具类
typescript 复制代码
package com.zzyl.serve.util;
​
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zzyl.common.exception.base.BaseException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
​
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
​
/**
 * @author liuyp
 * @since 2025/8/17
 */
@Component
public class WechatUtil {
    private static final String OPENID_URL = "https://api.weixin.qq.com/sns/jscode2session";
    private static final String PHONE_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";
    private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
​
    @Value("${wechat.appid}")
    private String appid;
    @Value("${wechat.secret}")
    private String secret;
​
    public String getOpenid(String code){
        //准备请求参数
        Map<String, Object> params = new HashMap<>();
        params.put("appid", appid);
        params.put("secret", secret);
        params.put("js_code", code);
        params.put("grant_type", "authorization_code");
​
        //向微信平台API接口发请求,拿code换取openid
        String result = HttpUtil.get(OPENID_URL, params);
​
        //解析结果
        JSONObject jsonResult = JSONUtil.parseObj(result);
        if (Objects.nonNull(jsonResult.getInt("errcode"))) {
            throw new BaseException(jsonResult.getStr("errmsg"));
        }
​
        return jsonResult.getStr("openid");
    }
​
    public String getPhone(String phoneCode){
        //准备请求参数
        Map<String, Object> params = new HashMap<>();
        params.put("code", phoneCode);
​
        //向微信平台API发请求,拿phoneCode换取手机号
        String result = HttpUtil.post(PHONE_URL + getToken(), JSONUtil.toJsonStr(params));
​
        //解析结果
        JSONObject jsonResult = JSONUtil.parseObj(result);
        if (jsonResult.getInt("errcode") != 0) {
            throw new BaseException(jsonResult.getStr("errmsg"));
        }
​
        //获取手机号
        return jsonResult.getJSONObject("phone_info").getStr("phoneNumber");
    }
​
    public String getToken() {
        //准备参数
        Map<String, Object> params = new HashMap<>();
        params.put("grant_type", "client_credential");
        params.put("appid", appid);
        params.put("secret", secret);
​
        //向微信平台发请求,获取微信平台的token
        String result = HttpUtil.get(TOKEN_URL, params);
​
        //解析结果
        JSONObject jsonResult = JSONUtil.parseObj(result);
​
        return jsonResult.getStr("access_token");
    }
}
5.3.2 准备配置参数

application.yml添加对应配置,这里大家注意,这个要跟微信开发者平台设置的相同,就是我们刚才自己申请的测试小程序的appid和appSecret

⚡️注意:使用自己申请的appId和secret,不然小程序无法登录

yaml 复制代码
wechat:
  appid: wxf2a61d4863401d54
  secret: b9f52a8f8a93284d952d74d7895b2515
5.3.3 单元测试

在zzyl-admin中创建单元测试WechatUtilTest

提示:测试中的参数从哪获取呢?在小程序端获取参数,然后输入到单元测试中

typescript 复制代码
@SpringBootTest
public class WechatUtilTest {
​
    @Autowired
    private WechatUtil wechatUtil;
​
    @Test
    public void testGetOpenid() {
        String openid = wechatUtil.getOpenid("0d3fPOkl2VLGbe4IACol2M0x5Q2fPOk5");
        System.out.println(openid);//o3CsK6_C6f4WP9b0AxXNJOkc6q9Q
    }
​
    @Test
    public void testGetPhone() {
        String phone = wechatUtil.getPhone("ff1a2f2e61482537ea19c8e57850919051b43ffadbf78cd4fcd3a1707e0c1e14");
        System.out.println(phone);
    }
}

5.4 控制层

操作步骤:

  1. zzyl-nursing-platform模块中,把FamilyMemberController移动到member包下
  2. 删除FamilyMemberController类中的所有方法,添加登录方法
  3. 基于接口文档创建对应的DTO和VO
5.4.1 准备实体类

准备DTO类 用于接收参数

less 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel("小程序登录实体类")
public class MemberLoginDto {
    @ApiModelProperty("临时凭证")
    private String code;
    @ApiModelProperty("微信昵称")
    private String nickName;
    @ApiModelProperty("手机号")
    private String phoneCode;
}

准备VO类 用于返回结果

less 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberLoginVo {
    @ApiModelProperty("令牌")
    private String token;
    @ApiModelProperty("昵称")
    private String nickName;
}
5.4.2 控制层代码
less 复制代码
@Api(tags = "老人家属管理")
@RestController
@RequestMapping("/member/user")
public class FamilyMemberController extends BaseController
{
    @Autowired
    private IFamilyMemberService familyMemberService;
​
    @PostMapping("/login")
    @ApiOperation("家属小程序登录")
    public AjaxResult login(@RequestBody MemberLoginDto loginDto){
        MemberLoginVo vo = familyMemberService.login(loginDto);
        return AjaxResult.success(vo);
    }
}

5.5 业务层

5.5.1 准备token生成方法

微信登录成功之后,需要生成令牌。而若依项目中已经包含了生成令牌的工具,只要稍做调整即可使用。

  1. 在zzyl-nursing-platform中导入zzyl-framework包
xml 复制代码
<dependency>
    <groupId>com.zzyl</groupId>
    <artifactId>zzyl-framework</artifactId>
</dependency>
  1. 将生成令牌的方法修改为public
go 复制代码
创建JWT的token的时候,可以使用项目中自带的`TokenService`中的方法`createToken`该方法的访问权限为`private`,需要将其设置为`public`
5.5.2 微信登录业务层
实现微信登录功能

在IFamilyMemberService中定义login方法

java 复制代码
/**
 * 微信登录
 * @param loginDto
 * @return
 */
MemberLoginVo login(MemberLoginDto loginDto);

实现方法:

scss 复制代码
   @Autowired
    private WechatUtil wechatUtil;
    @Autowired
    private TokenService tokenService;
    private static final List<String> nickNamePool = List.of("生活更美好",
            "大桔大利",
            "日富一日",
            "好柿开花",
            "柿柿如意",
            "一椰暴富",
            "大柚所为",
            "杨梅吐气",
            "天生荔枝");
​
    @Override
    public MemberLoginVo login(MemberLoginDto loginDto) {
        //访问微信平台,换取微信用户的openid
        String openid = wechatUtil.getOpenid(loginDto.getCode());
​
        //根据openid去数据库中查询此微信用户
        LambdaQueryWrapper<FamilyMember> wrapper = Wrappers.<FamilyMember>lambdaQuery().eq(FamilyMember::getOpenId, openid);
        FamilyMember member = getOne(wrapper);
​
        //访问微信平台,换取微信用户的手机号
        String phone = wechatUtil.getPhone(loginDto.getPhoneCode());
        
        if (member == null) {
            //如果找不到,说明这个微信用户是第一次访问,则准备家属信息对象,新增到数据库中
            String nickName = nickNamePool.get((int) (Math.random() * nickNamePool.size())) + phone.substring(7);
            member = FamilyMember.builder().openId(openid).phone(phone).name(nickName).build();
            save(member);
        }else if (ObjectUtil.notEqual(phone, member.getPhone())) {
            //否则,如果本次提交的手机号 与数据库中手机号不同的话,则更新数据库中的手机号
            member.setPhone(phone);
            updateById(member);
        }
​
        //准备返回值
        MemberLoginVo vo = new MemberLoginVo();
        vo.setNickName(member.getName());
​
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", member.getId());
        claims.put("nickName", member.getName());
        vo.setToken(tokenService.createToken(claims));
        return vo;
    }
}
修改FamilyMember类

在构建Member对象的时候,用到了构建则设计模式,参考mfg6vszyp7.feishu.cn/wiki/IwLCw4...

为实体类添加@Builder注解,并保证已有@NoArgsConstructor和@AllArgsConstructor

5.5.3 调整MP的自动填充逻辑

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

修改后的代码:

java 复制代码
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 {
​
    //注入request对象
    @Autowired
    private HttpServletRequest request;
​
    /**
     * 添加排除方法:如果请求路径以/member开头,就排除掉
     */
    @SneakyThrows
    public boolean isExclude() {
        String requestURI = request.getRequestURI();
        if(requestURI.startsWith("/member")) {
            return true;
        }
        return false;
    }
​
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
        //只有非排除的请求路径 才要设置createBy
        if(!isExclude()) {
            this.strictInsertFill(metaObject, "createBy", String.class, getLoginUserId() + "");
        }
​
    }
​
    @Override
    public void updateFill(MetaObject metaObject) {
        this.setFieldValByName("updateTime", new Date(), metaObject);
        //只有非排除的请求路径 才要设置updateBy
        if(!isExclude()) {
            this.setFieldValByName("updateBy", getLoginUserId() + "", metaObject);
        }
​
    }
​
    /**
     * 获取当前登录人的ID
     *
     * @return
     */
    private static Long getLoginUserId() {
​
        // 获取当前登录人的id
        try {
            LoginUser loginUser = SecurityUtils.getLoginUser();
            if (ObjectUtils.isNotEmpty(loginUser)) {
                return loginUser.getUserId();
            }
            return 1L;
        } catch (Exception e) {
            return 1L;
        }
    }
}

5.5 前后端联调

代码都开发完毕之后,重启服务,然后我们可以继续打开小程序,找到我的, 就可以验证是否能登录成功,详细操作如下图

6. 校验token

6.1 思路分析

通过我们刚才的流程,我们知道,用户登录成功以后,会给前端返回一个token,这个token就是来验证用户信息的,当用户点击了小程序中的其他操作(需要登录),则会把token携带到请求头(header)中,方便后台去验证并获取用户信息,简易流程如下

如果想要验证用户的token,我们可以使用自定义的拦截器实现,整体的流程如下:

6.2 准备ThreadLocal

把资料中提供的工具类UserThreadLocal拷贝到zzyl-common模块中

6.3 编写拦截器

在zzyl-framework模块的com.zzyl.framework.interceptor包中定义拦截器MemberInterceptor

⚡️注意:代码中我们使用了tokenService来解析token,其中的parseToken方法需要修改为public才可以

kotlin 复制代码
package com.zzyl.framework.interceptor;
​
import cn.hutool.core.util.ObjectUtil;
import com.zzyl.common.utils.StringUtils;
import com.zzyl.common.utils.UserThreadLocal;
import com.zzyl.framework.web.service.TokenService;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
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;
​
/**
 * @author liuyp
 * @since 2025/8/17
 */
@Slf4j
@Component
public class MemberInterceptor implements HandlerInterceptor {
    @Autowired
    private TokenService tokenService;
​
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果本次请求的目标资源不是Controller里的方法,则直接放行,不需要处理
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
​
        //获取本次请求携带的令牌
        String token = request.getHeader("authorization");
​
        //如果令牌为空,则直接返回401
        if (StringUtils.isEmpty(token)) {
            response.setStatus(401);
            log.warn("认证失败");
            return false;
        }
​
        //解析令牌
        Claims claims = tokenService.parseToken(token);
        if (ObjectUtil.isEmpty(claims)) {
            response.setStatus(401);
            log.warn("认证失败");
            return false;
        }
​
        //获取令牌中的用户信息
        Long userId = claims.get("userId", Long.class);
        if (userId == null) {
            response.setStatus(401);
            log.warn("认证失败");
            return false;
        }
​
        //将用户id绑定到当前线程上
        UserThreadLocal.set(userId);
        return true;
    }
​
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserThreadLocal.remove();
    }
}

6.4 配置拦截器

找到ResourcesConfig类,这里面定义了拦截器的生效规则,如下图

typescript 复制代码
/**
 * 通用配置
 * 
 * @author liuyp
 */
@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{
    //......其它略......
    
    //↓↓↓注入拦截器对象↓↓↓
    @Autowired
    private MemberInterceptor memberInterceptor;
    //↓↓↓声明要排除不拦截的路径↓↓↓
    private static final String[] EXCLUDE_PATH_PATTERNS = new String[] {
            "/member/user/login",
            "/member/roomTypes"
    };
​
    /**
     * 自定义拦截规则
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
        //↓↓↓配置拦截器↓↓↓
        registry.addInterceptor(memberInterceptor).addPathPatterns("/member/**")
            .excludePathPatterns(EXCLUDE_PATH_PATTERNS);
    }
​
    //......其它略......
}

6.5 验证效果

如果想要测试当前拦截器是否生效,可以随便定义一个控制层代码(需符合拦截要求的),测试代码如下:

这个代码是我们马上要开发的预约管理中的一个需求,这个请求路径是符合拦截需求的

大家可以使用在线接口文档测试或者是直接在小程序中使用:

  • 在登录成功以后,可以点击"参观预约"按钮测试
  • 然后查看idea控制台,是否输出了用户的id。如果可以成功输出,则表示成功
kotlin 复制代码
package com.zzyl.nursing.controller.member;
​
import com.zzyl.common.core.controller.BaseController;
import com.zzyl.common.core.domain.R;
import com.zzyl.common.utils.UserThreadLocal;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * 预约信息Controller
 * 
 * @author ruoyi
 */
@RestController
@RequestMapping("/member/reservation")
@Api(tags =  "预约信息相关接口")
public class MemberReservationController extends BaseController
{
    @GetMapping("/cancelled-count")
    @ApiOperation("查询取消预约数量")
    public R<Integer> getCancelledReservationCount() {
        Long userId = UserThreadLocal.getUserId();
        System.out.println("------" + userId);
        return R.ok(1);
    }
}
相关推荐
ningqw4 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友4 小时前
vi编辑器命令常用操作整理(持续更新)
后端
胡gh5 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫6 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong6 小时前
技术人如何对客做好沟通(上篇)
后端
颜如玉6 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源
Moment7 小时前
毕业一年了,分享一下我的四个开源项目!😊😊😊
前端·后端·开源
why技术7 小时前
在我眼里,这就是天才般的算法!
后端·面试
绝无仅有7 小时前
Jenkins+docker 微服务实现自动化部署安装和部署过程
后端·面试·github
程序视点7 小时前
Escrcpy 3.0投屏控制软件使用教程:无线/有线连接+虚拟显示功能详解
前端·后端