微信登录之OpenID与UnionID获取全流程解析

点击上方"程序员蜗牛g",选择"设为星标"

跟蜗牛哥一起,每天进步一点点

程序员蜗牛g

大厂程序员一枚 跟蜗牛一起 每天进步一点点

33篇原创内容

**

公众号

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

需求分析

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

微信登录业务逻辑规则:

思路说明

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

developers.weixin.qq.com/miniprogram...

微信官方推荐登录流程:

注意点:

  • • 前端在小程序集成微信相关依赖,调用wx.login获取临时登录凭证code,传给后端。
  • • 后端调用auth.code2Session接口,换取openId和、UnionId、会话秘钥Session_Key
  • • 开发者服务器可以根据用户标识自定义登录状态,用于后续业务逻辑中前后端交互识别用户身份。

表结构说明

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

建表语句:

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='老人家属';

接口说明

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

请求参数:

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

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

  • code: 临时登录凭证code(有效时间5分钟)
  • nickName: 微信用户昵称(现在统一返回:微信用户)
  • phoneCode: 详细用户信息code,后台根据此code获取手机号。

响应示例:

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

小程序环境搭建

必要配置

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

基础环境说明

修改请求路径

本地开发忽略https校验

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

功能实现

实现思路

null

控制层

Controller:

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

}

UserLoginRequestDTO:

kotlin 复制代码
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:

kotlin 复制代码
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接口:

typescript 复制代码
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实现类:

typescript 复制代码
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配置对于配置。

null

微信登录业务开发

java 复制代码
/**
 * 微信登录
 * @param userLoginRequestDto
 * @return
 */
LoginVo login(UserLoginRequestDto userLoginRequestDto);

实现方法:

scss 复制代码
@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的自动填充。

typescript 复制代码
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")){
            return false;
        }
        return true;
    }
 
    @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中,方便后台去验证获取用户信息,流程如下:

null

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

null

代码如下:

java 复制代码
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)){
            return true;
        }
 
        //获取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);
        return true;
 
    }
 
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserThreadLocal.remove();
    }
}

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

scss 复制代码
/**
 * 自定义拦截规则
 */
@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,然后再去获取手机号。

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记 就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

相关推荐
天天摸鱼的java工程师7 小时前
线程池深度解析:核心参数 + 拒绝策略 + 动态调整实战
java·后端
小杨同学497 小时前
C 语言实战:动态规划求解最长公共子串(连续),附完整实现与优化
后端
Cache技术分享7 小时前
290. Java Stream API - 从文本文件的行创建 Stream
前端·后端
用户948357016517 小时前
拒绝 try-catch:如何设计全局通用的异常拦截体系?
后端
golang学习记7 小时前
Go 1.22 隐藏彩蛋:cmp.Or —— 让“默认值”写起来像呼吸一样自然!
后端
阿里巴巴P8高级架构师7 小时前
从0到1:用 Spring Boot 4 + Java 21 打造一个智能AI面试官平台
java·后端
桦说编程8 小时前
并发编程踩坑实录:这些原则,帮你少走80%的弯路
java·后端·性能优化
小杨同学498 小时前
C 语言实战:枚举类型实现数字转星期(输入 1~7 对应星期几)
前端·后端
用户8307196840828 小时前
Shiro登录验证与鉴权核心流程详解
spring boot·后端
码头整点薯条8 小时前
基于Java实现的简易规则引擎(日常开发难点记录)
java·后端