小程序登录概述

小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。

登录流程时序:

登录流程时序说明:

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

wx.login 方法

wx.login 方法用于调用接口获取登录凭证 code,通过凭证进而换取用户登录态信息。

wx.login 方法的基本选项:

  • timeout:超时时间,单位为毫秒。
  • success:调用成功的回调函数,该事件回调函数可以接收一个 res 参数,通过 res.code 可以获取用户登录凭证。用户登录凭证 code 的有效期只有五分钟,开发者需要在开发者服务器后台调用 code2Session,使用 code 换取 openid、unionid、session_key 等信息。
  • fail:调用失败的回调函数。
  • complete:调用结束的回调函数。

code2Session 接口

code2Session 接口用于登录凭证校验,通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。

code2Session 接口的调用方式:

GET https://api.weixin.qq.com/sns/jscode2session

code2Session 接口的基本请求参数:

  • appid:小程序 AppId。
  • secret:小程序 AppSecret。
  • js_code:前端传递的用户登录凭证 code,通过 wx.login 获取。一个用户登录凭证 code 只能使用一次。
  • grant_type:授权类型,此处只需填写 authorization_code。

code2Session 接口的基本返回参数:

  • session_key:会话密钥,是对用户数据进行加密签名的密钥,用于解密用户数据。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥。
  • unionid:用户在开放平台的唯一标识符,若当前小程序已绑定到微信开放平台账号下会返回。
  • openid:用户唯一标识。

小程序登录实现

模拟微信小程序用户授权登录功能的数据库设计:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `openid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'openid',
  `unionid` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'unionid',
  `login_time` bigint DEFAULT NULL COMMENT '登录时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

使用 IntelliJ IDEA 创建 Spring Boot 项目,并导入相关依赖。

Java 后端实现微信小程序用户授权登录功能,可以借助:

  • Sa-Token:轻量级 Java 权限认证框架,让鉴权变得简单、优雅。
  • WxJava:微信 Java 开发工具包,支持包括微信支付、开放平台、公众号、企业微信、视频号、小程序等微信功能模块的后端开发。

Maven 项目配置文件(pom.xml):

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.32</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.7</version>
</dependency>
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-spring-boot-starter</artifactId>
    <version>1.39.0</version>
</dependency>
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-miniapp</artifactId>
    <version>4.6.0</version>
</dependency>

项目配置文件(classpath:application.properties):

# MySQL数据库配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/ma_db?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456..
server.port=8080

# MyBatis Plus配置
mybatis-plus.mapper-locations=classpath:mappers/*.xml
mybatis-plus.configuration.auto-mapping-behavior=FULL

# Sa-Token配置
sa-token.token-name=Authorization
sa-token.token-style=uuid
sa-token.is-log=true

借助插件生成用户表的实体类、Mapper 接口及其映射文件、业务逻辑层接口及其实现类。

用户控制器(cn.duozai.malogin.controller.UsersController):

@RestController
@RequestMapping("/api")
public class UsersController {

    @Resource
    UsersService usersService;

    /**
     * 获取当前登录的用户信息
     *
     * @return java.lang.String
     */
    @GetMapping("/getUserInfo")
    public String getUserInfo() {
        // 借助 Sa-Token 验证是否已登录
        if(!StpUtil.isLogin()) {
            // 未登录,返回失败结果
            return "failure: 请先登录";
        }

        // 已登录,获取当前登录的用户的id,并查询用户数据
        Integer loginId = StpUtil.getLoginIdAsInt();
        Users userObject = usersService.getById(loginId);

        // 返回用户实体对象
        return JSONUtil.toJsonStr(userObject);
    }

}

使用微信开发者工具创建小程序前端项目,并设置不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS。

首页页面结构(pages/index/index.wxml):

<button bindtap="getUserInfo" style="margin: 10px auto;">获取用户信息</button>

首页页面逻辑(pages/index/index.js):

Page({
    getUserInfo() {
        wx.request({
            url: "http://localhost:8080/api/getUserInfo",
            method: "GET",
            success: res => {
                console.log(res)
            }
        })
    }
})

测试结果:

借助 Sa-Token 进行登录权限验证,前后端分离项目要求前端在发送请求时验证请求头中的 token,以判断用户是否登录。

在未进行授权登录的情况下,无法获取已登录的用户信息,因此执行操作后,后端返回 failure 的结果给小程序前端。‍

Java 后端实现小程序授权登录,可以结合 WxJava 提供的相关能力。

WxJava 微信小程序服务配置类(cn.duozai.malogin.wxjava.WxMaServiceConfig):

@Configuration  // 标记为配置类
public class WxMaServiceConfig {

    @Bean
    public WxMaService wxMaService() {
        // 实例化WxJava微信小程序服务类对象
        WxMaService wxMaService = new WxMaServiceImpl();

        // 实例化微信小程序配置类对象
        WxMaDefaultConfigImpl wxMaConfig = new WxMaDefaultConfigImpl();
        // 设置微信小程序的appid和appsecret
        wxMaConfig.setAppid("wx676ed081a4ed9be4");
        wxMaConfig.setSecret("24545f1a52aa287217bea08604c66f79");

        // 将配置类对象注入到WxMaService中
        wxMaService.setWxMaConfig(wxMaConfig);

        // 返回WxMaService对象
        return wxMaService;
    }

}

用户控制器(cn.duozai.malogin.controller.UsersController):

@RestController
@RequestMapping("/api")
public class UsersController {

    @Resource
    WxMaService wxMaService;
    @Resource
    UsersService usersService;

    // ...

    /**
     * 微信小程序授权登录
     *
     * @param code 用户登录凭证
     *             小程序前端通过wx.login获取,通过wx.request传递
     * @return java.lang.String
     */
    @PostMapping("/login")
    public String login(@RequestParam String code) {
        WxMaJscode2SessionResult sessionInfo = null;

        try {
              // 借助WxJava提供的方法,快速调用小程序的code2Session接口
              // 其本质是发送请求到微信平台的code2Session接口,获得返回数据
            // 即登录凭证校验,传递appid+appsecret+code
            sessionInfo = wxMaService.getUserService()
                    .getSessionInfo(code);

            // 登录凭证校验完毕,返回session_key+openid+unionid等数据
            String openid = sessionInfo.getOpenid();
            String unionid = sessionInfo.getUnionid();
            String sessionKey = sessionInfo.getSessionKey();

            // 将用户信息保存到数据库
            // 根据openid查询数据库中是否存在该用户
            Users userObject = usersService.getOne(new LambdaQueryWrapper<Users>()
                    .eq(Users::getOpenid, openid));

            // 判断用户是否已存在,用户不存在则新增
            if(userObject == null) {
                // 创建新用户对象
                userObject = new Users();
                userObject.setOpenid(openid);
                userObject.setUnionid(unionid);
            }

            // 设置登录时间
            userObject.setLoginTime(DateUtil.currentSeconds());

            // 新增/修改用户数据
            usersService.saveOrUpdate(userObject);

            // 调用Sa-token实现登录
            StpUtil.login(userObject.getId());

            // 返回Sa-Token生成的token令牌给前端
            // 即自定义登录态,前端保存后每次请求都携带token
            return StpUtil.getTokenValue();
        } catch (WxErrorException e) {
            // 登录失败,返回失败结果
            return "failure:" + e.getMessage();
        }
    }

}

首页页面结构(pages/index/index.wxml):

<button bindtap="login" style="margin: 10px auto;">微信小程序授权登录</button>

首页页面逻辑(pages/index/index.js):

Page({
    getUserInfo() {
        wx.request({
            url: "http://localhost:8080/api/getUserInfo",
            method: "GET",
            header: {
                // 在请求头中设置token
                // 请求时携带自定义登录态
                "Authorization": wx.getStorageSync("authorizationToken")
            },
            success: res => {
                console.log(res)
            }
        })
    },
    login() {
        wx.login({
            success: res => {
                console.log("用户登录凭证:" + res.code)

                wx.request({
                    url: "http://localhost:8080/api/login",
                    method: 'POST',
                    data: {
                        code: res.code
                    },
                    header: {
                        "Content-Type": "application/x-www-form-urlencoded"
                    },
                    success: res => {
                        console.log(res)

                        // 将后端返回给小程序前端的token保存到Storage中
                        // 即自定义登录态存入Storage
                        wx.setStorageSync("authorizationToken", res.data)
                    }
                })
            }
        })
    }
})

测试结果:

流程总结

小程序授权登录流程总结:

  • 小程序前端通过 wx.login 方法获取用户登录凭证 code。
  • 小程序前端获取到用户登录凭证后,通过 wx.request 发送请求到后端进行登录验证,并将用户登录凭证传递给后端接口。
  • Java 后端借助 WxJava 提供的相关方法,快速调用微信平台的 code2Session 接口,其本质上向微信平台的 code2Session 接口传递了 appid + appsecret + code 参数,进行登录验证。
  • 登录凭证校验完毕,微信平台的 code2Session 接口返回 session_key + openid + unionid 等数据。
  • Java 后端通过 openid 验证用户是否存在,数据库中用户数据不存在即创建用户,用户数据存在即进行登录操作。
  • 用户数据存在,借助 Sa-Token 执行登录操作,并返回由 Sa-Token 生成的 token 令牌给小程序前端,即通过 openid 与数据库中的用户数据进行关联,并自定义登录态,返回自定义登录态给小程序前端。
  • 小程序前端接收到 token 令牌后,将其保存到 Storage 中。
  • 小程序前端使用 wx.request 发送业务请求时,在请求头中携带 token 令牌参数。
  • Java 后端借助 Sa-Token 进行登录校验,token 令牌有效即已登录,可以执行业务操作。