前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >从零玩转系列之腾讯云微信扫码授权系统

从零玩转系列之腾讯云微信扫码授权系统

原创
作者头像
杨不易呀
修改2024-12-03 07:34:09
修改2024-12-03 07:34:09
1.5K16
举报
文章被收录于专栏:杨不易呀杨不易呀

推荐

在文章开始之前,推荐一些很值得阅读的好文章!感兴趣的也可以去看一下哦!

推荐文章

推荐原因

立即前往

『前端必修课』视频文字特效

这篇文章是腾讯云开发者社区的BNTang的“前端必修课”系列之一 在这篇文章中,介绍了如何实现视频文字特效。文章总结了通过mix-blend-mode实现视频与文字融合特效的方法,以及如何通过CSS的object-fit属性让视频适应容器尺寸. 感兴趣的同学快来阅读吧~

前言

halo 好久不见,我是杨不易呀,本片文章是写给热爱折腾的技术小伙伴们!

在我读书的时候就想玩这个功能很久了那个时候受限于这个功能需要企业或个体户去花费三百块认证服务号等方式, 反正企业或者个体户就难倒一大片了吧? 还要钱, 对于很多程序员是舍不得的, 那么不想认证又不想花费怎么办?

我看到腾讯云开发者社区的统一授权系统当中有一个扫码功能.

当我通过扫码进行授权后,我注意到它调用的并不是常见的微信授权, 而是小程序的授权。一般情况下,微信授权会直接跳转到微信授权页面,用户通过微信客户端完成授权, 但是在这个场景下, 扫码之后, 系统引导我进入的是一个小程序授权页面.

腾讯云云开发授权界面

我们接下来要开发的授权界面,好不好看我设计的.

自定义授权页面
自定义授权页面

通过小程序来实现有什么好处?

嘿嘿! 好处就是不用企业认证、不用个体户认证、不用花费三百大洋, 更加舒服的还能给小程序带来流量支持, 如果你的小程序人流量五百以上还能开通流量主每天赚点小钱.

⚠️ 本期内容涉及较多, 本次的项目实战题材内容较多就本篇文章就有五万字所以我将分两个文章展示, 本篇文章为后端篇,web前端和uniapp移动端将在另外一篇呈现! 点我前往: PC+小程序篇

技术架构图

技术选型: 这个功能关系三端、PC 端、后端、移动端(小程序) 前端部分我不会讲的太深, 主要点是思路和后端 前端使用 Vue3 + Vite 搭建前端简易架构 后端使用 Java SpringBoot3 + Redis 框架搭建服务 移动端使用 UniApp + Vue3 搭建用户交互(可以集成你自己的小程序搞流量) 准备材料:已经注册好小程序拿到对应的 APPID 等信息 准备 Redis 中间件

说了这么多来看看我们接下来要实现的整个流程吧, 在此之前我已经写好了一个案例

案例演示

正常授权: web 端请求二维码 ->> 用户微信扫码跳转到小程序授权页面,主动发起扫码已成功请求 ->> 点击授权 更新二维码状态为成功

取消授权: web 端请求二维码 ->> 用户微信扫码跳转到小程序授权页面,主动发起扫码已成功请求 ->> 点击取消授权 更新二维码授权失败

整体业务解析

我这里画了整个业务的流程图, 先解析整个业务

  1. 页面首次加载,发起调用服务器构建二维码接口
  2. 服务端调用微信小程序服务器获取二维码并且返回给前端,前端渲染
  3. 用户扫码二维码, 跳转到自定义小程序指定授权页面当中,并且更新扫码状态为已扫码
  4. 用户点击授权按钮,发起授权请求给到后端,并且更新状态码为已授权,创建登录 Token(系统唯一登录码)
  5. 前端短轮训检查是否登录成功

后端实现解析

在实现功能前我们肯定要先看对应的文档,第一步我们要构建小程序二维码哪么前往小程序官方文档当中找到小程序专区有没有获取二维码的开放接口.

获取不限制的小程序码 | 微信开放文档

可以看到提供了小程序码的接口,一共有三个, 下面我介绍一下三个的区别

  1. 获取小程序码 - 适用需要的码数少的场景
    1. 该接口用于获取小程序码,适用于需要的码数量较少的业务场景。通过该接口生成的小程序码,永久有效,有数量限制
    2. 与 createQRCode 总共生成的码数量限制为 100,000,请谨慎调用.
  2. 获取不限制的小程序码 - 适用数量无限制
    1. 该接口用于获取小程序码,适用于需要的码数量极多的业务场景。通过该接口生成的小程序码,永久有效,数量暂无限制.
  3. 获取小程序二维码 - 适用业务较少
    1. 适用于需要的码数量较少的业务场景。通过该接口生成的小程序码,永久有效,有数量限制与 wxacode.get 总共生成的码数量限制为 100,000,请谨慎调用.

根据三个特点肯定是第二个最好,没有限制, 所以我们就直接使用第二个获取不限制的小程序码.

可以看到该接口的一些注意事项以及二维码 SCENE 带入这个 scene 字段的值会作为 query 参数传递给小程序/小游戏,用户扫描该码进入小程序/小游戏后,开发者可以获取到二维码中的 scene 值,再做处理逻辑.

那么我们生成二维码的时候需要生成一个这个 scene 的参数,接下来搭建后端来实现这个接口

后端服务实现搭建

代码语言:xml
复制
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>


<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.12.0</version>
</dependency>


<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-all</artifactId>
  <version>5.8.21</version>
</dependency>

接入不限制的小程序码

根据文档的提示我们需要 POST 请求去调用 getwxacodeunlimit接口, 完整如下:

https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN

可以看到接口后面的参数 access_token 鉴权不是放在请求头和 body 当中是放在 url 当中这个请注意

根据文档查看一下该接口的完整请求参数

请求参数

属性

类型

必填

说明

access_token

string

接口调用凭证,该参数为 URL 参数,非 Body 参数。使用getAccessToken 或者 authorizer_access_token

scene

string

最大32个可见字符,只支持数字,大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~,其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式)

page

string

默认是主页,页面 page,例如 pages/index/index,根路径前不要填加 /,不能携带参数(参数请放在scene字段里),如果不填写这个字段,默认跳主页面。scancode_time为系统保留参数,不允许配置

check_path

bool

默认是true,检查page 是否存在,为 true 时 page 必须是已经发布的小程序存在的页面(否则报错);为 false 时允许小程序未发布或者 page 不存在, 但page 有数量上限(60000个)请勿滥用。

env_version

string

要打开的小程序版本。正式版为 "release",体验版为 "trial",开发版为 "develop"。默认是正式版。

width

number

默认430,二维码的宽度,单位 px,最小 280px,最大 1280px

auto_color

bool

自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认 false

可以看到 access_token 和 scene 参数是必填并且最大支持 32 个可见字符且限制了文本格式

其它的参数:

page: 默认主页,我们需要自定义跳转则 必填

check_path: 默认检查是否存在否则会报错, 我们就默认设置 false 自己把控就行

env_version: 打开的小程序版本,我们就先设置开发版或者体验版

width: 二维码宽度, 默认 430 的大小差不多了都是这个大小我们就默认就行了

上面提到了需要 token 调用跟着文档继续找一下 token 怎么拿?

获取 ACCESS_TOKEN

点我前往: token 文档

可以看到请求方式为 get 接口英文名 getAccessToken

请求参数也就三个必填项最后请求的接口

https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}

返回参数

属性

类型

说明

access_token

string

获取到的凭证

expires_in

number

凭证有效时间,单位:秒。目前是7200秒之内的值

代码实现

注意:小程序唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得

巧妙使用腾讯云 AI 代码助手生成请求 ACCESSTOKEN 代码,

然后执行发送请求,可以正常拿到 token 参数

代码语言:java
复制
/**
     * 获取访问令牌
     */
@Test
public void getAccessTokenTest() {
    getAccessToken();
}

public String getAccessToken() {
    String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}";
    String body = HttpUtil.get(StrUtil.format(url, appId, secret));
    JSONObject object = JSONUtil.parseObj(body);
    return object.getStr("access_token");
}

开发获取小程序码

在上面我介绍了小程序码的请求接口如下:

https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN

编写请求参数

在上面我介绍了请求参数都需要哪些这里就不多说直接上代码

代码语言:java
复制
    /**
     * 创建二维码
     */
    @Test
    public void createQrCode() {
        // 组装请求参数
        String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getAccessToken();
        JSONObject jsonParam = new JSONObject();
        // 封装请求对象
        // scene值 - 最大32字符 雪花算法够了
        jsonParam.set("scene", IdUtil.getSnowflakeNextIdStr());
        // 跳往的小程序页面,一般为认证界面
        jsonParam.set("page", "/page/auth/index");
        // 图片宽度,默认为 430
        jsonParam.set("width", "430");
        // 检测页面是否存在,默认为 true
        jsonParam.set("check_path", false);
    }

编写请求工具类

小程序码的接口有点特殊,请求成功它返回 buffer 参数但是你请求失败它返回的 JSON 所以我们要编写自定义发送请求工具

成功
成功
失败
失败

通过发送 POST 请求获取二维码的功能,并且对响应进行处理。

下面定义一个远程请求方法我将讲解该方法做了些什么操作

方法定义

代码语言:java
复制
public String remoteQRCode(String url, JSONObject jsonParam)
  • 该方法名为 remoteQRCode,返回类型为 String,接收两个参数:
    • url:一个字符串,表示请求的目标 URL。
    • jsonParam:一个 JSONObject,包含请求体中需要发送的数据。

代码逻辑

1. 发送 POST 请求
代码语言:java
复制
byte[] responseBytes;
try (cn.hutool.http.HttpResponse response = HttpUtil.createPost(url)
        .body(jsonParam.toString())
        .header("Content-Type", "application/json")
        .execute()) {
  • 使用 Hutool 库中的 HttpUtil.createPost(url) 创建一个 POST 请求,并设置请求体(jsonParam.toString())和请求头(Content-Type: application/json)。
  • execute() 方法会发送请求,并返回一个 HttpResponse 对象,表示 HTTP 响应。
  • 使用 try 语法糖(自动关闭资源)来确保 HttpResponse 在结束后会被正确关闭。
2. 检查响应状态
代码语言:java
复制
if (!response.isOk()) {
    log.error("[remoteQRCode] 请求失败,状态码: {}", response.getStatus());
    throw new RuntimeException("暂未支持小程序扫码登录!");
}
  • response.isOk() 会检查响应的状态码是否表示成功(即状态码 200-299)。如果不是成功状态码,则记录错误日志,并抛出一个 RuntimeException,表示未支持小程序扫码登录。
3. 获取响应内容
代码语言:java
复制
responseBytes = response.bodyBytes();
  • 如果响应成功,使用 response.bodyBytes() 获取响应的字节数据(即二维码图片的二进制数据)。
4. 检查响应内容是否为 JSON 错误信息
代码语言:java
复制
String responseStr = new String(responseBytes, StandardCharsets.UTF_8);
if (JSONUtil.isTypeJSON(responseStr)) {
    log.error("[remoteQRCode] 微信接口返回错误: {}", responseStr);
    throw new RuntimeException("暂未支持小程序扫码登录!");
}
  • 将字节数据转换为字符串 responseStr,并检查它是否为合法的 JSON 格式。
  • 使用 JSONUtil.isTypeJSON() 方法判断返回的数据是否是 JSON 格式。如果是 JSON 格式,说明接口返回了错误信息(比如错误码或错误消息),记录日志并抛出异常。
5. 保存二维码图片
代码语言:java
复制
Files.write(Paths.get("output.png"), responseBytes);
  • 将二维码的字节数据 responseBytes 保存为一个本地文件 output.png,用于确认二维码图片是否损坏或无法读取。
6. 返回 Base64 编码的二维码数据
代码语言:java
复制
return Base64.encode(responseBytes);
  • 将获取到的二维码字节数据 responseBytes 编码为 Base64 格式的字符串并返回。这样返回的字符串可以方便地嵌入到网页或其他地方进行显示。

异常处理

代码语言:java
复制
} catch (Exception e) {
    log.error("获取小程序二维码异常", e);
    throw new RuntimeException("暂未支持小程序扫码登录!");
}
  • 如果在任何步骤中发生异常(如请求失败、响应格式错误等),会捕获异常并记录日志。随后抛出一个 RuntimeException,提示 "暂未支持小程序扫码登录!"。
代码语言:java
复制
    /**
     * 创建二维码
     */
    @Test
    public void createQrCode() {
        // 组装请求参数
        String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + getAccessToken();
        JSONObject jsonParam = new JSONObject();
        // 封装请求对象
        // scene值 - 最大32字符 雪花算法够了
        jsonParam.set("scene", IdUtil.getSnowflakeNextIdStr());
        // 跳往的小程序页面,一般为认证界面
        jsonParam.set("page", "page/auth/index");
        // 图片宽度,默认为 430
        jsonParam.set("width", "430");
        // 检测页面是否存在,默认为 true
        jsonParam.set("check_path", false);
        // 构建发起请求
        String remotedQRCode = remoteQRCode(url, jsonParam);
        System.out.println(remotedQRCode);
    }
    
    
    /**
     * 发送post请求
     *
     * @param url       请求地址
     * @param jsonParam 请求参数
     * @return 响应流
     */
    public String remoteQRCode(String url, JSONObject jsonParam) {
        try {
            // 发送POST请求
            byte[] responseBytes;
            try (cn.hutool.http.HttpResponse response = HttpUtil.createPost(url)
                    .body(jsonParam.toString())
                    .header("Content-Type", "application/json")
                    .execute()) {
                
                // 检查响应状态
                if (!response.isOk()) {
                    log.error("[remoteQRCode] 请求失败,状态码: {}", response.getStatus());
                    throw new RuntimeException("暂未支持小程序扫码登录!");
                }
                
                responseBytes = response.bodyBytes();
            }
            
            // 尝试解析为JSON,判断是否为错误响应
            String responseStr = new String(responseBytes, StandardCharsets.UTF_8);
            if (JSONUtil.isTypeJSON(responseStr)) {
                log.error("[remoteQRCode] 微信接口返回错误: {}", responseStr);
                throw new RuntimeException("暂未支持小程序扫码登录!");
            }
            
            // 保存图片到文件中以确认图片是否损坏
            Files.write(Paths.get("output.png"), responseBytes);
            return Base64.encode(responseBytes);
        } catch (Exception e) {
            log.error("获取小程序二维码异常", e);
            throw new RuntimeException("暂未支持小程序扫码登录!");
        }
    }

正常测试获取小程序码

可以看到成功拿到小程序码 Base64 数据,并且生成了本地图片提供查看是否是损坏,如果没有问题可以将这段代码删除

后端默认返回data:image/png;base64,前端可直接渲染使用

代码语言:txt
复制
/*
* 提示:该行代码过长,系统自动注释不进行高亮。一键复制会移除系统注释 
* data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBA6ahYzrbzjCzxBZVADYr+7v/g0h8O6tQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAGuAa4DASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/iiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigBrukas8jKiKCWd2CqoHUsxIAA7kkCnV/ni/wDB5T+3F+0L4L/aP/Z4/ZL+G/xK8dfDf4ZWvwVb4ueLLHwT4u1nw1H458SeLPGPiDw7py+Ik0SewmvrTw1p/g1/7Ntby5urVZ9YvJ1gilyz/rb/AMGhX7ZPxu/ai/YR+MPgb44eNfFHxI1P9nn40WvhTwh4z8Z+IL/xJ4jl8GeLfCdjr1h4bu9T1V7jUbi18N39rqSaZJdX1y0djqENjEsEFjErgH9ZtFFFABRRRQAUUV5vbfGP4R3nj26+Fdp8Ufh3dfE2ytlvLz4d2/jTw5N44tbVw7LcXHhWPUm1yGIojPvksVAjHmHCEMQD0iiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKK/ND/grX/wUa8O/wDBLT9i3xz+1ZrPgqT4j6xpeueGfBfgbwIuqnQ7fxJ4y8XXzWumwalq6Wl/Lp+k6faW+oatqM0NnPPJb2BtYBHNcpNGAfpfXy5+2j+138Jv2Ef2Zfit+1V8bp9Vj+Hfwm0OHVtVs9AtoL3xBrd9qGo2ei6H4fb1nUbHTrJLm8tbZJJ/NuJ4oY5HH83v/BE//g6B0z/gpJ+0PH+yf+0V81UNV8F+M7jRrefVbvwVqNn4iVNT0rxQujQXF3pNzb3l/Z679hurf7Lpt0IEuv2M/4Lcfslav8Atsf8EwP2svgX4Xhv7vxvc/D2Xx74B0/TnUXGr+NPhle23jrQtCEbqy3A16fQ20ZYCYzJLfRbZYmVXUA+E/8AglX/AMHL/wCyT/wU9+PF3+zXZfDXx/8As/8AxZ1Sz1nVfhrpnjzVvD+vaR8SLLRUlvr3S9K1TQ2iksPFdrokNxrU2jXVi9pJY2GoyWuqzPbLFN/SLX+E3+yJ+0R4u/Y8/ao+BX7SXhON08T/AAN+KnhbxwlhK81t9vg0HVoW1zQbsxgTxQaxpP8AaOjXgUeYsN5KApYYr/cm+FXxH8M/GL4Y/Dz4s+C76HU/CPxM8E+F/Hnhm/t5Y54bvQ/Fmi2WuaZMksTNG+6zvotxRiA24Z4oA76ivkD9q79vz9jX9hyw8Nah+1j+0R8N/ggvjK4e38K2PjDWGj1rXxDNFBd3el6Dp8F9rV1punyTRjUdUjsDp1hvUXV1EzIre/8Aws+K3w1+N/gDwz8VPg/468L/ABK+HHjPT11Xwt418G6xZ694c12wMkkDT6fqdhLNbzeTcQzW1zFuE1rdQzWtzHFcQyRqAegUUUUAFFFFAH+YJ/wejf8AKTX4Lf8AZoHgn/1Z/wAWK/X/AP4Mhv8Ak1j9uD/s4D4ef+q6mr4A/wCD039mj4sD9q39m79p7TfCuua38KfEXwJHwqu/EOkaNf32neG/GHgzxt4l159O17ULaOa30+XW9M8aWlxo63X2dbz7BqCW7zyW8yRfr1/wZr/s3fFP4NfsF/HP4n/EfwzrHhPS/j78c7LW/h9puv6Te6Pqep+F/BfhCy0KXxPFb3yQzS6NrGq3t5baTdCFI7pdKuLmB5raaCVgD+wGiiigAooooA+Jv+CkH7TDfsdfsIftWftK215DY6z8J/gr411/wpNOI3Q+N59Ll0nwREIpQyzNL4r1HR0ERjm3AnMEwBib/G1/ZE+IPx68Z/t//s7/ABB8HeL/ABLqf7QXjX9p74aatp3iw6hdSa/rHjjxN8R9Ilmur26jlSa4XUr68lF/bhhFPazTWwj8phHX+ht/weQ/tQp8Jv8AgnL4D/Z30y88rxB+1D8ZNHs7+3iu4I5j4E+FMcXjTXHltDMLme1m8Sv4Nti6W8lvHI482eCb7Ok/8jn/AAa1/sxS/tG/8Fe/gfrd5ZQ3fhf9nTRPFvx/8R/aGj8tLjwvp66D4PCxyE+dKPG3ibw/crGIpcR2krnygnnxAH+uZF5iwx/aGjMyxJ57R5WIyBB5jJu5Ee7cV3chcZ5r+Tn9o7/g7z/YN+AX7WGt/s7af8LPi18VPAvgnxXceC/Hvx28HXnhiLw5Ya3Y3kdjq83hbw3qV5DqnijSNCuhd22o3st3ok81zZzpp9ndRok037Lf8Fif2yof2DP+CcX7T/7RdtcW0Pi3RfAV34Q+GsNzcG3N38SviDInhDwgICjLNLJp2oar/bskUGZTa6TcuTHFHJLH/jYfs6/BTx1+1V+0V8JPgR4Ktb3W/Hfxv+JvhrwTpqwp591Jf+Ktbgtr7VJ8kDydPt57vVr+Z2WOK1tbieVlRGYAH+7L4W8TaH418MeHfGPhnUIdV8N+LNC0nxL4f1S3JMGo6Lrthb6npd9CSAfKu7G6gnjyAdsgyAcit6vO/hH8OtJ+D3wp+Gvwo0Ka4n0T4ZeAvCXgLSrm9lE13Pp3hHQbDQbS4upgkSyTzW9gksziONWkZiI0GFH8W/8AwUD/AODwuL9mz9snxn8CP2bv2c/BHxw+D3wg8YXHgzx18R9f8ca5ouqeOda0SQ2Pi6DwDHpulzabpOn6NrCXul6drOpR69b6zLpzX0MEen3UJIB/cZRXiv7OHxy8K/tM/AH4NftC+CIrq38JfGn4z1mLT7AhvNPN01ldFQFM8EhUAEAe1UAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFcb8RPiB4R+FHgHxr8T/H+tWvhzwP8ADzwrr3jXxfr96xW00bw34Z0u61jWtSnIBYx2en2dxOVQF32bEVnZQQDsqK/mz/ZM/wCDp3/gmP8AtaftH6Z+zdod58WfhfrPjHxNb+E/hh45+KnhTStD8DePdavp0tNLsl1DTvEGrXnhS41i6YQ6UPFVlpkE7SW0Nxc2l7cLZr/SZQAV+Xv/AAWS/YSX/gox/wAE8fj9+zTp8r2/jnUNDg8d/Cm4E7wQj4o/D+R/EPhCyvdrBJLDXLqCbw9eCYOkMGrvdqomtomX9QqKAP8ABp+G3j74pfsufHfwh8RfCc+qeCPi/wDAr4k6br+lmVJrPU/D/jTwLrySvY31udkqPBqNhLp+pWUgAliNzazKUd1P+6F8EPHN38Vvgn8JPiTq+mjTb/4jfC/wJ411TSJba4t1sbzxb4V0vW77TzaXyJcxRW89/LbrDdRrKI0CyruyK/Ob4x/8ELf+CV3x7/aKH7UvxQ/ZJ8Ea/wDF241ZPEGvX0GoeJNG8MeMPEMU0NxHr3jHwTo2s2PhXxFqxlhD3V1qGlSHU3Zn1Zb+TYyfrRb28FrBBa20MVvbW0Mdvb28KLFDBBCixxQxRoAkcUUaqkaKAqKoVQAAKAP8Y3/gu9+xY/7Cf/BT/wDaX+Eem2TWvgPxV4qk+MvwuZYDDa/8IJ8VZJ/FFlp1mDLPuh8Natc6x4Wyzhy2iF2RFdM/6AX/AAaYftUeJv2iv+CU/hzwL4vfUL3WP2X/AIkeJvgnYa1f3KXJ1Pwelvp/jLwfbRs13Pdovh/SvEv/AAjkSTw20KWWl2UVqsixyMPu/wD4Kl/8ERP2M/8AgrND4L1f49WvjDwX8Tvh9Z3Wk+GPiz8L77SNI8X/APCP3c/2t/DHiD+19H1iw8ReH7e9ae902yv7cS6Td3uozaZc2jaje+f9T/8ABPr/AIJ9fs8f8E0v2eNJ/Zs/Zs0nXLTwZaa5qnivW9b8V6pHrXi7xh4u1qOzh1TxH4j1SGz0+3nvJ7fT7Gzt7ezsbOwsLCztrOztYoYgCAf5+v8AwejeFPH2l/8ABRf4G+LNevprvwF4q/Zi0Sz8AW52rbaXd+HPG/iyHxhZJGLydmuJr7UtM1Ke6a0svOivrW2X7SLEyj9Xf+DKP9qfU/F3wG/ar/ZG8Ra/eag3wi8aeFviz8PtKvr2W4XSvCnxEtb3RvFNlpMEryG00238UeHrTUp7eAQWq6h4hnuY4nuLy7lP6a/8HLP/AASB+Kn/AAVJ/Zu+Fuu/s42+k6n+0T+zt4m1/VfC3hPWtWtNAtPHngzxnaabbeLfDdtrep3dtpGna1bXOh6Nq+jNqRitruW2ubCS/sRc+Y3wz/wa0/8ABFn9r/8A4J7/ABB/aA/aP/bB8I2fws1r4hfD/RPhj8PvhynifRPEWvSWA8Qp4j8ReIvEi+G7/U9L0yNZNL0ey0eykvpr+Xz9Qmnhs1iRbgA/s9ooooA/jY/4PAP2vf22f2Y/gp+y7oX7NHjv4g/CH4W/E7xP8QbL4u/Ef4a6rqfh/XbvWNG0/wAPyeEfBGoeJdJEF/oek6laXuv6qYrW/tX1ufTzbTMbeykhufdP+DSv9q39sr9qb9iX4x6h+1Z4v8b/ABQ0DwD8YrTwx8G/ij8Rb6+1bxRruizeFrG/8S+HW17Uka98RaZ4Y1J7KSz1O7vLy6im1m70+S4eO1hSH+onxn4F8FfEfw9feEviF4Q8MeOfCuqII9S8N+L9B0vxJoV8gOVF3pOsWt5Y3GxvmQywMUYBkKsARc8M+FvDHgrQ9P8ADPg7w7oXhPw3pUIt9L8P+GtJsNC0XToASRDY6Xplva2NpFkk+XBBGuSTjJJoAuavpmj6xYT2Gu6dpuqaXMv+lWWr2lrfWEqD/nvbXkctu6jJ/wBYhAq9FDFbxRwQRRwwwoscUMSLHFFGgCpHHGgVERVAVVUBVAAAAFf55P8AweU/txftC+C/2j/2eP2S/hv8SvHXw3+GVr8FW+Lniyx8E+LtZ8NR+OfEnizxj4g8O6cviJNEnsJr608Naf4Nf+zbW8ubq1WfWLydYIpcs/62/wDBoV+2T8bv2ov2EfjD4G+OHjXxR8SNT/Z5+NFr4U8IeM/GfiC/8SeI5fBni3wnY69YeG7vU9Ve41G4tfDd/a6kmmSXV9ctHY6hDYxLBBYxK4B+MP8Awdg/8FV/2wfht+3boX7InwA+O/xA+CXw0+FPwv8AAvi/xDF8JfHGp+FNW8YePfGhuvEC3ninU/D1xY6rGvh7TI9JttK0OW9NrGzy6rNA8t3bmD+kf/g2K/bm+Nn7df8AwTR07xb+0J4m1rx78UPhH8W/Gnwe1P4ieIZI59a8baRpO6ahY+7v/g0/g0h8O6t6a/g0h8/g0h8O6t6ah/g0h8O6t6ah/g0h8O6t6ah/g0h8O6t6ah/g0h8O6t6ah/g0h8O6t6ah/g0h8O6t6ahO6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjC+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjC/g0h8O6t6ahYzrbzjC+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjC+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6tn+HvFHh7UtWu0Ctf6np2leKoNBu9QnVbm9OlRz3LTXL6ahYzrbzjCzxBZVADYr+7v/g0h8O6tTXE386ahY6ahYzrbzjCzxBZVADYr+7v/g0h8O6tzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6tAP/AAcreL9G8Z/8Fqv22L3RHuHg0XxR6ahYzrbzjCzxBZVADYr+7v/g0h8O6t6ahYzrbzjCzxBZVADYr+7v/g0h8O6t4B8IXxuYGt2XWfCXwp8D6BrCRqxPmW6ahYzrbzjCzxBZVADYr+7v/g0h8O6toP8AwRo+GV1qmmT
*/

拿到 Base64 去浏览器访问看看, 也是可以正常显示没任何问题!!

错误测试获取小程序码

在前面的请求参数描述当中 部分参数如果搞错了会返回 JSON

哪么测试一下是否返回 JSON 从而抛出我们自定义的异常

故意在前面打上 /

通过测试正常的抛出了我们的异常,并且打印了微信的错误信息我们就可以去文档查看对应 code 是什么信息

没问题,也是正确的,这样子一来如果出现错误就可以快速的排查

提取动态配置参数

application.yml 配置当中编写 yml 变量

代码语言:bash
复制
# 小程序登录配置
login:
  enable: true
  appId: xxxxxxxxxx
  secret: xxxxxxxxxxxx
  # 小程序认证界面,扫码后将跳转到这个界面
  authPage: pages/oauth/index
  # 图片宽度,默认为 430
  width: 430
  # 是否页面是否存在 默认是true
  checkPath: false

新增 WechatLoginConfig 配置文件

代码语言:java
复制
/*
 * 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
 * You may change this item but please do not remove the author's signature,
 * otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
 * yangbuyi Copyright (c) https://yby6.com 2024.
 */

package com.yby6.tencentwechatappletloginproject.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;


/**
 * 小程序配置
 *
 * @author Yang Shuai
 * Create By 2024/11/26
 */
@Configuration
@ConfigurationProperties(prefix = "login")
public class WechatLoginConfig {
    /**
     * 是否开启
     */
    public static boolean enable;
    /**
     * 小程序appId
     */
    public static String appId;

    /**
     * 小程序密钥
     */
    public static String secret;

    /**
     * 小程序授权登录页面
     */
    public static String authPage;
    /**
     * 二维码大小 默认430
     */
    public static String width = "430";

    /**
     * 是否检查路径
     */
    public static boolean checkPath;


    public void setEnable(boolean enable) {
        WechatLoginConfig.enable = enable;
    }
    public void setAppId(String appId) {
        WechatLoginConfig.appId = appId;
    }

    public void setSecret(String secret) {
        WechatLoginConfig.secret = secret;
    }

    public void setAuthpage(String authPage) {
        WechatLoginConfig.authPage = authPage;
    }

}

这样子就可以以静态的方式去调用参数了

编写创建小程序码接口

创建文件夹 controller 新增 AppletAuthController控制器

编写 createAppletQrCode接口 该接口是创建唯一小程序码提供调用方使用

代码语言:java
复制
package com.yby6.tencentwechatappletloginproject.controller;

import com.yby6.tencentwechatappletloginproject.domain.R;
import com.yby6.tencentwechatappletloginproject.service.AppletAuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 小程序身份验证控制器
 *
 * @author Yang Shuai
 * Create By 2024/11/26
 */
@RestController
@RequestMapping("applet")
@RequiredArgsConstructor
public class AppletAuthController {

    private final AppletAuthService appletAuthService;

    /**
     * 小程序二维码认证
     *
     * @return {@code R<Object> }
     */
    @PostMapping("createAppletQrCode")
    public R<Object> createAppletQrCode() {
        return appletAuthService.createAppletQrCode();
    }


}

创建 domain文件夹 新增 R 类 用于返回接口参数给前端

代码语言:java
复制
/*
 * 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
 * You may change this item but please do not remove the author's signature,
 * otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
 * yangbuyi Copyright (c) https://yby6.com 2024.
 */

package com.yby6.tencentwechatappletloginproject.domain;

import java.io.Serial;
import java.io.Serializable;

/**
 * 响应信息主体
 *
 * @author 杨不易呀
 * Create By 2024/11/26
 */
public class R<T> implements Serializable {
    /**
     * 成功
     */
    public static final int SUCCESS = 200;
    /**
     * 失败
     */
    public static final int FAIL = 500;
    /**
     * 失败忽略
     */
    public static final int FAILIGNORE = 201;
    @Serial
    private static final long serialVersionUID = 1L;
    /**
     * 消息状态码
     */
    private int code;

    /**
     * 消息内容
     */
    private String msg;

    /**
     * 数据对象
     */
    private T data;

    public static <T> R<T> ok() {
        return restResult(null, SUCCESS, "操作成功");
    }

    public static <T> R<T> ok(T data) {
        return restResult(data, SUCCESS, "操作成功");
    }

    public static <T> R<T> ok(String msg) {
        return restResult(null, SUCCESS, msg);
    }

    public static <T> R<T> ok(String msg, T data) {
        return restResult(data, SUCCESS, msg);
    }

    public static <T> R<T> fail() {
        return restResult(null, FAIL, "操作失败");
    }

    public static <T> R<T> failIgnore() {
        return restResult(null, FAILIGNORE, "操作失败");
    }

    public static <T> R<T> failIgnore(T data) {
        return restResult(data, FAILIGNORE, "操作失败");
    }

    public static <T> R<T> fail(String msg) {
        return restResult(null, FAIL, msg);
    }

    public static <T> R<T> fail(T data) {
        return restResult(data, FAIL, "操作失败");
    }

    public static <T> R<T> fail(String msg, T data) {
        return restResult(data, FAIL, msg);
    }

    public static <T> R<T> fail(int code, String msg) {
        return restResult(null, code, msg);
    }

    public static <T> R<T> check(int row) {
        return row > 0 ? ok() : fail();
    }

    public static <T> R<T> check(boolean isTure) {
        return isTure ? ok() : fail();
    }


    /**
     * 返回警告消息
     *
     * @param msg 返回内容
     * @return 警告消息
     */
    public static <T> R<T> warn(String msg) {
        return restResult(null, 601, msg);
    }

    /**
     * 返回警告消息
     *
     * @param msg  返回内容
     * @param data 数据对象
     * @return 警告消息
     */
    public static <T> R<T> warn(String msg, T data) {
        return restResult(data, 601, msg);
    }

    private static <T> R<T> restResult(T data, int code, String msg) {
        R<T> r = new R<>();
        r.setCode(code);
        r.setData(data);
        r.setMsg(msg);
        return r;
    }

    public static <T> Boolean isError(R<T> ret) {
        return !isSuccess(ret);
    }

    public static <T> Boolean isSuccess(R<T> ret) {
        return R.SUCCESS == ret.getCode();
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

创建 service文件夹 新增 AppletAuthService服务层 提供业务逻辑支撑, 实现 createAppletQrCode创建小程序码方法

代码语言:java
复制
package com.yby6.tencentwechatappletloginproject.service;

import com.yby6.tencentwechatappletloginproject.domain.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * 小程序身份验证服务
 *
 * @author Yang Shuai
 * Create By 2024/11/26
 */
@Slf4j
@Service
public class AppletAuthService {

    /**
     * 小程序二维码认证
     *
     * @return {@code R<Object> }
     */
    public R<Object> createAppletQrCode() {

        // .... TODO 创建二维码

        return null;
    }
}

接着把上面我们写好的获取小程序码测试代码直接复制过来

代码语言:java
复制
    /**
     * 带参数无限个数小程序码接口
     * <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getUnlimitedQRCode.html">...</a>
     *
     * @param accessToken 访问令牌
     * @param scene       场景
     * @return {@code InputStream }
     */
    private String getWechatQrCode(String accessToken, String scene) {
        String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
        JSONObject jsonParam = new JSONObject();
        // 封装请求对象
        // scene值
        jsonParam.set("scene", scene);
        // 跳往的小程序页面,一般为认证界面
        jsonParam.set("page", WechatLoginConfig.authPage);
        // 图片宽度,默认为 430
        jsonParam.set("width", WechatLoginConfig.width);
        // 检测页面是否存在,默认为 true
        jsonParam.set("check_path", WechatLoginConfig.checkPath);
        // 返回请求结果
        return remoteQRCode(url, jsonParam);
    }

    /**
     * 获取微信唯一凭证
     *
     * @param code 代码
     * @return {@link String}
     */
    private String getOpenId(String code) {
        String url = "https://api.weixin.qq.com/sns/jscode2session";
        Map<String, Object> map = new HashMap<>();
        map.put("appId", WechatLoginConfig.appId);
        map.put("secret", WechatLoginConfig.secret);
        map.put("js_code", code);
        map.put("grant_type", "authorization_code");
        String post = HttpUtil.post(url, map);
        log.info("微信返回: {}", post);
        JSONObject obj = JSONUtil.parseObj(post);
        String openid = obj.getStr("openid");
        if (StringUtils.isNoneBlank(openid)) {
            return openid;
        }
        throw new RuntimeException("临时登录凭证错误");
    }


    /**
     * 发送post请求
     *
     * @param url       请求地址
     * @param jsonParam 请求参数
     * @return 响应流
     */
    private String remoteQRCode(String url, JSONObject jsonParam) {
        try {
            // 发送POST请求
            byte[] responseBytes;
            try (cn.hutool.http.HttpResponse response = HttpUtil.createPost(url)
                    .body(jsonParam.toString())
                    .header("Content-Type", "application/json")
                    .execute()) {

                // 检查响应状态
                if (!response.isOk()) {
                    log.error("[remoteQRCode] 请求失败,状态码: {}", response.getStatus());
                    throw new RuntimeException("暂未支持小程序扫码登录!");
                }

                responseBytes = response.bodyBytes();
            }

            // 尝试解析为JSON,判断是否为错误响应
            String responseStr = new String(responseBytes, StandardCharsets.UTF_8);
            if (JSONUtil.isTypeJSON(responseStr)) {
                log.error("[remoteQRCode] 微信接口返回错误: {}", responseStr);
                throw new RuntimeException("暂未支持小程序扫码登录!");
            }

            // 保存图片到文件中以确认图片是否损坏
            // Files.write(Paths.get("output.png"), imageBytes);
            String base64 = Base64.encodeBase64String(responseBytes);
            // 返回带有 MIME 类型的 Base64 字符串
            return "data:image/png;base64," + base64;
        } catch (Exception e) {
            log.error("获取小程序二维码异常", e);
            throw new RuntimeException("暂未支持小程序扫码登录!");
        }
    }


    /**
     * 获取access_token 2小时
     * <a href="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html">...</a>
     *
     * @return access_token
     */
    private String getAccessToken() {
        String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}";
        String body = HttpUtil.get(StrUtil.format(url, WechatLoginConfig.appId, WechatLoginConfig.secret));
        JSONObject object = JSONUtil.parseObj(body);
        return object.getStr("access_token");
    }

改造一下引入 Redisson 中间件持久化AccessToken

我们获取到的 AccessToken 它是有过期时间的, 我们这样子反复去重新拉取微信 TOKEN 可能会拉黑名单一段时间,所以我们需要缓存一下, 后续扫码业务也需要用到缓存, 避免资源浪费

代码语言:java
复制
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.37.0</version>
</dependency>

修改 Yml 配置文件填入 redis 配置参数

代码语言:java
复制
--- # redis 配置
spring:
  data:
    redis:
      # 地址
      host: 127.0.0.1
      # 端口,默认为6379
      port: 6379
      # 数据库索引
      database: 0
      # 密码
      password: '123123123123..'
      # 连接超时时间
      timeout: 10s
      ssl:
        enabled: false

# 分布式redis配置
redisson:
  # redis key前缀
  keyPrefix: xcx_qrcode
  # 线程池数量
  threads: 4
  # Netty线程池数量
  nettyThreads: 8
  # 单节点配置
  singleServerConfig:
    # 客户端名称
    clientName: yangbuyiya
    # 最小空闲连接数
    connectionMinimumIdleSize: 8
    # 连接池大小
    connectionPoolSize: 32
    # 连接空闲超时,单位:毫秒
    idleConnectionTimeout: 10000
    # 命令等待超时,单位:毫秒
    timeout: 3000
    # 发布和订阅连接池大小
    subscriptionConnectionPoolSize: 50

接着在 config 目录下 创建 redis 文件夹

创建配置 Redisson 缓存key前缀处理
代码语言:java
复制
/*
 * 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
 * You may change this item but please do not remove the author's signature,
 * otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
 * yangbuyi Copyright (c) https://yby6.com 2024.
 */

package com.yby6.tencentwechatappletloginproject.config.redis;

import org.apache.commons.lang3.StringUtils;
import org.redisson.api.NameMapper;

/**
 * redis缓存key前缀处理
 *
 * @author 杨不易呀
 * Create By 2024/11/27
 */
public class KeyPrefixHandler implements NameMapper {

    private final String keyPrefix;

    public KeyPrefixHandler(String keyPrefix) {
        //前缀为空 则返回空前缀
        this.keyPrefix = StringUtils.isBlank(keyPrefix) ? "" : keyPrefix + ":";
    }

    /**
     * 增加前缀
     */
    @Override
    public String map(String name) {
        if (StringUtils.isBlank(name)) {
            return null;
        }
        if (StringUtils.isNotBlank(keyPrefix) && !name.startsWith(keyPrefix)) {
            return keyPrefix + name;
        }
        return name;
    }

    /**
     * 去除前缀
     */
    @Override
    public String unmap(String name) {
        if (StringUtils.isBlank(name)) {
            return null;
        }
        if (StringUtils.isNotBlank(keyPrefix) && name.startsWith(keyPrefix)) {
            return name.substring(keyPrefix.length());
        }
        return name;
    }

}
创建配置 Redisson 配置属性
代码语言:java
复制
/*
 * 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
 * You may change this item but please do not remove the author's signature,
 * otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
 * yangbuyi Copyright (c) https://yby6.com 2024.
 */

package com.yby6.tencentwechatappletloginproject.config.redis;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.redisson.config.ReadMode;
import org.redisson.config.SubscriptionMode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * Redisson 配置属性
 *
 * @author 杨不易呀
 */
@Data
@Component
@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties {

    /**
     * redis缓存key前缀
     */
    private String keyPrefix;

    /**
     * 线程池数量,默认值 = 当前处理核数量 * 2
     */
    private int threads;

    /**
     * Netty线程池数量,默认值 = 当前处理核数量 * 2
     */
    private int nettyThreads;

    /**
     * 单机服务配置
     */
    private SingleServerConfig singleServerConfig;

    /**
     * 集群服务配置
     */
    private ClusterServersConfig clusterServersConfig;

    @Data
    @NoArgsConstructor
    public static class SingleServerConfig {

        /**
         * 客户端名称
         */
        private String clientName;

        /**
         * 最小空闲连接数
         */
        private int connectionMinimumIdleSize;

        /**
         * 连接池大小
         */
        private int connectionPoolSize;

        /**
         * 连接空闲超时,单位:毫秒
         */
        private int idleConnectionTimeout;

        /**
         * 命令等待超时,单位:毫秒
         */
        private int timeout;

        /**
         * 发布和订阅连接池大小
         */
        private int subscriptionConnectionPoolSize;

    }

    @Data
    @NoArgsConstructor
    public static class ClusterServersConfig {

        /**
         * 客户端名称
         */
        private String clientName;

        /**
         * master最小空闲连接数
         */
        private int masterConnectionMinimumIdleSize;

        /**
         * master连接池大小
         */
        private int masterConnectionPoolSize;

        /**
         * slave最小空闲连接数
         */
        private int slaveConnectionMinimumIdleSize;

        /**
         * slave连接池大小
         */
        private int slaveConnectionPoolSize;

        /**
         * 连接空闲超时,单位:毫秒
         */
        private int idleConnectionTimeout;

        /**
         * 命令等待超时,单位:毫秒
         */
        private int timeout;

        /**
         * 发布和订阅连接池大小
         */
        private int subscriptionConnectionPoolSize;

        /**
         * 读取模式
         */
        private ReadMode readMode;

        /**
         * 订阅模式
         */
        private SubscriptionMode subscriptionMode;

    }

}
创建配置 Redisson 完整配置
代码语言:java
复制
/*
 * 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
 * You may change this item but please do not remove the author's signature,
 * otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
 * yangbuyi Copyright (c) https://yby6.com 2024.
 */

package com.yby6.tencentwechatappletloginproject.config.redis;

import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * redis配置
 *
 * @author 杨不易呀
 */
@Slf4j
@Configuration
@EnableCaching
@EnableConfigurationProperties(RedissonProperties.class)
public class RedisConfig {

    @Autowired
    private RedissonProperties redissonProperties;

    @Autowired
    private ObjectMapper objectMapper;

    @Bean
    public RedissonAutoConfigurationCustomizer redissonCustomizer() {
        return config -> {
            config.setThreads(redissonProperties.getThreads())
                .setNettyThreads(redissonProperties.getNettyThreads())
                .setCodec(new JsonJacksonCodec(objectMapper));
            RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
            if (ObjectUtil.isNotNull(singleServerConfig)) {
                // 使用单机模式
                config.useSingleServer()
                    //设置redis key前缀
                    .setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
                    .setTimeout(singleServerConfig.getTimeout())
                    .setClientName(singleServerConfig.getClientName())
                    .setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout())
                    .setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize())
                    .setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize())
                    .setConnectionPoolSize(singleServerConfig.getConnectionPoolSize());
            }
            // 集群配置方式 参考下方注释
            RedissonProperties.ClusterServersConfig clusterServersConfig = redissonProperties.getClusterServersConfig();
            if (ObjectUtil.isNotNull(clusterServersConfig)) {
                config.useClusterServers()
                    //设置redis key前缀
                    .setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
                    .setTimeout(clusterServersConfig.getTimeout())
                    .setClientName(clusterServersConfig.getClientName())
                    .setIdleConnectionTimeout(clusterServersConfig.getIdleConnectionTimeout())
                    .setSubscriptionConnectionPoolSize(clusterServersConfig.getSubscriptionConnectionPoolSize())
                    .setMasterConnectionMinimumIdleSize(clusterServersConfig.getMasterConnectionMinimumIdleSize())
                    .setMasterConnectionPoolSize(clusterServersConfig.getMasterConnectionPoolSize())
                    .setSlaveConnectionMinimumIdleSize(clusterServersConfig.getSlaveConnectionMinimumIdleSize())
                    .setSlaveConnectionPoolSize(clusterServersConfig.getSlaveConnectionPoolSize())
                    .setReadMode(clusterServersConfig.getReadMode())
                    .setSubscriptionMode(clusterServersConfig.getSubscriptionMode());
            }
            log.info("初始化 redis 配置");
        };
    }

    /**
     * redis集群配置 yml
     *
     * --- # redis 集群配置(单机与集群只能开启一个另一个需要注释掉)
     * spring:
     *   redis:
     *     cluster:
     *       nodes:
     *         - 192.168.0.100:6379
     *         - 192.168.0.101:6379
     *         - 192.168.0.102:6379
     *     # 密码
     *     password:
     *     # 连接超时时间
     *     timeout: 10s
     *     # 是否开启ssl
     *     ssl: false
     *
     * redisson:
     *   # 线程池数量
     *   threads: 16
     *   # Netty线程池数量
     *   nettyThreads: 32
     *   # 集群配置
     *   clusterServersConfig:
     *     # 客户端名称
     *     clientName: ${ruoyi.name}
     *     # master最小空闲连接数
     *     masterConnectionMinimumIdleSize: 32
     *     # master连接池大小
     *     masterConnectionPoolSize: 64
     *     # slave最小空闲连接数
     *     slaveConnectionMinimumIdleSize: 32
     *     # slave连接池大小
     *     slaveConnectionPoolSize: 64
     *     # 连接空闲超时,单位:毫秒
     *     idleConnectionTimeout: 10000
     *     # 命令等待超时,单位:毫秒
     *     timeout: 3000
     *     # 发布和订阅连接池大小
     *     subscriptionConnectionPoolSize: 50
     *     # 读取模式
     *     readMode: "SLAVE"
     *     # 订阅模式
     *     subscriptionMode: "MASTER"
     */

}

然后启动项目查看是否加载成功

如果无法启动请检查配置是否正确

修改 getAccessToken 避免重复获取
代码语言:java
复制
/**
 * 获取access_token 2小时
 * <a href="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html">...</a>
 *
 * @return access_token
 */
public String getAccessToken() {
    // 存在缓存TOKEN继续使用
    String accessToken = RedisUtils.getCacheObject(GlobalConstants.WECHAT_AUTH_TOKEN + WechatLoginConfig.appId);
    if (StrUtil.isNotBlank(accessToken)) {
        return accessToken;
    }
    String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}";
    String body = HttpUtil.get(StrUtil.format(url, WechatLoginConfig.appId, WechatLoginConfig.secret));
    JSONObject object = JSONUtil.parseObj(body);
    accessToken = object.getStr("access_token");
    long expiresIn = Long.parseLong(object.getStr("expires_in"));
    // 设置过期时间 expiresIn 秒
    RedisUtils.setCacheObject(GlobalConstants.WECHAT_AUTH_TOKEN + WechatLoginConfig.appId,
            accessToken, Duration.ofSeconds(expiresIn));
    return accessToken;
}

装修createQrCodeLogin 接口

在前面我们已经完成了生成小程序码、获取 token、在生成小程序码的时候我们需要传递一个 场景值(唯一 ID)

我们需要生成提供唯一的 ID 放入二维码当中,当扫码的时候小程序拿到的就是 scene=xxxxxx 这样子就知道当前是谁在扫码, ID 生成策略我这里使用 Hutool 工具当中 IdUtil 的Snowflake ID 来确保唯一性,当然你也可以增加一些时间戳签名等.

接着我们得要设计扫码的状态, 在前面的整体业务解析我解析了我们的业务

前端需要短轮训查询二维码的扫码状态, 我们需要定义一下各个阶段的状态, 并且状态对应不同的接口.

  1. 初始状态: wait
  2. 扫码状态: scanned
  3. 取消状态: close
  4. 超时状态: 共用取消状态,前端超时后主动发起取消接口
  5. 授权成功: success

那么我们就要定义一个状态枚举

完善小程序码生成, 在生成二维码时持久化二维码状态, 设计规则:

  1. key 为 scene 值
  2. value 为状态参数

定义缓存 Key

接着根据代码规范我们得要为 Key 设置一个缓存前缀方便查看缓存知道是什么

创建 constants 常量目录 新增 GlobalConstants 类, 分别定义扫码状态、微信 accessToken 上面的修改一下.

代码语言:java
复制
package com.yby6.tencentwechatappletloginproject.constant;

/**
 * 全局常量
 *
 * @author Yang Shuai
 * Create By 2024/11/28
 */
public interface GlobalConstants {
    
    /**
     * 微信登录状态
     */
    String WECHAT_LOGIN_STATE = "wechat_login_state:";
    /**
     * 微信授权TOKEN
     */
    String WECHAT_AUTH_TOKEN = "wechat_auth_token:";
    
}

装修createQrCodeLogin完毕

修改createQrCodeLogin 方法编写生成二维码逻辑

返回 qrcodescene 提供前端渲染

测试创建小程序码接口调用

启动后台使用 api 调试工具对其进行发起请求

  1. 观察返回数据

可以看到数据成功返回, 复制这个 Base64 浏览器访问是否可以正常显示, 不可以则检查代码是否报错.

  1. 观察 Redis 是否成功打入

可以看到成功打入缓存, 过期时间设置为一分钟

上面就完成了接口调用生成小程序二维码,写下来我们编写其他不同状态下的接口.

编写扫码成功接口

扫码接口就很简单了, 前面解析了整个业务, 当用户扫码后会跳转到我们的小程序, 那么在小程序加载的时候获取到二维码当中的 scene 使用 场景值来发起接口调用更新为扫码成功状态.

接口设计 Restful: userScanQrcode/{scene}/applet

编写接口 userScanQrcode方法

修改 AppletAuthService服务实现扫码接口业务

思考? 扫码我们需要判断一些东西?


在这里可以校验改二维码生成时的 IP、和当前二维码的状态必须是 wait 否则都是异常操作. ip 我们前面没记录那么就不操作了感兴趣的同学可以极速操作一下. 那么代码如下:

先校验是否为 null 是那么表示取消了或者超时缓存过期了, 用户刚刚好那一时刻扫码, 为了严谨我们是不允许通过的. 那么在判断是否为刚开始默认的状态,变更那么表示存在恶意访问!

最后就直接更新缓存为 scanned 状态 并且过期时间不变

代码语言:java
复制
/**
 * 修改扫码状态为 已扫码
 *
 * @param scene 场景
 * @return {@code R<Void> }
 */
public R<Void> userScanQrcode(String scene) {
// 1.先判断本次请求是否合法 || 判断状态是否为 wait
String state = RedisUtils.getCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene);
if (null == scene || !ScanStatus.WAIT.getStatus().equals(state)) {
    return R.fail("非法操作,请勿重复扫码!");
}
// 2.修改状态为 已扫码
RedisUtils.setCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene,
                          ScanStatus.SCANNED.getStatus(),
                          true);
return R.ok();
}

简单测试一下执行生成二维码, 把生成的 scene 参数拿过来访问观察环境的状态是否变更

扫码接口完毕那么就继续编写个简单的, 取消授权/超时授权接口.

编写取消/超时接口

这个就非常简单啦, 当扫码成功跳转到小程序授权页面的时候页面有两个按钮一个是授权一个取消授权如下图, 那么点击取消授权主动发起请求取消本次扫码状态更新为 close 直接

接口设计 Restful: userCancelAuth/{scene}/applet

编写接口 userCancelAuth方法

修改 AppletAuthService服务实现扫码接口业务

首先要判断一下是否还存在, 否则就不走后面代码逻辑了, 避免资源浪费

代码语言:java
复制
/**
 * 用户取消认证
 *
 * @param scene 场景值
 * @return {@code R<Void> }
 */
public R<Void> userCancelAuth(String scene) {
// 1.判断是否存在 避免走后面的逻辑了
String state = RedisUtils.getCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene);
if (null == scene) {
    return R.ok();
}
// 2.存在就更新缓存为关闭扫码
RedisUtils.setCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene,
                          ScanStatus.CANCEL.getStatus(),
                          true);
return R.ok();
}

简单测试一下, 调用生成二维码接口, 拿到 scene 参数去主动取消观察缓存信息

接下来就是重中之重的业务, 用户授权扫码.

编写授权成功接口

思考, 那么授权成功接口我们应该注意些什么呢?

在授权的时候, 顾名思义我们要向用户索要授权信息, 拿到是哪个微信用户进行的授权信息, 那么微信就提供了一个 openId 微信用户唯一 ID 字段, 通过此字段我们就可以绑定现有系统当中的用户表或者新增该用户.

那么如何获取到 OpenId 我们看看文档 点我前往

可以看到文档的描述, 需要调用小程序内置的 login 接口函数获取临时凭证 code 码, 根据这个 Code 码我们就可以获取到该用户的 OpenID,

接口调用方式

GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code

需要传递四个参数 小程序 appId、小程序 appSecret、 code、 授权类型

请求参数

需要四个参数这些我们都有直接编写代码吧简简单单的.切菜一样

属性

类型

必填

说明

appid

string

小程序 appId

secret

string

小程序 appSecret

js_code

string

登录时获取的 code,可通过wx.login 获取

grant_type

string

授权类型,此处只需填写 authorization_code

编写单元测试, 直接使用 Hutool 工具发送请求 code 码我们现在还拿不到我们身边设置观察报错信息, 调用成功即可

代码语言:java
复制
@Test
public void getOpenId () {
    String url = "https://api.weixin.qq.com/sns/jscode2session";
    Map<String, Object> map = new HashMap<>();
    map.put("appId", appId);
    map.put("secret", secret);
    map.put("js_code", "这里就是小程序登录的code");
    map.put("grant_type", "authorization_code");
    String post = HttpUtil.post(url, map);
    log.info("微信返回: {}", post);
    JSONObject obj = JSONUtil.parseObj(post);
    String openid = obj.getStr("openid");
}

编写好后直接运行测试, 可以看到调用成功, 返回了 Code 错误的问题

接入获取 OpenId

修改 AppletAuthService服务, 新增 getOpenId 接口

代码语言:java
复制
/**
 * 获取微信唯一凭证
 *
 * @param code 代码
 * @return {@link String}
 */
private String getOpenId(String code) {
String url = "https://api.weixin.qq.com/sns/jscode2session";
    Map<String, Object> map = new HashMap<>();
    map.put("appId", WechatLoginConfig.appId);
    map.put("secret", WechatLoginConfig.secret);
    map.put("js_code", code);
    map.put("grant_type", "authorization_code");
    String post = HttpUtil.post(url, map);
    log.info("微信返回: {}", post);
    JSONObject obj = JSONUtil.parseObj(post);
    String openid = obj.getStr("openid");
    if (StringUtils.isNoneBlank(openid)) {
        return openid;
    }
    throw new RuntimeException("临时登录凭证错误");
}

完善授权接口

接口设计 restful: userAuthWebPro/{scene}/{code}/applet

传递场景值和 code 码

编写 userAuthWebPro方法

代码语言:java
复制
/**
 * 用户点击确认授权
 *
 * @param scene 场景
 * @param code  代码
 * @return {@code R<Void> }
 */
@PostMapping("userAuthWebPro/{scene}/{code}/applet")
public R<Void> userAuthWebPro(@PathVariable String scene, @PathVariable String code) {
    return appletAuthService.userAuthWebPro(scene,code);
}

修改 AppletAuthService服务实现授权接口业务

还是老规矩, 首先要判断一下是否还存在, 否则就不走后面代码逻辑了, 避免资源浪费

然后根据小程序传递的 code码去获取OpenId 这里就是做一些和你的系统绑定业务....我这里就简单记录一下当前登录人.

最后更改扫码状态为授权成功.

代码语言:java
复制
/**
 * 用户点击确认授权
 *
 * @param scene 场景
 * @param code  代码
 * @return {@code R<Void> }
 */
public R<Void> userAuthWebPro(String scene, String code) {
    // 1.判断是否存在 避免走后面的逻辑了
    String status = RedisUtils.getCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene);
    // 只能是扫码状态进来授权否则都认定为异常操作
    if (null == status || !status.equals(ScanStatus.SCANNED.getStatus())) {
        return R.fail("扫码超时,请重新扫码授权!");
    }

    // 2.获取OpenId 这里就是做一些和你的系统绑定业务....我这里就简单记录一下当前登录人
    String openId = getOpenId(code);
    RedisUtils.setCacheObject(GlobalConstants.WECHAT_AUTH_INFO, openId);

    // 3.修改扫码状态为 已授权 提供前端校验
    RedisUtils.setCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene,
                              ScanStatus.SUCCESS.getStatus(), true);
    return R.ok();
}
代码语言:java
复制
/**
 * 微信授权信息
 */
String WECHAT_AUTH_INFO = "wechat_auth_INFO:";

编写前端短轮训校验接口

在上面我们已经将扫码的授权功能后端接口全部开发完毕, 接下来就是最后一个接口提供前端查询扫码状态信息,来判断页面的渲染.

接口设计 Restful: userAuthStatus/{scene}/applet

编写接口 userAuthStatus 方法

检测到success直接删除并且查询到登录信息传递给前端即可, 前端就可以根据 result === success 表示成功那么就显示 cacheObject 的信息参数.

代码语言:java
复制
 /**
     * 浏览器查询 当前二维码状态
     *
     * @param scene 场景值
     * @return {@code R<String> }
     */
    @GetMapping("userAuthStatus/{scene}/applet")
    public R<Object> userAuthStatus(@PathVariable String scene) {
        String result = RedisUtils.getCacheObject(GlobalConstants.WECHAT_LOGIN_STATE + scene);
        // 检测到success直接删除并且传递给调用方
        if (null != result && result.equals(ScanStatus.SUCCESS.getStatus())) {
            RedisUtils.deleteObject(scene);
            // 获取到登录信息
            Object cacheObject = RedisUtils.getCacheObject(GlobalConstants.WECHAT_AUTH_INFO);
            return R.ok(result, cacheObject);
        }
        return R.ok(result);
    }

测试短轮训

调用生成二维码, 然后去调用查询状态接口中途调用取消接口观察查询到的数据是什么.

可以看到初始化 wait 接下来我调用取消接口

ok 没有任何问题, 接下来就可以接入前端实现扫码效果.

抽象公共方法

在我们的服务当中, 这几个方法是不是可以抽出来?

如果以后有新的授权方式可能需要用到并且还解耦合防止工具代码存在业务当中不太好看

所以我们得要抽象出来就行, 就整洁许多.

修改 AppletAuthService服务抽象工具代码

代码语言:java
复制
/*
 * 您可以更改此项目但请不要删除作者署名谢谢,否则根据中华人民共和国版权法进行处理.
 * You may change this item but please do not remove the author's signature,
 * otherwise it will be dealt with according to the Copyright Law of the People's Republic of China.
 * yangbuyi Copyright (c) https://yby6.com 2024.
 */

package com.yby6.tencentwechatappletloginproject.service.base;

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.yby6.tencentwechatappletloginproject.constant.GlobalConstants;
import com.yby6.tencentwechatappletloginproject.config.WechatLoginConfig;
import com.yby6.tencentwechatappletloginproject.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

/**
 * 抽象小程序服务
 *
 * @author 杨不易呀
 * Create By 2024/11/26
 */
@Slf4j
public abstract class AbstractAppletService {

    /**
     * 带参数无限个数小程序码接口
     * <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getUnlimitedQRCode.html">...</a>
     *
     * @param accessToken 访问令牌
     * @param scene       场景
     * @return {@code InputStream }
     */
    public String getWechatQrCode(String accessToken, String scene) {
        String url = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=" + accessToken;
        JSONObject jsonParam = new JSONObject();
        // 封装请求对象
        // scene值
        jsonParam.set("scene", scene);
        // 跳往的小程序页面,一般为认证界面
        jsonParam.set("page", WechatLoginConfig.authPage);
        // 图片宽度,默认为 430
        jsonParam.set("width", WechatLoginConfig.width);
        // 检测页面是否存在,默认为 true
        jsonParam.set("check_path", WechatLoginConfig.checkPath);
        // 返回请求结果
        return remoteQRCode(url, jsonParam);
    }

    /**
     * 获取微信唯一凭证
     *
     * @param code 代码
     * @return {@link String}
     */
    public String getOpenId(String code) {
        String url = "https://api.weixin.qq.com/sns/jscode2session";
        Map<String, Object> map = new HashMap<>();
        map.put("appId", WechatLoginConfig.appId);
        map.put("secret", WechatLoginConfig.secret);
        map.put("js_code", code);
        map.put("grant_type", "authorization_code");
        String post = HttpUtil.post(url, map);
        log.info("微信返回: {}", post);
        JSONObject obj = JSONUtil.parseObj(post);
        String openid = obj.getStr("openid");
        if (StringUtils.isNoneBlank(openid)) {
            return openid;
        }
        throw new RuntimeException("临时登录凭证错误");
    }


    /**
     * 发送post请求
     *
     * @param url       请求地址
     * @param jsonParam 请求参数
     * @return 响应流
     */
    public String remoteQRCode(String url, JSONObject jsonParam) {
        try {
            // 发送POST请求
            byte[] responseBytes;
            try (cn.hutool.http.HttpResponse response = HttpUtil.createPost(url)
                    .body(jsonParam.toString())
                    .header("Content-Type", "application/json")
                    .execute()) {
                
                // 检查响应状态
                if (!response.isOk()) {
                    log.error("[remoteQRCode] 请求失败,状态码: {}", response.getStatus());
                    throw new RuntimeException("暂未支持小程序扫码登录!");
                }
                
                responseBytes = response.bodyBytes();
            }
            
            // 尝试解析为JSON,判断是否为错误响应
            String responseStr = new String(responseBytes, StandardCharsets.UTF_8);
            if (JSONUtil.isTypeJSON(responseStr)) {
                log.error("[remoteQRCode] 微信接口返回错误: {}", responseStr);
                throw new RuntimeException("暂未支持小程序扫码登录!");
            }
            
            // 保存图片到文件中以确认图片是否损坏
            // Files.write(Paths.get("output.png"), imageBytes);
            String base64 = Base64.encodeBase64String(responseBytes);
            // 返回带有 MIME 类型的 Base64 字符串
            return "data:image/png;base64," + base64;
        } catch (Exception e) {
            log.error("获取小程序二维码异常", e);
            throw new RuntimeException("暂未支持小程序扫码登录!");
        }
    }
    
    
    /**
     * 获取access_token 2小时
     * <a href="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html">...</a>
     *
     * @return access_token
     */
    public String getAccessToken() {
        // 存在缓存TOKEN继续使用
        String accessToken = RedisUtils.getCacheObject(GlobalConstants.WECHAT_AUTH_TOKEN + WechatLoginConfig.appId);
        if (StrUtil.isNotBlank(accessToken)) {
            return accessToken;
        }
        String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={}&secret={}";
        String body = HttpUtil.get(StrUtil.format(url, WechatLoginConfig.appId, WechatLoginConfig.secret));
        JSONObject object = JSONUtil.parseObj(body);
        accessToken = object.getStr("access_token");
        long expiresIn = Long.parseLong(object.getStr("expires_in"));
        // 设置过期时间29分钟
        RedisUtils.setCacheObject(GlobalConstants.WECHAT_AUTH_TOKEN + WechatLoginConfig.appId, accessToken, Duration.ofSeconds(expiresIn));
        return accessToken;
    }
    
}

接着集成一下抽象类即可

这样子一看代码就清晰许多~

PC+小程序篇

点我前往

那么到这里我们的后端业务差不多就已经完成了, 是不是很简单, 后面就都是前端的知识点啦, 冲冲冲!!!

最后

本期结束咱们下次再见👋~

🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 推荐
  • 前言
    • 腾讯云云开发授权界面
    • 我们接下来要开发的授权界面,好不好看我设计的.
    • 技术架构图
    • 案例演示
  • 整体业务解析
  • 后端实现解析
  • 后端服务实现搭建
    • 接入不限制的小程序码
      • 请求参数
      • 获取 ACCESS_TOKEN
      • 代码实现
    • 开发获取小程序码
      • 编写请求参数
      • 编写请求工具类
      • 通过发送 POST 请求获取二维码的功能,并且对响应进行处理。
      • 方法定义
      • 代码逻辑
      • 异常处理
      • 正常测试获取小程序码
      • 错误测试获取小程序码
    • 提取动态配置参数
    • 编写创建小程序码接口
      • 改造一下引入 Redisson 中间件持久化AccessToken
      • 装修createQrCodeLogin 接口
      • 定义缓存 Key
      • 装修createQrCodeLogin完毕
      • 测试创建小程序码接口调用
    • 编写扫码成功接口
    • 编写取消/超时接口
    • 编写授权成功接口
      • 接口调用方式
      • 请求参数
      • 接入获取 OpenId
      • 完善授权接口
    • 编写前端短轮训校验接口
      • 测试短轮训
    • 抽象公共方法
  • PC+小程序篇
  • 最后
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档