Implementing WeChat Mini‑Program Login with Java Backend and Token Authentication

This guide walks through building a WeChat mini‑program login system using Java backend services, covering the retrieval of openId and phone number via WeChat APIs, database design, token generation, and request interception for secure authentication.

Architect's Tech Stack
Architect's Tech Stack
Architect's Tech Stack
Implementing WeChat Mini‑Program Login with Java Backend and Token Authentication

Preface

Mini‑program login is a common requirement. Even if you are not responsible for implementing it, you need to understand the process because later you will use openId and unionId.

Requirement Analysis

Clicking the login button shows a popup and requires the user's phone number for login.

WeChat login business logic diagram:

img_1
img_1

WeChat login flow diagram:

img_3
img_3

Frontend calls wx.login to obtain a temporary login credential code and passes it to the backend.

Backend calls auth.code2Session to exchange the code for openId , unionId and a session key.

The server can use these identifiers to maintain login state for subsequent business logic.

Table Structure Explanation

Create a table to store user information and openId.

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

API Description

The API differs from ordinary APIs; follow the WeChat developer platform flow.

Request parameters example:

{
  "code": "0e36jkGa1ercRF0Fu4Ia1V3fPD06jkGW", // temporary login credential
  "nickName": "微信用户",
  "phoneCode": "13fe315872a4fb9ed3deee1e5909d5af60dfce7911013436fddcfe13f55ecad3"
}

code: temporary login credential (valid for 5 minutes)

nickName: user nickname (currently always "微信用户")

phoneCode: detailed user information code used to obtain the phone number

Response example:

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

Mini‑Program Environment Setup

Necessary Configuration

During testing use a test account to obtain appId and secret; both frontend and backend need these parameters.

img_5
img_5

Basic Environment Description

Modify request path, ignore HTTPS verification locally, and replace the mini‑program APPID with your test APPID.

img_8
img_8

Feature Implementation

Implementation Idea

img_9
img_9

Controller Layer

@PostMapping("/login")
@ApiOperation("Mini‑program login")
public AjaxResult login(@RequestBody UserLoginRequestDto userLoginRequestDto) {
    LoginVo loginVo = familyMemberService.login(userLoginRequestDto);
    return success(loginVo);
}

UserLoginRequestDTO definition:

package com.zzyl.nursing.dto;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * C‑end user login
 */
@Data
public class UserLoginRequestDto {
    @ApiModelProperty("昵称")
    private String nickName;
    @ApiModelProperty("登录临时凭证")
    private String code;
    @ApiModelProperty("手机号临时凭证")
    private String phoneCode;
}

LoginVo definition:

package com.zzyl.nursing.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * LoginVO
 */
@Data
@ApiModel(value = "登录对象")
public class LoginVo {
    @ApiModelProperty(value = "JWT token")
    private String token;
    @ApiModelProperty(value = "昵称")
    private String nickName;
}

Business Layer (Important)

Encapsulate third‑party calls to obtain openId, phone number, and token.

package com.zzyl.nursing.service;

public interface WechatService {
    /**
     * Get openId
     * @param code temporary login credential
     * @return openId
     */
    String getOpenid(String code);
    /**
     * Get phone number
     * @param detailCode detailed code
     * @return phone number
     */
    String getPhone(String detailCode);
}

Implementation:

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";
    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;

    @Override
    public String getOpenid(String code) {
        Map<String, Object> paramMap = getAppConfig();
        paramMap.put("js_code", code);
        String result = HttpUtil.get(REQUEST_URL, paramMap);
        JSONObject jsonObject = JSONUtil.parseObj(result);
        if (ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))) {
            throw new RuntimeException(jsonObject.getStr("errmsg"));
        }
        return jsonObject.getStr("openid");
    }

    private Map<String, Object> getAppConfig() {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("appid", appid);
        paramMap.put("secret", secret);
        return paramMap;
    }

    @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));
        JSONObject jsonObject = JSONUtil.parseObj(result);
        if (jsonObject.getInt("errcode") != 0) {
            throw new RuntimeException(jsonObject.getStr("errmsg"));
        }
        return jsonObject.getJSONObject("phone_info").getStr("phoneNumber");
    }

    private String getToken() {
        Map<String, Object> paramMap = getAppConfig();
        String result = HttpUtil.get(TOKEN_URL, paramMap);
        JSONObject jsonObject = JSONUtil.parseObj(result);
        if (ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))) {
            throw new RuntimeException(jsonObject.getStr("errmsg"));
        }
        return jsonObject.getStr("access_token");
    }
}

Login service method:

public LoginVo login(UserLoginRequestDto dto) {
    // 1. Get openId from WeChat
    String openId = wechatService.getOpenid(dto.getCode());
    // 2. Query user by openId
    FamilyMember member = getOne(Wrappers.<FamilyMember>lambdaQuery().eq(FamilyMember::getOpenId, openId));
    // 3. If user does not exist, create a new one
    if (member == null) {
        member = FamilyMember.builder().openId(openId).build();
    }
    // 4. Get phone number from WeChat
    String phone = wechatService.getPhone(dto.getPhoneCode());
    // 5. Save or update user information
    saveOrUpdateFamilyMember(member, phone);
    // 6. Generate token containing userId and userName
    Map<String, Object> claims = new HashMap<>();
    claims.put("userId", member.getId());
    claims.put("userName", member.getName());
    String token = tokenService.createToken(claims);
    LoginVo vo = new LoginVo();
    vo.setToken(token);
    vo.setNickName(member.getName());
    return vo;
}

private void saveOrUpdateFamilyMember(FamilyMember member, String phone) {
    if (!ObjectUtil.equals(phone, member.getPhone())) {
        member.setPhone(phone);
    }
    if (ObjectUtil.isNotEmpty(member.getId())) {
        updateById(member);
        return;
    }
    // Generate a random nickname
    String nickName = DEFAULT_NICKNAME_PREFIX.get((int) (Math.random() * DEFAULT_NICKNAME_PREFIX.size()))
            + phone.substring(7);
    member.setName(nickName);
    save(member);
}

Token Validation

After login, the token is returned to the frontend and included in the request header for subsequent operations. An interceptor validates the 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 {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        String token = request.getHeader("authorization");
        if (StringUtils.isEmpty(token)) {
            throw new BaseException("Authentication failed");
        }
        Map<String, Object> claims = tokenService.parseToken(token);
        if (ObjectUtil.isEmpty(claims)) {
            throw new BaseException("Authentication failed");
        }
        Long userId = MapUtil.get(claims, "userId", Long.class);
        if (ObjectUtil.isEmpty(userId)) {
            throw new BaseException("Authentication failed");
        }
        UserThreadLocal.set(userId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserThreadLocal.remove();
    }
}

The interceptor is registered in a WebMvcConfigurer implementation to apply it to the desired URL patterns.

Summary

openId

uniquely identifies a user within a specific mini‑program; unionId is the same across multiple mini‑programs under the same WeChat open platform.

The frontend uses wx.login to obtain a temporary code, which the backend exchanges for openId.

To retrieve the user's phone number, the backend first obtains an access token and then calls the WeChat phone‑number API.

backendJavaMiniProgramAPIloginTokenWeChat
Architect's Tech Stack
Written by

Architect's Tech Stack

Java backend, microservices, distributed systems, containerized programming, and more.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.